Copilot pipeline rewrite v4
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Josh 2025-08-09 18:57:51 +00:00
parent d6e9b1f489
commit 333dbe3f02
2 changed files with 50 additions and 43 deletions

View File

@ -128,7 +128,8 @@ steps:
docker compose pull; \ 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 \ 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; \ 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: secrets:

View File

@ -5,43 +5,40 @@
import express from 'express'; import express from 'express';
import axios from 'axios'; import axios from 'axios';
import cors from 'cors'; import cors from 'cors';
import helmet from 'helmet'; // For HTTP security headers import helmet from 'helmet';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import xlsx from 'xlsx'; // Keep for CIP->SOC mapping only import xlsx from 'xlsx';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { open } from 'sqlite'; import { open } from 'sqlite';
import sqlite3 from 'sqlite3'; 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 fs from 'fs';
import { readFile } from 'fs/promises'; // <-- add this
import readline from 'readline'; import readline from 'readline';
import chatFreeEndpoint from "./utils/chatFreeEndpoint.js"; import chatFreeEndpoint from "./utils/chatFreeEndpoint.js";
import { OpenAI } from 'openai'; 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, verifyCanary } from './shared/crypto/encryption.js'; import { initEncryption, verifyCanary, SENTINEL } from './shared/crypto/encryption.js';
// --- Basic file init ---
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); 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 env = process.env.NODE_ENV?.trim() || 'development';
const envPath = path.resolve(rootPath, `.env.${env}`); 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 ROOT_DIR = path.resolve(__dirname, '..');
const PUBLIC_DIR = path.join(ROOT_DIR, 'public'); // static json files const PUBLIC_DIR = path.join(ROOT_DIR, 'public');
const CIP_TO_SOC_PATH = path.join(PUBLIC_DIR, 'CIP_to_ONET_SOC.xlsx'); 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 INSTITUTION_DATA_PATH= path.join(PUBLIC_DIR, 'Institution_data.json');
const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db'); const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db');
const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db'); const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db');
for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, SALARY_DB_PATH, USER_PROFILE_DB_PATH]) {
SALARY_DB_PATH, USER_PROFILE_DB_PATH]) {
if (!fs.existsSync(p)) { if (!fs.existsSync(p)) {
console.error(`FATAL Required data file not found → ${p}`); console.error(`FATAL Required data file not found → ${p}`);
process.exit(1); process.exit(1);
@ -56,12 +53,20 @@ const chatLimiter = rateLimit({
keyGenerator: req => req.user?.id || req.ip 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')); const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
await initEncryption(); // ── DEK + canary bootstrap (use raw pool to avoid DAO interception) ──
await pool.query('SELECT 1'); const db = pool.raw || pool;
await verifyCanary(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 // Create Express app
const app = express(); const app = express();
@ -72,14 +77,14 @@ function fprPathFromEnv() {
return p ? path.join(path.dirname(p), 'dek.fpr') : null; 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')); 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) => { app.get('/readyz', async (_req, res) => {
try { try {
await initEncryption(); // load/unlock DEK await initEncryption();
await verifyCanary(pool); // DB + decrypt sentinel await verifyCanary(db); // <-- use raw pool
return res.type('text').send('OK'); return res.type('text').send('OK');
} catch (e) { } catch (e) {
console.error('[READYZ]', e.message); 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) => { app.get('/healthz', async (_req, res) => {
const out = { const out = {
service: process.env.npm_package_name || 'server', service: process.env.npm_package_name || 'server2',
version: process.env.IMG_TAG || null, version: process.env.IMG_TAG || null,
uptime_s: Math.floor(process.uptime()), uptime_s: Math.floor(process.uptime()),
now: new Date().toISOString(), now: new Date().toISOString(),
checks: { checks: {
live: { ok: true }, // if we reached here, process is up live: { ok: true },
crypto: { ok: false, fp: null }, crypto: { ok: false, fp: null },
db: { ok: false, ping_ms: null }, db: { ok: false, ping_ms: null },
canary: { ok: false } canary: { ok: false }
} }
}; };
@ -108,8 +113,7 @@ app.get('/healthz', async (_req, res) => {
out.checks.crypto.ok = true; out.checks.crypto.ok = true;
const p = fprPathFromEnv(); const p = fprPathFromEnv();
if (p) { if (p) {
try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); } try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); } catch {}
catch { /* fp optional */ }
} }
} catch (e) { } catch (e) {
out.checks.crypto.error = e.message; out.checks.crypto.error = e.message;
@ -118,7 +122,7 @@ app.get('/healthz', async (_req, res) => {
// DB ping // DB ping
const t0 = Date.now(); const t0 = Date.now();
try { try {
await pool.query('SELECT 1'); await db.query('SELECT 1'); // <-- use raw pool
out.checks.db.ok = true; out.checks.db.ok = true;
out.checks.db.ping_ms = Date.now() - t0; out.checks.db.ping_ms = Date.now() - t0;
} catch (e) { } catch (e) {
@ -127,7 +131,7 @@ app.get('/healthz', async (_req, res) => {
// canary // canary
try { try {
await verifyCanary(pool); await verifyCanary(db); // <-- use raw pool
out.checks.canary.ok = true; out.checks.canary.ok = true;
} catch (e) { } catch (e) {
out.checks.canary.error = e.message; out.checks.canary.error = e.message;
@ -138,15 +142,14 @@ app.get('/healthz', async (_req, res) => {
}); });
/************************************************** /**************************************************
* DB connections * DB connections (SQLite)
**************************************************/ **************************************************/
let dbSqlite;
let db;
let userProfileDb; let userProfileDb;
async function initDatabases() { async function initDatabases() {
try { try {
db = await open({ dbSqlite = await open({
filename: SALARY_DB_PATH, filename: SALARY_DB_PATH,
driver : sqlite3.Database, driver : sqlite3.Database,
mode : sqlite3.OPEN_READONLY mode : sqlite3.OPEN_READONLY
@ -160,12 +163,15 @@ async function initDatabases() {
console.log('✅ Connected to user_profile.db'); console.log('✅ Connected to user_profile.db');
} catch (err) { } catch (err) {
console.error('❌ DB init failed →', 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 * SECURITY, CORS, JSON Body
* */ * */