Copilot pipeline fix v5
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Josh 2025-08-09 19:08:27 +00:00
parent 333dbe3f02
commit 9365ce40b8
2 changed files with 47 additions and 46 deletions

View File

@ -27,7 +27,6 @@ steps:
trivy image --scanners vuln --ignore-unfixed --ignorefile .trivyignore --exit-code 1 --severity CRITICAL $REG/server3:$IMG_TAG trivy image --scanners vuln --ignore-unfixed --ignorefile .trivyignore --exit-code 1 --severity CRITICAL $REG/server3:$IMG_TAG
trivy image --scanners vuln --ignore-unfixed --ignorefile .trivyignore --exit-code 1 --severity CRITICAL $REG/nginx:$IMG_TAG trivy image --scanners vuln --ignore-unfixed --ignorefile .trivyignore --exit-code 1 --severity CRITICAL $REG/nginx:$IMG_TAG
- name: staging-deploy - name: staging-deploy
image: google/cloud-sdk:latest image: google/cloud-sdk:latest
entrypoint: entrypoint:
@ -38,7 +37,6 @@ steps:
mkdir -p ~/.ssh mkdir -p ~/.ssh
# ── Inject known-hosts and SSH key ───────────────────────────────
gcloud secrets versions access latest \ gcloud secrets versions access latest \
--secret=STAGING_KNOWN_HOSTS --project=aptivaai-dev \ --secret=STAGING_KNOWN_HOSTS --project=aptivaai-dev \
| base64 -d > ~/.ssh/known_hosts | base64 -d > ~/.ssh/known_hosts
@ -51,7 +49,6 @@ steps:
echo "🔑 SSH prerequisites installed" echo "🔑 SSH prerequisites installed"
# ── SSH into staging and deploy ──────────────────────────────────
ssh -o StrictHostKeyChecking=yes \ ssh -o StrictHostKeyChecking=yes \
-i ~/.ssh/id_ed25519 \ -i ~/.ssh/id_ed25519 \
jcoakley@10.128.0.12 \ jcoakley@10.128.0.12 \
@ -109,24 +106,24 @@ steps:
DEK_PATH=$(gcloud secrets versions access latest --secret=DEK_PATH_$ENV --project=$PROJECT); \ DEK_PATH=$(gcloud secrets versions access latest --secret=DEK_PATH_$ENV --project=$PROJECT); \
export DEK_PATH; \ export DEK_PATH; \
export FROM_SECRETS_MANAGER=true; \ export FROM_SECRETS_MANAGER=true; \
# ── NEW: sync dev DEK into staging volume (uses Secret Manager) ── \ \
if gcloud secrets describe WRAPPED_DEK_dev --project=$PROJECT >/dev/null 2>&1; then \ if gcloud secrets describe WRAPPED_DEK_dev --project=$PROJECT >/dev/null 2>&1; then \
echo \"🔁 Syncing dev DEK into staging volume\"; \ echo "🔁 Syncing dev DEK into staging volume"; \
gcloud secrets versions access latest --secret=WRAPPED_DEK_dev --project=$PROJECT > /tmp/dev_dek.enc; \ gcloud secrets versions access latest --secret=WRAPPED_DEK_dev --project=$PROJECT > /tmp/dev_dek.enc; \
if [ -s /tmp/dev_dek.enc ]; then \ if [ -s /tmp/dev_dek.enc ]; then \
docker volume ls -q | grep -qx aptiva_dek_staging || docker volume create aptiva_dek_staging >/dev/null; \ docker volume ls -q | grep -qx aptiva_dek_staging || docker volume create aptiva_dek_staging >/dev/null; \
sudo docker run --rm -v aptiva_dek_staging:/v -v /tmp:/host busybox sh -c 'set -e; mkdir -p /v/staging; cp -f /host/dev_dek.enc /v/staging/dek.enc; chown 1000:1000 /v/staging/dek.enc; chmod 400 /v/staging/dek.enc; rm -f /v/staging/dek.fpr; echo -n \"staging dek.enc bytes: \"; wc -c </v/staging/dek.enc; ls -l /v/staging' sudo docker run --rm -v aptiva_dek_staging:/v -v /tmp:/host busybox sh -c "set -e; mkdir -p /v/staging; cp -f /host/dev_dek.enc /v/staging/dek.enc; chown 1000:1000 /v/staging/dek.enc; chmod 400 /v/staging/dek.enc; rm -f /v/staging/dek.fpr; echo -n 'staging dek.enc bytes: '; wc -c </v/staging/dek.enc; ls -l /v/staging"; \
else \ else \
echo \"⚠️ WRAPPED_DEK_dev returned empty; skipping copy\"; \ echo "⚠️ WRAPPED_DEK_dev returned empty; skipping copy"; \
fi; \ fi; \
else \ else \
echo \" WRAPPED_DEK_dev not found; leaving existing staging DEK alone\"; \ echo " WRAPPED_DEK_dev not found; leaving existing staging DEK alone"; \
fi; \ fi; \
\ \
cd /home/jcoakley/aptiva-staging-app; \ cd /home/jcoakley/aptiva-staging-app; \
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 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
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\"'

