From 2f4c8fb2cf2b33a537765adcddc38bd931ec9670 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 9 Aug 2025 18:57:51 +0000 Subject: [PATCH] Copilot pipeline rewrite v4 --- .woodpecker.yml | 3 +- backend/server2.js | 90 ++++++++++++++++++++++++---------------------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 06a7d15..b926171 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -128,7 +128,8 @@ steps: docker compose pull; \ sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,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_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH \ docker compose up -d --force-recreate --remove-orphans; \ - echo \"✅ Staging stack refreshed with tag $IMG_TAG\"' + echo \"✅ Staging stack refreshed with tag \$IMG_TAG\"' + secrets: diff --git a/backend/server2.js b/backend/server2.js index 2cca847..4d3d597 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -5,43 +5,40 @@ import express from 'express'; import axios from 'axios'; import cors from 'cors'; -import helmet from 'helmet'; // For HTTP security headers +import helmet from 'helmet'; import dotenv from 'dotenv'; -import xlsx from 'xlsx'; // Keep for CIP->SOC mapping only +import xlsx from 'xlsx'; import path from 'path'; import { fileURLToPath } from 'url'; import { open } from 'sqlite'; import sqlite3 from 'sqlite3'; -import pool from './config/mysqlPool.js'; // adjust path if needed +import pool from './config/mysqlPool.js'; // exports { query, execute, raw, ... } import fs from 'fs'; +import { readFile } from 'fs/promises'; // <-- add this import readline from 'readline'; import chatFreeEndpoint from "./utils/chatFreeEndpoint.js"; -import { OpenAI } from 'openai'; -import rateLimit from 'express-rate-limit'; +import { OpenAI } from 'openai'; +import rateLimit from 'express-rate-limit'; import authenticateUser from './utils/authenticateUser.js'; -import { vectorSearch } from "./utils/vectorSearch.js"; -import { initEncryption, verifyCanary } from './shared/crypto/encryption.js'; +import { vectorSearch } from "./utils/vectorSearch.js"; +import { initEncryption, verifyCanary, SENTINEL } from './shared/crypto/encryption.js'; - - -// --- Basic file init --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const rootPath = path.resolve(__dirname, '..'); // Up one level +const rootPath = path.resolve(__dirname, '..'); const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); -dotenv.config({ path: envPath }); // Load .env +dotenv.config({ path: envPath, override: false }); // don't clobber compose-injected env -const ROOT_DIR = path.resolve(__dirname, '..'); // repo root -const PUBLIC_DIR = path.join(ROOT_DIR, 'public'); // static json files -const CIP_TO_SOC_PATH = path.join(PUBLIC_DIR, 'CIP_to_ONET_SOC.xlsx'); -const INSTITUTION_DATA_PATH = path.join(PUBLIC_DIR, 'Institution_data.json'); -const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db'); -const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db'); +const ROOT_DIR = path.resolve(__dirname, '..'); +const PUBLIC_DIR = path.join(ROOT_DIR, 'public'); +const CIP_TO_SOC_PATH = path.join(PUBLIC_DIR, 'CIP_to_ONET_SOC.xlsx'); +const INSTITUTION_DATA_PATH= path.join(PUBLIC_DIR, 'Institution_data.json'); +const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db'); +const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db'); -for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, - SALARY_DB_PATH, USER_PROFILE_DB_PATH]) { +for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, SALARY_DB_PATH, USER_PROFILE_DB_PATH]) { if (!fs.existsSync(p)) { console.error(`FATAL Required data file not found → ${p}`); process.exit(1); @@ -56,12 +53,20 @@ const chatLimiter = rateLimit({ keyGenerator: req => req.user?.id || req.ip }); -// Institution data +// Load institution data (kept for existing routes) const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8')); -await initEncryption(); -await pool.query('SELECT 1'); -await verifyCanary(pool); +// ── DEK + canary bootstrap (use raw pool to avoid DAO interception) ── +const db = pool.raw || pool; + +try { + await initEncryption(); + await db.query('SELECT 1'); + await verifyCanary(db); +} catch (e) { + console.error('FATAL during crypto/DB bootstrap:', e?.message || e); + process.exit(1); +} // Create Express app const app = express(); @@ -72,14 +77,14 @@ function fprPathFromEnv() { return p ? path.join(path.dirname(p), 'dek.fpr') : null; } -// 1) Liveness: process is up and event loop responsive +// 1) Liveness: process up app.get('/livez', (_req, res) => res.type('text').send('OK')); -// 2) Readiness: crypto + canary are good +// 2) Readiness: DEK + canary OK app.get('/readyz', async (_req, res) => { try { - await initEncryption(); // load/unlock DEK - await verifyCanary(pool); // DB + decrypt sentinel + await initEncryption(); + await verifyCanary(db); // <-- use raw pool return res.type('text').send('OK'); } catch (e) { console.error('[READYZ]', e.message); @@ -87,17 +92,17 @@ app.get('/readyz', async (_req, res) => { } }); -// 3) Health: detailed JSON (you can curl this to “see everything”) +// 3) Health: detailed JSON you can curl app.get('/healthz', async (_req, res) => { const out = { - service: process.env.npm_package_name || 'server', + service: process.env.npm_package_name || 'server2', version: process.env.IMG_TAG || null, uptime_s: Math.floor(process.uptime()), now: new Date().toISOString(), checks: { - live: { ok: true }, // if we reached here, process is up + live: { ok: true }, crypto: { ok: false, fp: null }, - db: { ok: false, ping_ms: null }, + db: { ok: false, ping_ms: null }, canary: { ok: false } } }; @@ -108,8 +113,7 @@ app.get('/healthz', async (_req, res) => { out.checks.crypto.ok = true; const p = fprPathFromEnv(); if (p) { - try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); } - catch { /* fp optional */ } + try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); } catch {} } } catch (e) { out.checks.crypto.error = e.message; @@ -118,7 +122,7 @@ app.get('/healthz', async (_req, res) => { // DB ping const t0 = Date.now(); try { - await pool.query('SELECT 1'); + await db.query('SELECT 1'); // <-- use raw pool out.checks.db.ok = true; out.checks.db.ping_ms = Date.now() - t0; } catch (e) { @@ -127,7 +131,7 @@ app.get('/healthz', async (_req, res) => { // canary try { - await verifyCanary(pool); + await verifyCanary(db); // <-- use raw pool out.checks.canary.ok = true; } catch (e) { out.checks.canary.error = e.message; @@ -138,15 +142,14 @@ app.get('/healthz', async (_req, res) => { }); /************************************************** - * DB connections + * DB connections (SQLite) **************************************************/ - -let db; +let dbSqlite; let userProfileDb; async function initDatabases() { try { - db = await open({ + dbSqlite = await open({ filename: SALARY_DB_PATH, driver : sqlite3.Database, mode : sqlite3.OPEN_READONLY @@ -160,11 +163,14 @@ async function initDatabases() { console.log('✅ Connected to user_profile.db'); } catch (err) { console.error('❌ DB init failed →', err); - process.exit(1); // let Docker restart the service + process.exit(1); } } -await initDatabases(); +await initDatabases(); + +// …rest of your routes and app.listen(PORT) + /* ────────────────────────────────────────────────────────────── * SECURITY, CORS, JSON Body