From a39da267294c92eb3b757ca1ae04e12ce290f9c8 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 6 Aug 2025 19:44:50 +0000 Subject: [PATCH] Field-level encryption implemented --- .env | 2 +- backend/config/mysqlPool.js | 42 +- backend/jobs/reminderCron.js | 1 + backend/server1.js | 37 +- backend/server2.js | 3 + backend/server3.js | 82 ++-- backend/shared/crypto/encryption.js | 140 +++++++ backend/shared/db/withEncryption.js | 158 +++++++ backend/utils/ctxCache.js | 1 + deploy_all.sh | 47 +-- docker-compose.yml | 16 +- migrate_encrypted_columns.sql | 136 ++++++ package-lock.json | 626 ++++++++++++++++++++++++++++ package.json | 1 + src/App.js | 2 + src/components/CareerModal.js | 2 +- 16 files changed, 1189 insertions(+), 107 deletions(-) create mode 100644 backend/shared/crypto/encryption.js create mode 100644 backend/shared/db/withEncryption.js create mode 100644 migrate_encrypted_columns.sql diff --git a/.env b/.env index 76fb34f..bc66427 100644 --- a/.env +++ b/.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 \ No newline at end of file +IMG_TAG=1e039bf-202508061933 \ No newline at end of file diff --git a/backend/config/mysqlPool.js b/backend/config/mysqlPool.js index 55ed530..d373fa5 100644 --- a/backend/config/mysqlPool.js +++ b/backend/config/mysqlPool.js @@ -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; \ No newline at end of file +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 +}; diff --git a/backend/jobs/reminderCron.js b/backend/jobs/reminderCron.js index 1516718..090f643 100644 --- a/backend/jobs/reminderCron.js +++ b/backend/jobs/reminderCron.js @@ -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 diff --git a/backend/server1.js b/backend/server1.js index cc6cebe..09ddcfd 100755 --- a/backend/server1.js +++ b/backend/server1.js @@ -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 diff --git a/backend/server2.js b/backend/server2.js index 915d63b..cfa5da2 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -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; diff --git a/backend/server3.js b/backend/server3.js index 107b5c2..91de710 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -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) => { diff --git a/backend/shared/crypto/encryption.js b/backend/shared/crypto/encryption.js new file mode 100644 index 0000000..9c1cb30 --- /dev/null +++ b/backend/shared/crypto/encryption.js @@ -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)); +} diff --git a/backend/shared/db/withEncryption.js b/backend/shared/db/withEncryption.js new file mode 100644 index 0000000..6b5e8dd --- /dev/null +++ b/backend/shared/db/withEncryption.js @@ -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 diff --git a/backend/utils/ctxCache.js b/backend/utils/ctxCache.js index 9c01b5c..ee27b25 100644 --- a/backend/utils/ctxCache.js +++ b/backend/utils/ctxCache.js @@ -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 diff --git a/deploy_all.sh b/deploy_all.sh index 36407e5..ed908bb 100755 --- a/deploy_all.sh +++ b/deploy_all.sh @@ -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" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 50a2c1f..de50fe8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/migrate_encrypted_columns.sql b/migrate_encrypted_columns.sql new file mode 100644 index 0000000..ae7a404 --- /dev/null +++ b/migrate_encrypted_columns.sql @@ -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; diff --git a/package-lock.json b/package-lock.json index d09b2b3..c8bddf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bab33ee..b885daa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.js b/src/App.js index 6f83a23..60ac63e 100644 --- a/src/App.js +++ b/src/App.js @@ -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(() => { diff --git a/src/components/CareerModal.js b/src/components/CareerModal.js index 97f3acf..ef79dc2 100644 --- a/src/components/CareerModal.js +++ b/src/components/CareerModal.js @@ -281,7 +281,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { {/* Conditional disclaimer when AI risk is Moderate or High */} - {aiRisk?.riskLevel && + {aiRisk.riskLevel && (aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (

Note: These 10‑year projections may change if AI‑driven tools