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
|
SERVER1_PORT=5000
|
||||||
SERVER2_PORT=5001
|
SERVER2_PORT=5001
|
||||||
SERVER3_PORT=5002
|
SERVER3_PORT=5002
|
||||||
IMG_TAG=6a57e00-202508051419
|
IMG_TAG=1e039bf-202508061933
|
@ -1,28 +1,16 @@
|
|||||||
// backend/config/mysqlPool.js
|
// backend/config/mysqlPool.js
|
||||||
import './env.js';
|
import {
|
||||||
import mysql from 'mysql2/promise';
|
query as queryWrapped,
|
||||||
|
exec as executeWrapped,
|
||||||
|
pool as rawPool,
|
||||||
const pool = mysql.createPool({
|
getConnection,
|
||||||
host : process.env.DB_HOST || '127.0.0.1',
|
end
|
||||||
port : process.env.DB_PORT || 3306,
|
} from '../shared/db/withEncryption.js'; // ../ (up one)
|
||||||
user : process.env.DB_USER || 'root',
|
// then shared/…
|
||||||
password : process.env.DB_PASSWORD || '',
|
export default {
|
||||||
database : process.env.DB_NAME || 'user_profile_db',
|
query : queryWrapped,
|
||||||
waitForConnections : true,
|
execute : executeWrapped,
|
||||||
connectionLimit : 10,
|
getConnection,
|
||||||
...(process.env.DB_SOCKET ? { socketPath: process.env.DB_SOCKET } : {}),
|
end,
|
||||||
ssl: {
|
raw: rawPool
|
||||||
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;
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import pool from '../config/mysqlPool.js';
|
import pool from '../config/mysqlPool.js';
|
||||||
import { sendSMS } from '../utils/smsService.js';
|
import { sendSMS } from '../utils/smsService.js';
|
||||||
|
import { query } from '../shared/db/withEncryption.js';
|
||||||
|
|
||||||
const BATCH_SIZE = 25; // tune as you like
|
const BATCH_SIZE = 25; // tune as you like
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import path from 'path';
|
|||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import jwt from 'jsonwebtoken'; // For token-based authentication
|
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
|
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
|
process.exit(1); // container exits, Docker marks it unhealthy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await initEncryption();
|
||||||
// Test a quick query (optional)
|
// Test a quick query (optional)
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT 1');
|
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' });
|
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
|
// Return user info + token
|
||||||
// 'authId' is user_auth's PK, but typically you won't need it on the client
|
// 'authId' is user_auth's PK, but typically you won't need it on the client
|
||||||
// 'row.userProfileId' is the actual user_profile.id
|
// 'row.userProfileId' is the actual user_profile.id
|
||||||
res.status(200).json({
|
const [profileRows] = await pool.query(
|
||||||
message: 'Login successful',
|
'SELECT firstname, lastname, email, zipcode, state, area, career_situation \
|
||||||
token,
|
FROM user_profile WHERE id = ?',
|
||||||
id: row.userProfileId, // This is user_profile.id (important if your frontend needs it)
|
[row.userProfileId]
|
||||||
user: {
|
);
|
||||||
firstname: row.firstname,
|
const profile = profileRows[0]; // ← already decrypted
|
||||||
lastname: row.lastname,
|
|
||||||
email: row.email,
|
const token = jwt.sign({ id: row.userProfileId }, JWT_SECRET, { expiresIn: '2h' });
|
||||||
zipcode: row.zipcode,
|
|
||||||
state: row.state,
|
res.status(200).json({
|
||||||
area: row.area,
|
message: 'Login successful',
|
||||||
career_situation: row.career_situation,
|
token,
|
||||||
},
|
id: row.userProfileId,
|
||||||
});
|
user: profile
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error querying user_auth:', err.message);
|
console.error('Error querying user_auth:', err.message);
|
||||||
return res
|
return res
|
||||||
|
@ -19,6 +19,7 @@ import { OpenAI } from 'openai';
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import authenticateUser from './utils/authenticateUser.js';
|
import authenticateUser from './utils/authenticateUser.js';
|
||||||
import { vectorSearch } from "./utils/vectorSearch.js";
|
import { vectorSearch } from "./utils/vectorSearch.js";
|
||||||
|
import { initEncryption } from './shared/crypto/encryption.js';
|
||||||
|
|
||||||
|
|
||||||
// --- Basic file init ---
|
// --- Basic file init ---
|
||||||
@ -56,6 +57,8 @@ const chatLimiter = rateLimit({
|
|||||||
// Institution data
|
// Institution data
|
||||||
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
||||||
|
|
||||||
|
await initEncryption();
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.SERVER2_PORT || 5001;
|
const PORT = process.env.SERVER2_PORT || 5001;
|
||||||
|
@ -16,11 +16,17 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import pkg from 'pdfjs-dist';
|
import pkg from 'pdfjs-dist';
|
||||||
import db from './config/mysqlPool.js';
|
import db from './config/mysqlPool.js';
|
||||||
import './jobs/reminderCron.js';
|
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { createReminder } from './utils/smsService.js';
|
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";
|
import { cacheSummary } from "./utils/ctxCache.js";
|
||||||
|
|
||||||
const rootPath = path.resolve(__dirname, '..'); // one level up
|
const rootPath = path.resolve(__dirname, '..'); // one level up
|
||||||
@ -53,6 +59,7 @@ function isSafeRedirect(url) {
|
|||||||
} catch { return false; }
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const { getDocument } = pkg;
|
const { getDocument } = pkg;
|
||||||
const bt = "`".repeat(3);
|
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' }),
|
express.raw({ type: 'application/json' }),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
|
|
||||||
let event;
|
let event;
|
||||||
try {
|
try {
|
||||||
event = stripe.webhooks.constructEvent(
|
event = stripe.webhooks.constructEvent(
|
||||||
@ -152,12 +157,13 @@ app.post('/api/premium/stripe/webhook',
|
|||||||
}
|
}
|
||||||
|
|
||||||
const upFlags = async (customerId, premium, pro) => {
|
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(
|
await pool.query(
|
||||||
`UPDATE user_profile
|
`UPDATE user_profile
|
||||||
SET is_premium = ?, is_pro_premium = ?
|
SET is_premium = ?, is_pro_premium = ?
|
||||||
WHERE stripe_customer_id = ?`,
|
WHERE stripe_customer_id_hash = ?`,
|
||||||
[premium, pro, customerId]
|
[premium, pro, h]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,11 +172,11 @@ app.post('/api/premium/stripe/webhook',
|
|||||||
case 'customer.subscription.updated': {
|
case 'customer.subscription.updated': {
|
||||||
const sub = event.data.object;
|
const sub = event.data.object;
|
||||||
const pid = sub.items.data[0].price.id;
|
const pid = sub.items.data[0].price.id;
|
||||||
const tier = [process.env.STRIPE_PRICE_PRO_MONTH, process.env.STRIPE_PRICE_PRO_YEAR]
|
const tier = [process.env.STRIPE_PRICE_PRO_MONTH,
|
||||||
.includes(pid) ? 'pro' : 'premium';
|
process.env.STRIPE_PRICE_PRO_YEAR].includes(pid)
|
||||||
|
? 'pro' : 'premium';
|
||||||
await upFlags(sub.customer, tier === 'premium', tier === 'pro');
|
await upFlags(sub.customer, tier === 'premium', tier === 'pro');
|
||||||
break;
|
break;
|
||||||
console.log('[Stripe] flags updated', { id: sub.customer, tier });
|
|
||||||
}
|
}
|
||||||
case 'customer.subscription.deleted': {
|
case 'customer.subscription.deleted': {
|
||||||
const sub = event.data.object;
|
const sub = event.data.object;
|
||||||
@ -178,10 +184,9 @@ app.post('/api/premium/stripe/webhook',
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// ignore everything else
|
// Ignore everything else
|
||||||
}
|
}
|
||||||
|
res.sendStatus(200);
|
||||||
res.status(200).end();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -258,28 +263,55 @@ const pool = db;
|
|||||||
* Returns the user’s stripe_customer_id (or null) given req.id.
|
* Returns the user’s stripe_customer_id (or null) given req.id.
|
||||||
* Creates a new Stripe Customer & saves it if missing.
|
* 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) {
|
async function getOrCreateStripeCustomerId(req) {
|
||||||
// 1) look up current row
|
/* 1 ── look up existing row (wrapped pool auto‑decrypts) */
|
||||||
const [[row]] = await pool.query(
|
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]
|
[req.id]
|
||||||
);
|
);
|
||||||
if (row?.stripe_customer_id) return row.stripe_customer_id;
|
|
||||||
|
|
||||||
// 2) create → cache → return
|
if (row?.stripe_customer_id) {
|
||||||
const customer = await stripe.customers.create({ metadata: { userId: req.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(
|
await pool.query(
|
||||||
'UPDATE user_profile SET stripe_customer_id = ? WHERE id = ?',
|
`UPDATE user_profile
|
||||||
[customer.id, req.id]
|
SET stripe_customer_id = ?,
|
||||||
|
stripe_customer_id_hash = ?
|
||||||
|
WHERE id = ?`,
|
||||||
|
[customer.id, h, req.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return customer.id;
|
return customer.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
const priceMap = {
|
const priceMap = {
|
||||||
premium: { monthly: process.env.STRIPE_PRICE_PREMIUM_MONTH,
|
premium: {
|
||||||
annual : process.env.STRIPE_PRICE_PREMIUM_YEAR },
|
monthly: process.env.STRIPE_PRICE_PREMIUM_MONTH,
|
||||||
pro : { monthly: process.env.STRIPE_PRICE_PRO_MONTH,
|
annual : process.env.STRIPE_PRICE_PREMIUM_YEAR
|
||||||
annual : process.env.STRIPE_PRICE_PRO_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) => {
|
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
|
// utils/ctxCache.js
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import pool from "../config/mysqlPool.js";
|
import pool from "../config/mysqlPool.js";
|
||||||
|
import { query } from '../shared/db/withEncryption.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
# ───────────────────────── config ─────────────────────────
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# CONFIG – adjust only these 4 if needed
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
ENV=dev
|
ENV=dev
|
||||||
PROJECT=aptivaai-dev
|
PROJECT=aptivaai-dev
|
||||||
ROOT=/home/jcoakley/aptiva-dev1-app
|
ROOT=/home/jcoakley/aptiva-dev1-app
|
||||||
@ -12,10 +7,13 @@ REG=us-central1-docker.pkg.dev/${PROJECT}/aptiva-repo
|
|||||||
ENV_FILE="${ROOT}/.env"
|
ENV_FILE="${ROOT}/.env"
|
||||||
SECRETS=(
|
SECRETS=(
|
||||||
JWT_SECRET OPENAI_API_KEY ONET_USERNAME ONET_PASSWORD
|
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
|
STRIPE_SECRET_KEY STRIPE_PUBLISHABLE_KEY STRIPE_WH_SECRET \
|
||||||
DB_HOST DB_PORT DB_USER DB_PASSWORD
|
STRIPE_PRICE_PREMIUM_MONTH STRIPE_PRICE_PREMIUM_YEAR \
|
||||||
DB_SSL_CERT DB_SSL_KEY DB_SSL_CA
|
STRIPE_PRICE_PRO_MONTH STRIPE_PRICE_PRO_YEAR \
|
||||||
TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_MESSAGING_SERVICE_SID
|
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"
|
cd "$ROOT"
|
||||||
@ -23,14 +21,11 @@ echo "🛠 Building front‑end bundle"
|
|||||||
npm ci --silent
|
npm ci --silent
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ───────────────────── build & push images ─────────────────────
|
||||||
# 1. Build → Push → Stamp .env
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
TAG="$(git rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M)"
|
TAG="$(git rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M)"
|
||||||
echo "🔨 Building & pushing containers (tag = ${TAG})"
|
echo "🔨 Building & pushing containers (tag = ${TAG})"
|
||||||
|
|
||||||
for svc in server1 server2 server3 nginx; do
|
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}"
|
docker push "${REG}/${svc}:${TAG}"
|
||||||
done
|
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)"
|
echo "📦 IMG_TAG pushed to Secret Manager (no suffix)"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ───────────────────── pull secrets (incl. KMS key path) ───────
|
||||||
# 2. Pull secrets into runtime (never written to disk)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
echo "🔐 Pulling secrets from Secret Manager"
|
echo "🔐 Pulling secrets from Secret Manager"
|
||||||
for S in "${SECRETS[@]}"; do
|
for S in "${SECRETS[@]}"; do
|
||||||
export "$S"="$(gcloud secrets versions access latest \
|
export "$S"="$(gcloud secrets versions access latest \
|
||||||
--secret="${S}_${ENV}" \
|
--secret="${S}_${ENV}" --project="$PROJECT")"
|
||||||
--project="$PROJECT")"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
export FROM_SECRETS_MANAGER=true
|
export FROM_SECRETS_MANAGER=true
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ───────────────────── compose up ───────────────────────────────
|
||||||
# 3. Re-create the container stack
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
preserve=IMG_TAG,FROM_SECRETS_MANAGER,REACT_APP_API_URL,$(IFS=,; echo "${SECRETS[*]}")
|
preserve=IMG_TAG,FROM_SECRETS_MANAGER,REACT_APP_API_URL,$(IFS=,; echo "${SECRETS[*]}")
|
||||||
|
|
||||||
echo "🚀 docker compose up -d (env: $preserve)"
|
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_file:
|
||||||
- .env # committed, non‑secret
|
- .env # committed, non‑secret
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ───────────────────────────── server1 ─────────────────────────────
|
# ───────────────────────────── server1 ─────────────────────────────
|
||||||
server1:
|
server1:
|
||||||
@ -14,6 +14,8 @@ services:
|
|||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
||||||
expose: ["${SERVER1_PORT}"]
|
expose: ["${SERVER1_PORT}"]
|
||||||
environment:
|
environment:
|
||||||
|
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||||
|
DEK_PATH: ${DEK_PATH}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
DB_HOST: ${DB_HOST}
|
DB_HOST: ${DB_HOST}
|
||||||
DB_PORT: ${DB_PORT}
|
DB_PORT: ${DB_PORT}
|
||||||
@ -29,6 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./salary_info.db:/app/salary_info.db:ro
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
- ./user_profile.db:/app/user_profile.db
|
- ./user_profile.db:/app/user_profile.db
|
||||||
|
- dek-vol:/run/secrets/dev
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -41,6 +44,8 @@ services:
|
|||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG}
|
||||||
expose: ["${SERVER2_PORT}"]
|
expose: ["${SERVER2_PORT}"]
|
||||||
environment:
|
environment:
|
||||||
|
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||||
|
DEK_PATH: ${DEK_PATH}
|
||||||
ONET_USERNAME: ${ONET_USERNAME}
|
ONET_USERNAME: ${ONET_USERNAME}
|
||||||
ONET_PASSWORD: ${ONET_PASSWORD}
|
ONET_PASSWORD: ${ONET_PASSWORD}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
@ -57,7 +62,7 @@ services:
|
|||||||
- ./public:/app/public:ro
|
- ./public:/app/public:ro
|
||||||
- ./salary_info.db:/app/salary_info.db:ro
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
- ./user_profile.db:/app/user_profile.db
|
- ./user_profile.db:/app/user_profile.db
|
||||||
|
- dek-vol:/run/secrets/dev
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -70,6 +75,8 @@ services:
|
|||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG}
|
||||||
expose: ["${SERVER3_PORT}"]
|
expose: ["${SERVER3_PORT}"]
|
||||||
environment:
|
environment:
|
||||||
|
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||||
|
DEK_PATH: ${DEK_PATH}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
||||||
@ -96,6 +103,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./salary_info.db:/app/salary_info.db:ro
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
- ./user_profile.db:/app/user_profile.db
|
- ./user_profile.db:/app/user_profile.db
|
||||||
|
- dek-vol:/run/secrets/dev
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -121,4 +129,8 @@ networks:
|
|||||||
name: aptiva_default
|
name: aptiva_default
|
||||||
aptiva-shared:
|
aptiva-shared:
|
||||||
external: true
|
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",
|
"version": "0.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/kms": "^5.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
@ -2498,6 +2499,90 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@ -3044,6 +3129,16 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@kurkle/color": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
"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": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
@ -4208,6 +4367,12 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
@ -4425,6 +4590,35 @@
|
|||||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||||
@ -4491,6 +4685,12 @@
|
|||||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@ -6026,6 +6226,15 @@
|
|||||||
"node": "*"
|
"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": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@ -7573,6 +7782,15 @@
|
|||||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||||
"license": "BSD-2-Clause"
|
"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": {
|
"node_modules/data-urls": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
||||||
@ -8130,6 +8348,32 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@ -9371,6 +9615,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@ -9463,6 +9713,38 @@
|
|||||||
"bser": "2.1.1"
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@ -9887,6 +10169,18 @@
|
|||||||
"node": ">= 12.20"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -10053,6 +10347,74 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/generate-function": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||||
@ -10374,6 +10736,94 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@ -10398,6 +10848,40 @@
|
|||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/gzip-size": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||||
@ -12814,6 +13298,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
@ -13148,6 +13641,12 @@
|
|||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash.castarray": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||||
@ -16466,6 +16965,42 @@
|
|||||||
"react-is": "^16.13.1"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@ -17423,6 +17958,20 @@
|
|||||||
"node": ">= 4"
|
"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": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
@ -18586,6 +19135,21 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/streamsearch": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
@ -18867,6 +19431,12 @@
|
|||||||
"node": ">=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": {
|
"node_modules/style-loader": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
|
||||||
@ -19359,6 +19929,62 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/kms": "^5.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
|
@ -154,6 +154,8 @@ const canShowRetireBot =
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
// Invalid token => remove it, force sign in
|
// Invalid token => remove it, force sign in
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUser(null);
|
||||||
navigate('/signin?session=expired');
|
navigate('/signin?session=expired');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -281,7 +281,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
{/* Conditional disclaimer when AI risk is Moderate or High */}
|
||||||
{aiRisk?.riskLevel &&
|
{aiRisk.riskLevel &&
|
||||||
(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
|
||||||
<p className="text-sm text-red-600 mt-2">
|
<p className="text-sm text-red-600 mt-2">
|
||||||
Note: These 10‑year projections may change if AI‑driven tools
|
Note: These 10‑year projections may change if AI‑driven tools
|
||||||
|
Loading…
Reference in New Issue
Block a user