/* ──────────────────────────────────────────────────────────────── 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