Field-level encryption implemented
This commit is contained in:
parent
f8bf022b54
commit
a39da26729
2
.env
2
.env
@ -2,4 +2,4 @@ CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http://
|
||||
SERVER1_PORT=5000
|
||||
SERVER2_PORT=5001
|
||||
SERVER3_PORT=5002
|
||||
IMG_TAG=6a57e00-202508051419
|
||||
IMG_TAG=1e039bf-202508061933
|
@ -1,28 +1,16 @@
|
||||
// backend/config/mysqlPool.js
|
||||
import './env.js';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host : process.env.DB_HOST || '127.0.0.1',
|
||||
port : process.env.DB_PORT || 3306,
|
||||
user : process.env.DB_USER || 'root',
|
||||
password : process.env.DB_PASSWORD || '',
|
||||
database : process.env.DB_NAME || 'user_profile_db',
|
||||
waitForConnections : true,
|
||||
connectionLimit : 10,
|
||||
...(process.env.DB_SOCKET ? { socketPath: process.env.DB_SOCKET } : {}),
|
||||
ssl: {
|
||||
minVersion: 'TLSv1.2',
|
||||
rejectUnauthorized: true,
|
||||
ca: process.env.DB_SSL_CA,
|
||||
cert: process.env.DB_SSL_CERT,
|
||||
key: process.env.DB_SSL_KEY,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('[mysqlPool] Using config →',
|
||||
{ host: process.env.DB_HOST, port: process.env.DB_PORT, socket: process.env.DB_SOCKET });
|
||||
|
||||
export default pool;
|
||||
import {
|
||||
query as queryWrapped,
|
||||
exec as executeWrapped,
|
||||
pool as rawPool,
|
||||
getConnection,
|
||||
end
|
||||
} from '../shared/db/withEncryption.js'; // ../ (up one)
|
||||
// then shared/…
|
||||
export default {
|
||||
query : queryWrapped,
|
||||
execute : executeWrapped,
|
||||
getConnection,
|
||||
end,
|
||||
raw: rawPool
|
||||
};
|
||||
|
@ -2,6 +2,7 @@
|
||||
import cron from 'node-cron';
|
||||
import pool from '../config/mysqlPool.js';
|
||||
import { sendSMS } from '../utils/smsService.js';
|
||||
import { query } from '../shared/db/withEncryption.js';
|
||||
|
||||
const BATCH_SIZE = 25; // tune as you like
|
||||
|
||||
|
@ -9,6 +9,7 @@ import path from 'path';
|
||||
import bodyParser from 'body-parser';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken'; // For token-based authentication
|
||||
import { initEncryption } from './shared/crypto/encryption.js';
|
||||
|
||||
import pool from './config/mysqlPool.js'; // adjust path if needed
|
||||
|
||||
@ -36,6 +37,7 @@ if (!JWT_SECRET) {
|
||||
process.exit(1); // container exits, Docker marks it unhealthy
|
||||
}
|
||||
|
||||
await initEncryption();
|
||||
// Test a quick query (optional)
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT 1');
|
||||
@ -241,29 +243,24 @@ app.post('/api/signin', async (req, res) => {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
// IMPORTANT: Use 'row.userProfileId' (from user_profile.id) in the token
|
||||
// so your '/api/user-profile' can decode it and do SELECT * FROM user_profile WHERE id=?
|
||||
const token = jwt.sign({ id: row.userProfileId }, JWT_SECRET, {
|
||||
expiresIn: '2h',
|
||||
});
|
||||
|
||||
// Return user info + token
|
||||
// 'authId' is user_auth's PK, but typically you won't need it on the client
|
||||
// 'row.userProfileId' is the actual user_profile.id
|
||||
res.status(200).json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
id: row.userProfileId, // This is user_profile.id (important if your frontend needs it)
|
||||
user: {
|
||||
firstname: row.firstname,
|
||||
lastname: row.lastname,
|
||||
email: row.email,
|
||||
zipcode: row.zipcode,
|
||||
state: row.state,
|
||||
area: row.area,
|
||||
career_situation: row.career_situation,
|
||||
},
|
||||
});
|
||||
const [profileRows] = await pool.query(
|
||||
'SELECT firstname, lastname, email, zipcode, state, area, career_situation \
|
||||
FROM user_profile WHERE id = ?',
|
||||
[row.userProfileId]
|
||||
);
|
||||
const profile = profileRows[0]; // ← already decrypted
|
||||
|
||||
const token = jwt.sign({ id: row.userProfileId }, JWT_SECRET, { expiresIn: '2h' });
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
id: row.userProfileId,
|
||||
user: profile
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error querying user_auth:', err.message);
|
||||
return res
|
||||
|
@ -19,6 +19,7 @@ import { OpenAI } from 'openai';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import authenticateUser from './utils/authenticateUser.js';
|
||||
import { vectorSearch } from "./utils/vectorSearch.js";
|
||||
import { initEncryption } from './shared/crypto/encryption.js';
|
||||
|
||||
|
||||
// --- Basic file init ---
|
||||
@ -56,6 +57,8 @@ const chatLimiter = rateLimit({
|
||||
// Institution data
|
||||
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
||||
|
||||
await initEncryption();
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const PORT = process.env.SERVER2_PORT || 5001;
|
||||
|
@ -16,11 +16,17 @@ import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import pkg from 'pdfjs-dist';
|
||||
import db from './config/mysqlPool.js';
|
||||
import './jobs/reminderCron.js';
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import Fuse from 'fuse.js';
|
||||
import Stripe from 'stripe';
|
||||
import { createReminder } from './utils/smsService.js';
|
||||
|
||||
import { initEncryption } from './shared/crypto/encryption.js';
|
||||
import { hashForLookup } from './shared/crypto/encryption.js';
|
||||
|
||||
await initEncryption();
|
||||
import './jobs/reminderCron.js';
|
||||
import { cacheSummary } from "./utils/ctxCache.js";
|
||||
|
||||
const rootPath = path.resolve(__dirname, '..'); // one level up
|
||||
@ -53,6 +59,7 @@ function isSafeRedirect(url) {
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
|
||||
const app = express();
|
||||
const { getDocument } = pkg;
|
||||
const bt = "`".repeat(3);
|
||||
@ -133,12 +140,10 @@ async function storeRiskAnalysisInDB({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
app.post('/api/premium/stripe/webhook',
|
||||
app.post(
|
||||
'/api/premium/stripe/webhook',
|
||||
express.raw({ type: 'application/json' }),
|
||||
async (req, res) => {
|
||||
|
||||
let event;
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
@ -152,12 +157,13 @@ app.post('/api/premium/stripe/webhook',
|
||||
}
|
||||
|
||||
const upFlags = async (customerId, premium, pro) => {
|
||||
console.log('[Stripe] upFlags ->', { customerId, premium, pro});
|
||||
const h = hashForLookup(customerId);
|
||||
console.log('[Stripe] upFlags', { customerId, premium, pro });
|
||||
await pool.query(
|
||||
`UPDATE user_profile
|
||||
SET is_premium = ?, is_pro_premium = ?
|
||||
WHERE stripe_customer_id = ?`,
|
||||
[premium, pro, customerId]
|
||||
WHERE stripe_customer_id_hash = ?`,
|
||||
[premium, pro, h]
|
||||
);
|
||||
};
|
||||
|
||||
@ -166,11 +172,11 @@ app.post('/api/premium/stripe/webhook',
|
||||
case 'customer.subscription.updated': {
|
||||
const sub = event.data.object;
|
||||
const pid = sub.items.data[0].price.id;
|
||||
const tier = [process.env.STRIPE_PRICE_PRO_MONTH, process.env.STRIPE_PRICE_PRO_YEAR]
|
||||
.includes(pid) ? 'pro' : 'premium';
|
||||
const tier = [process.env.STRIPE_PRICE_PRO_MONTH,
|
||||
process.env.STRIPE_PRICE_PRO_YEAR].includes(pid)
|
||||
? 'pro' : 'premium';
|
||||
await upFlags(sub.customer, tier === 'premium', tier === 'pro');
|
||||
break;
|
||||
console.log('[Stripe] flags updated', { id: sub.customer, tier });
|
||||
}
|
||||
case 'customer.subscription.deleted': {
|
||||
const sub = event.data.object;
|
||||
@ -178,10 +184,9 @@ app.post('/api/premium/stripe/webhook',
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// ignore everything else
|
||||
// Ignore everything else
|
||||
}
|
||||
|
||||
res.status(200).end();
|
||||
res.sendStatus(200);
|
||||
}
|
||||
);
|
||||
|
||||
@ -258,28 +263,55 @@ const pool = db;
|
||||
* Returns the user’s stripe_customer_id (or null) given req.id.
|
||||
* Creates a new Stripe Customer & saves it if missing.
|
||||
* ----------------------------------------------------------------- */
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* Returns the user’s Stripe customer‑id (decrypted) given req.id.
|
||||
* If the user has no customer, it creates one, saves BOTH the
|
||||
* encrypted id and its deterministic hash, then returns the id.
|
||||
* ----------------------------------------------------------------- */
|
||||
async function getOrCreateStripeCustomerId(req) {
|
||||
// 1) look up current row
|
||||
/* 1 ── look up existing row (wrapped pool auto‑decrypts) */
|
||||
const [[row]] = await pool.query(
|
||||
'SELECT stripe_customer_id FROM user_profile WHERE id = ?',
|
||||
`SELECT stripe_customer_id
|
||||
FROM user_profile
|
||||
WHERE id = ?`,
|
||||
[req.id]
|
||||
);
|
||||
if (row?.stripe_customer_id) return row.stripe_customer_id;
|
||||
|
||||
// 2) create → cache → return
|
||||
const customer = await stripe.customers.create({ metadata: { userId: req.id } });
|
||||
if (row?.stripe_customer_id) {
|
||||
return row.stripe_customer_id; // already have it
|
||||
}
|
||||
|
||||
/* 2 ── create customer in Stripe */
|
||||
const customer = await stripe.customers.create({
|
||||
metadata: { userId: String(req.id) }
|
||||
});
|
||||
|
||||
/* 3 ── store encrypted id **and** deterministic hash */
|
||||
const h = hashForLookup(customer.id);
|
||||
|
||||
await pool.query(
|
||||
'UPDATE user_profile SET stripe_customer_id = ? WHERE id = ?',
|
||||
[customer.id, req.id]
|
||||
`UPDATE user_profile
|
||||
SET stripe_customer_id = ?,
|
||||
stripe_customer_id_hash = ?
|
||||
WHERE id = ?`,
|
||||
[customer.id, h, req.id]
|
||||
);
|
||||
|
||||
return customer.id;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const priceMap = {
|
||||
premium: { monthly: process.env.STRIPE_PRICE_PREMIUM_MONTH,
|
||||
annual : process.env.STRIPE_PRICE_PREMIUM_YEAR },
|
||||
pro : { monthly: process.env.STRIPE_PRICE_PRO_MONTH,
|
||||
annual : process.env.STRIPE_PRICE_PRO_YEAR }
|
||||
premium: {
|
||||
monthly: process.env.STRIPE_PRICE_PREMIUM_MONTH,
|
||||
annual : process.env.STRIPE_PRICE_PREMIUM_YEAR
|
||||
},
|
||||
pro: {
|
||||
monthly: process.env.STRIPE_PRICE_PRO_MONTH,
|
||||
annual : process.env.STRIPE_PRICE_PRO_YEAR
|
||||
}
|
||||
};
|
||||
|
||||
app.get('/api/premium/subscription/status', authenticatePremiumUser, async (req, res) => {
|
||||
|
140
backend/shared/crypto/encryption.js
Normal file
140
backend/shared/crypto/encryption.js
Normal file
@ -0,0 +1,140 @@
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
AES‑GCM field‑level encryption helper backed by Google Cloud KMS
|
||||
---------------------------------------------------------------- */
|
||||
|
||||
import {
|
||||
randomBytes,
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHmac
|
||||
} from 'crypto';
|
||||
import { KeyManagementServiceClient } from '@google-cloud/kms';
|
||||
import { open as fsOpen, readFile, mkdir, writeFile } from 'fs/promises';
|
||||
import { constants as FS } from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
/* ── constants ─────────────────────────────────────────────── */
|
||||
const ALGO = 'aes-256-gcm'; // symmetric cipher
|
||||
const IV_LEN = 12; // 96‑bit nonce for GCM
|
||||
const TAG_LEN = 16; // 128‑bit auth‑tag
|
||||
const MAGIC = 'gcm:'; // prefix to mark ciphertext
|
||||
|
||||
/* ── env config (injected via docker‑compose) ──────────────── */
|
||||
const KMS_KEY = (process.env.KMS_KEY_NAME || '').trim(); // projects/*/locations/*/keyRings/*/cryptoKeys/*
|
||||
const EDEK_PATH = (process.env.DEK_PATH || '').trim(); // e.g. /run/secrets/dev/dek.enc
|
||||
|
||||
let dek; // in‑memory data‑encryption‑key
|
||||
let initPromise = null;
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
One‑time DEK unwrap (or generate + wrap)
|
||||
──────────────────────────────────────────────────────────── */
|
||||
export async function initEncryption () {
|
||||
/* fast path ─ already done in this process */
|
||||
if (dek) return;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
const kms = new KeyManagementServiceClient();
|
||||
const dir = path.dirname(EDEK_PATH);
|
||||
|
||||
/* 1 ── try the happy path: read existing wrapped key */
|
||||
try {
|
||||
const edek = await readFile(EDEK_PATH);
|
||||
const [resp] = await kms.decrypt({ name: KMS_KEY, ciphertext: edek });
|
||||
dek = resp.plaintext;
|
||||
process.env.DEBUG_ENCRYPTION === 'true'
|
||||
&& console.log('[ENCRYPT] DEK unwrapped from KMS');
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err; // genuine failure
|
||||
}
|
||||
|
||||
/* 2 ── file does not exist ⇒ we’re in a race to create it */
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
try {
|
||||
/* exclusive create – succeeds for exactly ONE container */
|
||||
const fh = await fsOpen(EDEK_PATH, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL, 0o600);
|
||||
try {
|
||||
dek = randomBytes(32); // 256‑bit AES key
|
||||
const [wrapResp] = await kms.encrypt({ name: KMS_KEY, plaintext: dek });
|
||||
await fh.writeFile(wrapResp.ciphertext);
|
||||
process.env.DEBUG_ENCRYPTION === 'true'
|
||||
&& console.log('[ENCRYPT] New DEK generated & wrapped');
|
||||
} finally {
|
||||
await fh.close();
|
||||
}
|
||||
return; // we are the winner
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err; // unexpected error
|
||||
/* another container won – fall through to retry loop */
|
||||
}
|
||||
|
||||
/* 3 ── wait until the winner finishes writing, then read */
|
||||
const maxRetries = 30; // ~3 s total
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const edek = await readFile(EDEK_PATH);
|
||||
const [resp] = await kms.decrypt({ name: KMS_KEY, ciphertext: edek });
|
||||
dek = resp.plaintext;
|
||||
process.env.DEBUG_ENCRYPTION === 'true'
|
||||
&& console.log('[ENCRYPT] DEK unwrapped after retry');
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
await new Promise(r => setTimeout(r, 100)); // back‑off 100 ms
|
||||
}
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${EDEK_PATH} to appear`);
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
Symmetric encryption helpers
|
||||
──────────────────────────────────────────────────────────── */
|
||||
export function encrypt (plain) {
|
||||
if (plain == null) return null; // leave NULLs untouched
|
||||
if (isEncrypted(plain)) return plain; // already encrypted
|
||||
|
||||
const iv = randomBytes(IV_LEN);
|
||||
const cipher = createCipheriv(ALGO, dek, iv);
|
||||
const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return MAGIC + Buffer.concat([iv, tag, ct]).toString('base64');
|
||||
}
|
||||
|
||||
export function decrypt (val) {
|
||||
if (!isEncrypted(val)) return val; // fast‑exit for plain text
|
||||
|
||||
const buf = Buffer.from(val.slice(MAGIC.length), 'base64');
|
||||
const iv = buf.subarray(0, IV_LEN);
|
||||
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
||||
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
||||
|
||||
const decipher = createDecipheriv(ALGO, dek, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
Deterministic HMAC (for indexed look‑ups, optional)
|
||||
──────────────────────────────────────────────────────────── */
|
||||
export function hashForLookup (plain) {
|
||||
if (plain == null) return null;
|
||||
const h = createHmac('sha256', dek).update(String(plain)).digest('hex');
|
||||
return h; // 64‑char hex
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
Utility: is this value an AES‑GCM ciphertext we created?
|
||||
──────────────────────────────────────────────────────────── */
|
||||
export function isEncrypted (val) {
|
||||
return typeof val === 'string'
|
||||
&& val.startsWith(MAGIC)
|
||||
&& /^[A-Za-z0-9+/]+={0,2}$/.test(val.slice(MAGIC.length));
|
||||
}
|
158
backend/shared/db/withEncryption.js
Normal file
158
backend/shared/db/withEncryption.js
Normal file
@ -0,0 +1,158 @@
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
Drop‑in replacement for mysql2 that transparently encrypts /
|
||||
decrypts selected columns, using helpers above.
|
||||
---------------------------------------------------------------- */
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
import {
|
||||
encrypt,
|
||||
decrypt,
|
||||
isEncrypted,
|
||||
initEncryption
|
||||
} from '../crypto/encryption.js';
|
||||
|
||||
/* ── map of columns that must be protected ─────────────────── */
|
||||
const TABLE_MAP = {
|
||||
user_profile : [
|
||||
'username', 'firstname', 'lastname', 'email', 'phone_e164',
|
||||
'zipcode', 'stripe_customer_id',
|
||||
'interest_inventory_answers', 'riasec_scores',
|
||||
'career_priorities', 'career_list'
|
||||
],
|
||||
financial_profiles : [
|
||||
'current_salary','additional_income','monthly_expenses',
|
||||
'monthly_debt_payments','retirement_savings','emergency_fund',
|
||||
'retirement_contribution','emergency_contribution',
|
||||
'extra_cash_emergency_pct','extra_cash_retirement_pct'
|
||||
],
|
||||
career_profiles : [
|
||||
'planned_monthly_expenses','planned_monthly_debt_payments',
|
||||
'planned_monthly_retirement_contribution','planned_monthly_emergency_contribution',
|
||||
'planned_surplus_emergency_pct','planned_surplus_retirement_pct',
|
||||
'planned_additional_income','career_goals','desired_retirement_income_monthly'
|
||||
],
|
||||
college_profiles : [
|
||||
'selected_school','selected_program','annual_financial_aid',
|
||||
'existing_college_debt','tuition','tuition_paid','loan_deferral_until_graduation',
|
||||
'loan_term','interest_rate','extra_payment','expected_salary'
|
||||
],
|
||||
milestones : ['title','description','date','progress'],
|
||||
tasks : ['title','description','due_date','status'],
|
||||
reminders : ['phone_e164','message_body'],
|
||||
milestone_impacts : ['amount','impact_type'],
|
||||
ai_risk_analysis : ['reasoning','risk_level'],
|
||||
ai_generated_ksa : ['knowledge_json','abilities_json','skills_json'],
|
||||
context_cache : ['ctx_text']
|
||||
};
|
||||
|
||||
/* ── initialise KMS unwrap once ─────────────────────────────── */
|
||||
async function ensureCryptoReady () { await initEncryption(); }
|
||||
|
||||
/* ── mysql connection pool (uses env injected by docker) ────── */
|
||||
export const pool = mysql.createPool({
|
||||
host : process.env.DB_HOST,
|
||||
port : process.env.DB_PORT,
|
||||
user : process.env.DB_USER,
|
||||
password : process.env.DB_PASSWORD,
|
||||
database : process.env.DB_NAME,
|
||||
waitForConnections : true,
|
||||
connectionLimit : 5,
|
||||
ssl : {
|
||||
ca : process.env.DB_SSL_CA,
|
||||
key : process.env.DB_SSL_KEY,
|
||||
cert : process.env.DB_SSL_CERT
|
||||
}
|
||||
});
|
||||
|
||||
/* ── tiny helpers to parse SQL (works for *your* queries) ───── */
|
||||
function extractTables (sql) {
|
||||
return [...new Set(
|
||||
[...sql.matchAll(/\b(?:from|join|into|update)\s+`?([a-z0-9_]+)`?/ig)]
|
||||
.map(m => m[1].toLowerCase())
|
||||
)];
|
||||
}
|
||||
|
||||
function extractColumn(sql, paramIndex) {
|
||||
const normalized = sql.replace(/\s+/g, ' ').toLowerCase();
|
||||
|
||||
// INSERT INTO table (col1, col2, ...) VALUES (?, ?, ...)
|
||||
if (normalized.includes('insert into')) {
|
||||
const m = normalized.match(/\(\s*([^)]+?)\s*\)\s*values/i);
|
||||
if (!m || !m[1]) {
|
||||
console.warn(`[DAO] INSERT column extraction failed for param ${paramIndex}`);
|
||||
return null;
|
||||
}
|
||||
const colList = m[1].split(',').map(s => s.replace(/`/g, '').trim());
|
||||
const col = colList[paramIndex] ?? null;
|
||||
console.log(`[DAO] Param ${paramIndex} maps to column: ${col}`);
|
||||
return col;
|
||||
}
|
||||
|
||||
// UPDATE table SET col1 = ?, col2 = ? WHERE ...
|
||||
if (normalized.includes('update')) {
|
||||
const m = normalized.match(/set\s+(.*?)\s*(where|$)/);
|
||||
if (!m || !m[1]) {
|
||||
console.warn(`[DAO] UPDATE column extraction failed for param ${paramIndex}`);
|
||||
return null;
|
||||
}
|
||||
const colList = m[1].split(',').map(s =>
|
||||
s.split('=')[0].replace(/`/g, '').trim()
|
||||
);
|
||||
const col = colList[paramIndex] ?? null;
|
||||
console.log(`[DAO] Param ${paramIndex} maps to column: ${col}`);
|
||||
return col;
|
||||
}
|
||||
|
||||
// SELECT ... WHERE col = ?
|
||||
if (normalized.includes('where')) {
|
||||
const m = normalized.match(/where\s+([a-z0-9_]+)\s*=/i);
|
||||
const col = m?.[1]?.trim() ?? null;
|
||||
console.log(`[DAO] Param ${paramIndex} maps to column (WHERE clause): ${col}`);
|
||||
return col;
|
||||
}
|
||||
|
||||
console.log(`[DAO] No column mapping for param ${paramIndex} — unsupported SQL`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function decryptRow (row, tables) {
|
||||
for (const t of tables) {
|
||||
const encSet = new Set((TABLE_MAP[t] ?? []).map(c => c.toLowerCase()));
|
||||
for (const k of Object.keys(row)) {
|
||||
if (!encSet.has(k.toLowerCase())) continue;
|
||||
const val = row[k];
|
||||
if (val != null && isEncrypted(val)) row[k] = decrypt(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
Replacement for pool.execute / pool.query
|
||||
──────────────────────────────────────────────────────────── */
|
||||
export async function exec (sql, params = []) {
|
||||
await ensureCryptoReady();
|
||||
|
||||
const tables = extractTables(sql);
|
||||
const encryptNeeded = (col) => tables.some(t => TABLE_MAP[t]?.includes(col));
|
||||
|
||||
const encParams = params.map((v, i) => {
|
||||
const col = extractColumn(sql, i);
|
||||
return (col && encryptNeeded(col) && v != null && !isEncrypted(v))
|
||||
? encrypt(v)
|
||||
: v;
|
||||
});
|
||||
|
||||
const [rows, fields] = await pool.execute(sql, encParams);
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
for (const row of rows) decryptRow(row, tables);
|
||||
}
|
||||
return [rows, fields];
|
||||
}
|
||||
|
||||
/* ── mysql‑like façade so existing code keeps working ───────── */
|
||||
export const query = exec;
|
||||
export const getConnection = () => pool.getConnection();
|
||||
export const end = () => pool.end();
|
||||
export const poolRaw = pool; // escape hatch
|
@ -1,6 +1,7 @@
|
||||
// utils/ctxCache.js
|
||||
import crypto from "node:crypto";
|
||||
import pool from "../config/mysqlPool.js";
|
||||
import { query } from '../shared/db/withEncryption.js';
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
|
@ -1,9 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# CONFIG – adjust only these 4 if needed
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# ───────────────────────── config ─────────────────────────
|
||||
ENV=dev
|
||||
PROJECT=aptivaai-dev
|
||||
ROOT=/home/jcoakley/aptiva-dev1-app
|
||||
@ -12,10 +7,13 @@ REG=us-central1-docker.pkg.dev/${PROJECT}/aptiva-repo
|
||||
ENV_FILE="${ROOT}/.env"
|
||||
SECRETS=(
|
||||
JWT_SECRET OPENAI_API_KEY ONET_USERNAME ONET_PASSWORD
|
||||
STRIPE_SECRET_KEY STRIPE_PUBLISHABLE_KEY STRIPE_WH_SECRET STRIPE_PRICE_PREMIUM_MONTH STRIPE_PRICE_PREMIUM_YEAR STRIPE_PRICE_PRO_MONTH STRIPE_PRICE_PRO_YEAR
|
||||
DB_HOST DB_PORT DB_USER DB_PASSWORD
|
||||
DB_SSL_CERT DB_SSL_KEY DB_SSL_CA
|
||||
TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_MESSAGING_SERVICE_SID
|
||||
STRIPE_SECRET_KEY STRIPE_PUBLISHABLE_KEY STRIPE_WH_SECRET \
|
||||
STRIPE_PRICE_PREMIUM_MONTH STRIPE_PRICE_PREMIUM_YEAR \
|
||||
STRIPE_PRICE_PRO_MONTH STRIPE_PRICE_PRO_YEAR \
|
||||
DB_HOST DB_NAME DB_PORT DB_USER DB_PASSWORD \
|
||||
DB_SSL_CERT DB_SSL_KEY DB_SSL_CA \
|
||||
TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_MESSAGING_SERVICE_SID \
|
||||
KMS_KEY_NAME DEK_PATH
|
||||
)
|
||||
|
||||
cd "$ROOT"
|
||||
@ -23,14 +21,11 @@ echo "🛠 Building front‑end bundle"
|
||||
npm ci --silent
|
||||
npm run build
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 1. Build → Push → Stamp .env
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# ───────────────────── build & push images ─────────────────────
|
||||
TAG="$(git rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M)"
|
||||
echo "🔨 Building & pushing containers (tag = ${TAG})"
|
||||
|
||||
for svc in server1 server2 server3 nginx; do
|
||||
docker build -f Dockerfile."$svc" -t "${REG}/${svc}:${TAG}" .
|
||||
docker build -f "Dockerfile.${svc}" -t "${REG}/${svc}:${TAG}" .
|
||||
docker push "${REG}/${svc}:${TAG}"
|
||||
done
|
||||
|
||||
@ -48,28 +43,18 @@ printf "%s" "${TAG}" | gcloud secrets versions add IMG_TAG --data-file=- --proje
|
||||
|
||||
echo "📦 IMG_TAG pushed to Secret Manager (no suffix)"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 2. Pull secrets into runtime (never written to disk)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# ───────────────────── pull secrets (incl. KMS key path) ───────
|
||||
echo "🔐 Pulling secrets from Secret Manager"
|
||||
for S in "${SECRETS[@]}"; do
|
||||
export "$S"="$(gcloud secrets versions access latest \
|
||||
--secret="${S}_${ENV}" \
|
||||
--project="$PROJECT")"
|
||||
--secret="${S}_${ENV}" --project="$PROJECT")"
|
||||
done
|
||||
|
||||
export FROM_SECRETS_MANAGER=true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 3. Re-create the container stack
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# ───────────────────── compose up ───────────────────────────────
|
||||
preserve=IMG_TAG,FROM_SECRETS_MANAGER,REACT_APP_API_URL,$(IFS=,; echo "${SECRETS[*]}")
|
||||
|
||||
echo "🚀 docker compose up -d (env: $preserve)"
|
||||
sudo --preserve-env="$preserve" docker compose up -d --force-recreate 2> >(grep -v 'WARN
|
||||
sudo --preserve-env="$preserve" docker compose up -d --force-recreate \
|
||||
2> >(grep -v 'WARN \[0000\]')
|
||||
|
||||
\[0000\]
|
||||
|
||||
')
|
||||
|
||||
echo "✅ Deployment finished"
|
||||
echo "✅ Deployment finished"
|
@ -6,7 +6,7 @@ x-env: &with-env
|
||||
env_file:
|
||||
- .env # committed, non‑secret
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
services:
|
||||
# ───────────────────────────── server1 ─────────────────────────────
|
||||
server1:
|
||||
@ -14,6 +14,8 @@ services:
|
||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
||||
expose: ["${SERVER1_PORT}"]
|
||||
environment:
|
||||
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||
DEK_PATH: ${DEK_PATH}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
DB_HOST: ${DB_HOST}
|
||||
DB_PORT: ${DB_PORT}
|
||||
@ -29,6 +31,7 @@ services:
|
||||
volumes:
|
||||
- ./salary_info.db:/app/salary_info.db:ro
|
||||
- ./user_profile.db:/app/user_profile.db
|
||||
- dek-vol:/run/secrets/dev
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
||||
interval: 30s
|
||||
@ -41,6 +44,8 @@ services:
|
||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG}
|
||||
expose: ["${SERVER2_PORT}"]
|
||||
environment:
|
||||
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||
DEK_PATH: ${DEK_PATH}
|
||||
ONET_USERNAME: ${ONET_USERNAME}
|
||||
ONET_PASSWORD: ${ONET_PASSWORD}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
@ -57,7 +62,7 @@ services:
|
||||
- ./public:/app/public:ro
|
||||
- ./salary_info.db:/app/salary_info.db:ro
|
||||
- ./user_profile.db:/app/user_profile.db
|
||||
|
||||
- dek-vol:/run/secrets/dev
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"]
|
||||
interval: 30s
|
||||
@ -70,6 +75,8 @@ services:
|
||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG}
|
||||
expose: ["${SERVER3_PORT}"]
|
||||
environment:
|
||||
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||
DEK_PATH: ${DEK_PATH}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
||||
@ -96,6 +103,7 @@ services:
|
||||
volumes:
|
||||
- ./salary_info.db:/app/salary_info.db:ro
|
||||
- ./user_profile.db:/app/user_profile.db
|
||||
- dek-vol:/run/secrets/dev
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"]
|
||||
interval: 30s
|
||||
@ -121,4 +129,8 @@ networks:
|
||||
name: aptiva_default
|
||||
aptiva-shared:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
dek-vol:
|
||||
driver: local
|
||||
|
||||
|
136
migrate_encrypted_columns.sql
Normal file
136
migrate_encrypted_columns.sql
Normal file
@ -0,0 +1,136 @@
|
||||
/* ───────────────────────── user_profile ───────────────────────── */
|
||||
ALTER TABLE user_profile
|
||||
MODIFY firstname VARCHAR(400),
|
||||
MODIFY lastname VARCHAR(400),
|
||||
MODIFY email VARCHAR(512),
|
||||
MODIFY phone_e164 VARCHAR(128),
|
||||
MODIFY zipcode VARCHAR(64),
|
||||
MODIFY stripe_customer_id VARCHAR(128),
|
||||
MODIFY interest_inventory_answers MEDIUMTEXT,
|
||||
MODIFY riasec_scores VARCHAR(768),
|
||||
MODIFY career_priorities MEDIUMTEXT,
|
||||
MODIFY career_list MEDIUMTEXT;
|
||||
|
||||
/* ───────────────────────── financial_profiles ─────────────────── */
|
||||
ALTER TABLE financial_profiles
|
||||
MODIFY current_salary VARCHAR(128),
|
||||
MODIFY additional_income VARCHAR(128),
|
||||
MODIFY monthly_expenses VARCHAR(128),
|
||||
MODIFY monthly_debt_payments VARCHAR(128),
|
||||
MODIFY retirement_savings VARCHAR(128),
|
||||
MODIFY emergency_fund VARCHAR(128),
|
||||
MODIFY retirement_contribution VARCHAR(128),
|
||||
MODIFY emergency_contribution VARCHAR(128),
|
||||
MODIFY extra_cash_emergency_pct VARCHAR(64),
|
||||
MODIFY extra_cash_retirement_pct VARCHAR(64);
|
||||
|
||||
/* ───────────────────────── career_profiles ────────────────────── */
|
||||
ALTER TABLE career_profiles
|
||||
MODIFY planned_monthly_expenses VARCHAR(128),
|
||||
MODIFY planned_monthly_debt_payments VARCHAR(128),
|
||||
MODIFY planned_monthly_retirement_contribution VARCHAR(128),
|
||||
MODIFY planned_monthly_emergency_contribution VARCHAR(128),
|
||||
MODIFY planned_surplus_emergency_pct VARCHAR(64),
|
||||
MODIFY planned_surplus_retirement_pct VARCHAR(64),
|
||||
MODIFY planned_additional_income VARCHAR(128),
|
||||
MODIFY career_goals MEDIUMTEXT,
|
||||
MODIFY desired_retirement_income_monthly VARCHAR(128);
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
college_profiles – migrate for encrypted VARCHAR columns
|
||||
────────────────────────────────────────────────────────────────
|
||||
Adjust index names below if SHOW INDEX tells you they differ */
|
||||
|
||||
ALTER TABLE user_profile
|
||||
ADD COLUMN stripe_customer_id_hash CHAR(64) NULL,
|
||||
ADD INDEX idx_customer_hash (stripe_customer_id_hash);
|
||||
|
||||
/*───────────────────
|
||||
STEP 1 – drop old indexes
|
||||
───────────────────*/
|
||||
SHOW INDEX FROM college_profiles\G
|
||||
|
||||
ALTER TABLE college_profiles
|
||||
DROP FOREIGN KEY fk_college_profiles_user,
|
||||
DROP FOREIGN KEY fk_college_profiles_career;
|
||||
|
||||
ALTER TABLE college_profiles
|
||||
DROP INDEX user_id;
|
||||
|
||||
/*───────────────────
|
||||
STEP 2 – widen columns
|
||||
(512‑byte text columns ≈ 684 B once encrypted/Base64‑encoded)
|
||||
───────────────────*/
|
||||
ALTER TABLE college_profiles
|
||||
MODIFY selected_school VARCHAR(512),
|
||||
MODIFY selected_program VARCHAR(512),
|
||||
MODIFY annual_financial_aid VARCHAR(128),
|
||||
MODIFY existing_college_debt VARCHAR(128),
|
||||
MODIFY tuition VARCHAR(128),
|
||||
MODIFY tuition_paid VARCHAR(128),
|
||||
MODIFY loan_deferral_until_graduation VARCHAR(64),
|
||||
MODIFY loan_term VARCHAR(64),
|
||||
MODIFY interest_rate VARCHAR(64),
|
||||
MODIFY extra_payment VARCHAR(128),
|
||||
MODIFY expected_salary VARCHAR(128);
|
||||
|
||||
ALTER TABLE college_profiles
|
||||
ADD UNIQUE KEY ux_user_school_prog (
|
||||
user_id,
|
||||
career_profile_id,
|
||||
selected_school(192),
|
||||
selected_program(192),
|
||||
program_type
|
||||
);
|
||||
|
||||
ALTER TABLE college_profiles
|
||||
ADD CONSTRAINT fk_college_profiles_user
|
||||
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT fk_college_profiles_career
|
||||
FOREIGN KEY (career_profile_id) REFERENCES career_profiles(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE college_profiles
|
||||
ADD INDEX idx_college_user (user_id),
|
||||
ADD INDEX idx_college_career (career_profile_id);
|
||||
|
||||
/*───────────────────
|
||||
STEP 3 – recreate indexes with safe prefixes (optional)
|
||||
If you *don’t* need to query by these columns any more,
|
||||
just comment‑out or delete this block.
|
||||
───────────────────*/
|
||||
CREATE INDEX idx_school ON college_profiles (selected_school(191));
|
||||
CREATE INDEX idx_program ON college_profiles (selected_program(191));
|
||||
CREATE INDEX idx_school_prog ON college_profiles (selected_school(191),
|
||||
selected_program(191));
|
||||
|
||||
/* ───────────────────────── misc small tables ──────────────────── */
|
||||
ALTER TABLE milestones
|
||||
MODIFY description MEDIUMTEXT;
|
||||
|
||||
ALTER TABLE tasks
|
||||
MODIFY description MEDIUMTEXT;
|
||||
|
||||
ALTER TABLE reminders
|
||||
MODIFY phone_e164 VARCHAR(128),
|
||||
MODIFY message_body MEDIUMTEXT;
|
||||
|
||||
ALTER TABLE milestone_impacts
|
||||
MODIFY amount VARCHAR(128),
|
||||
MODIFY impact_type VARCHAR(64);
|
||||
|
||||
ALTER TABLE ai_risk_analysis
|
||||
MODIFY reasoning MEDIUMTEXT,
|
||||
MODIFY raw_prompt MEDIUMTEXT;
|
||||
|
||||
ALTER TABLE ai_generated_ksa
|
||||
MODIFY knowledge_json MEDIUMTEXT,
|
||||
MODIFY abilities_json MEDIUMTEXT,
|
||||
MODIFY skills_json MEDIUMTEXT;
|
||||
|
||||
ALTER TABLE ai_suggested_milestones
|
||||
MODIFY suggestion_text MEDIUMTEXT;
|
||||
|
||||
ALTER TABLE context_cache
|
||||
MODIFY ctx_text MEDIUMTEXT;
|
626
package-lock.json
generated
626
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@google-cloud/kms": "^5.1.0",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
@ -2498,6 +2499,90 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@google-cloud/kms": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-5.1.0.tgz",
|
||||
"integrity": "sha512-KLPcaMDKWtGwGoetUkEdfD1x+OMndBQX1r2Q3XW5azG0DN3kmvRCM7cxX6PoYLfIFdy8k1lo98gcUYw1AEdPew==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-gax": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
@ -3044,6 +3129,16 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
@ -3264,6 +3359,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
@ -4208,6 +4367,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/caseless": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
|
||||
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
@ -4425,6 +4590,35 @@
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/request": {
|
||||
"version": "2.48.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz",
|
||||
"integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/caseless": "*",
|
||||
"@types/node": "*",
|
||||
"@types/tough-cookie": "*",
|
||||
"form-data": "^2.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/request/node_modules/form-data": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
|
||||
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@ -4491,6 +4685,12 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
@ -6026,6 +6226,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -7573,6 +7782,15 @@
|
||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
||||
@ -8130,6 +8348,32 @@
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/duplexify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.4.1",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stream-shift": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -9371,6 +9615,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -9463,6 +9713,38 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob/node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -9887,6 +10169,18 @@
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -10053,6 +10347,74 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz",
|
||||
"integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
|
||||
"integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
@ -10374,6 +10736,94 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.2.1.tgz",
|
||||
"integrity": "sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^7.0.0",
|
||||
"gcp-metadata": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"gtoken": "^8.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/google-gax": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.1.tgz",
|
||||
"integrity": "sha512-I8fTFXvIG8tYpiDxDXwCXoFsTVsvHJ2GA7DToH+eaRccU8r3nqPMFghVb2GdHSVcu4pq9ScRyB2S1BjO+vsa1Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.12.6",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"abort-controller": "^3.0.0",
|
||||
"duplexify": "^4.1.3",
|
||||
"google-auth-library": "^10.1.0",
|
||||
"google-logging-utils": "^1.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"proto3-json-serializer": "^3.0.0",
|
||||
"protobufjs": "^7.5.3",
|
||||
"retry-request": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-gax/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz",
|
||||
"integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -10398,6 +10848,40 @@
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gtoken": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
|
||||
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken/node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||
@ -12814,6 +13298,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
@ -13148,6 +13641,12 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
@ -16466,6 +16965,42 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proto3-json-serializer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.1.tgz",
|
||||
"integrity": "sha512-Rug90pDIefARAG9MgaFjd0yR/YP4bN3Fov00kckXMjTZa0x86c4WoWfCQFdSeWi9DvRXjhfLlPDIvODB5LOTfg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"protobufjs": "^7.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
|
||||
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -17423,6 +17958,20 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/retry-request": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.1.tgz",
|
||||
"integrity": "sha512-5sR3yWYODO2MTxsKjbCYFQUiXTbAe+83BV8NOB97lz6AS790OBQRUnPxT3SpxNYHUKNnYPwalk1UxuaALLJ77Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/request": "^2.48.13",
|
||||
"extend": "^3.0.2",
|
||||
"teeny-request": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@ -18586,6 +19135,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-events": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stubs": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-shift": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@ -18867,6 +19431,12 @@
|
||||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-loader": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
|
||||
@ -19359,6 +19929,62 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/teeny-request": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz",
|
||||
"integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^5.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"stream-events": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/teeny-request/node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/teeny-request/node_modules/http-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/once": "2",
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/teeny-request/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/temp-dir": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||
|
@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@google-cloud/kms": "^5.1.0",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
|
@ -154,6 +154,8 @@ const canShowRetireBot =
|
||||
console.error(err);
|
||||
// Invalid token => remove it, force sign in
|
||||
localStorage.removeItem('token');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
navigate('/signin?session=expired');
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -281,7 +281,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||
</table>
|
||||
|
||||
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
||||
{aiRisk?.riskLevel &&
|
||||
{aiRisk.riskLevel &&
|
||||
(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
||||
<p className="text-sm text-red-600 mt-2">
|
||||
Note: These 10‑year projections may change if AI‑driven tools
|
||||
|
Loading…
Reference in New Issue
Block a user