View File

@ -8,14 +8,14 @@ const __dirname = path.dirname(__filename);
import express from 'express'; import express from 'express';
import helmet from 'helmet'; import helmet from 'helmet';
import fs from 'fs/promises'; import { readFile } from 'fs/promises'; // <-- add this
import multer from 'multer'; import multer from 'multer';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import mammoth from 'mammoth'; import mammoth from 'mammoth';
import jwt from 'jsonwebtoken'; 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 pool from './config/mysqlPool.js'; // adjust path if needed import pool from './config/mysqlPool.js';
import OpenAI from 'openai'; import OpenAI from 'openai';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@ -25,19 +25,14 @@ import { createReminder } from './utils/smsService.js';
import { initEncryption, verifyCanary } from './shared/crypto/encryption.js'; import { initEncryption, verifyCanary } from './shared/crypto/encryption.js';
import { hashForLookup } from './shared/crypto/encryption.js'; import { hashForLookup } from './shared/crypto/encryption.js';
await initEncryption();
await pool.query('SELECT 1');
await verifyCanary(pool);
import './jobs/reminderCron.js'; 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, '..');
const env = (process.env.NODE_ENV || 'production'); // production in prod const env = (process.env.NODE_ENV || 'production');
const stage = env === 'staging' ? 'development' : env; const envPath = path.resolve(rootPath, `.env.${env}`);
const envPath = path.resolve(rootPath, `.env.${env}`); // => /app/.env.production
if (!process.env.FROM_SECRETS_MANAGER) { if (!process.env.FROM_SECRETS_MANAGER) {
dotenv.config({ path: envPath }); dotenv.config({ path: envPath, override: false });
} }
const PORT = process.env.SERVER3_PORT || 5002; const PORT = process.env.SERVER3_PORT || 5002;
@ -45,31 +40,38 @@ const API_BASE = `http://localhost:${PORT}/api`;
/* ─── helper: canonical public origin ─────────────────────────── */ /* ─── helper: canonical public origin ─────────────────────────── */
const PUBLIC_BASE = ( const PUBLIC_BASE = (
process.env.APTIVA_AI_BASE // ← preferred process.env.APTIVA_AI_BASE
|| process.env.REACT_APP_API_URL // ← old name, tolerated || process.env.REACT_APP_API_URL
|| '' || ''
).replace(/\/+$/, ''); // strip trailing “/” ).replace(/\/+$/, '');
/* allowlist for redirects to block openredirect attacks */
const ALLOWED_REDIRECT_HOSTS = new Set([ const ALLOWED_REDIRECT_HOSTS = new Set([
new URL(PUBLIC_BASE || 'http://localhost').host new URL(PUBLIC_BASE || 'http://localhost').host
]); ]);
function isSafeRedirect(url) { function isSafeRedirect(url) {
try { try {
const u = new URL(url); const u = new URL(url);
return ALLOWED_REDIRECT_HOSTS.has(u.host) && u.protocol === 'https:'; return ALLOWED_REDIRECT_HOSTS.has(u.host) && u.protocol === 'https:';
} catch { return false; } } catch { return false; }
} }
const app = express(); const app = express();
const { getDocument } = pkg; const { getDocument } = pkg;
const bt = "`".repeat(3); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2024-04-10' });
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { // ── Use raw pool for canary/db checks (avoid DAO wrapper noise) ──
apiVersion: '2024-04-10', const db = pool.raw || pool;
});
// Bootstrap: unwrap DEK, check DB, verify canary
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);
}
function fprPathFromEnv() { function fprPathFromEnv() {
const p = (process.env.DEK_PATH || '').trim(); const p = (process.env.DEK_PATH || '').trim();
@ -82,8 +84,8 @@ app.get('/livez', (_req, res) => res.type('text').send('OK'));
// 2) Readiness: crypto + canary are good // 2) Readiness: crypto + canary are good
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);
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);
@ -91,17 +93,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 || 'server3',
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 }
} }
}; };
@ -112,8 +114,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;
@ -122,7 +123,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');
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) {
@ -131,7 +132,7 @@ app.get('/healthz', async (_req, res) => {
// canary // canary
try { try {
await verifyCanary(pool); await verifyCanary(db);
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;
@ -141,6 +142,9 @@ app.get('/healthz', async (_req, res) => {
return res.status(ready ? 200 : 503).json(out); return res.status(ready ? 200 : 503).json(out);
}); });
// …rest of your routes and app.listen(PORT)
function internalFetch(req, urlPath, opts = {}) { function internalFetch(req, urlPath, opts = {}) {
return fetch(`${API_BASE}${urlPath}`, { return fetch(`${API_BASE}${urlPath}`, {
...opts, ...opts,