Copilot pipeline fix v5

This commit is contained in:
Josh 2025-08-09 19:08:27 +00:00
parent 2f4c8fb2cf
commit 5e277b9f94
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/nginx:$IMG_TAG
- name: staging-deploy
image: google/cloud-sdk:latest
entrypoint:
@ -38,7 +37,6 @@ steps:
mkdir -p ~/.ssh
# ── Inject known-hosts and SSH key ───────────────────────────────
gcloud secrets versions access latest \
--secret=STAGING_KNOWN_HOSTS --project=aptivaai-dev \
| base64 -d > ~/.ssh/known_hosts
@ -51,7 +49,6 @@ steps:
echo "🔑 SSH prerequisites installed"
# ── SSH into staging and deploy ──────────────────────────────────
ssh -o StrictHostKeyChecking=yes \
-i ~/.ssh/id_ed25519 \
jcoakley@10.128.0.12 \
@ -109,24 +106,24 @@ steps:
DEK_PATH=$(gcloud secrets versions access latest --secret=DEK_PATH_$ENV --project=$PROJECT); \
export DEK_PATH; \
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 \
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; \
if [ -s /tmp/dev_dek.enc ]; then \
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 \
echo \"⚠️ WRAPPED_DEK_dev returned empty; skipping copy\"; \
echo "⚠️ WRAPPED_DEK_dev returned empty; skipping copy"; \
fi; \
else \
echo \" WRAPPED_DEK_dev not found; leaving existing staging DEK alone\"; \
echo " WRAPPED_DEK_dev not found; leaving existing staging DEK alone"; \
fi; \
\
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 \
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; \
echo \"✅ Staging stack refreshed with tag \$IMG_TAG\"'

View File

@ -1,5 +1,5 @@
// ─── server3.js ────────────────────────────────────────────────────────────
import dotenv from 'dotenv';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
@ -8,14 +8,14 @@ const __dirname = path.dirname(__filename);
import express from 'express';
import helmet from 'helmet';
import fs from 'fs/promises';
import { readFile } from 'fs/promises'; // <-- add this
import multer from 'multer';
import fetch from 'node-fetch';
import mammoth from 'mammoth';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
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 Fuse from 'fuse.js';
@ -25,51 +25,53 @@ import { createReminder } from './utils/smsService.js';
import { initEncryption, verifyCanary } 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 { cacheSummary } from "./utils/ctxCache.js";
const rootPath = path.resolve(__dirname, '..'); // one level up
const env = (process.env.NODE_ENV || 'production'); // production in prod
const stage = env === 'staging' ? 'development' : env;
const envPath = path.resolve(rootPath, `.env.${env}`); // => /app/.env.production
const rootPath = path.resolve(__dirname, '..');
const env = (process.env.NODE_ENV || 'production');
const envPath = path.resolve(rootPath, `.env.${env}`);
if (!process.env.FROM_SECRETS_MANAGER) {
dotenv.config({ path: envPath });
dotenv.config({ path: envPath, override: false });
}
const PORT = process.env.SERVER3_PORT || 5002;
const API_BASE = `http://localhost:${PORT}/api`;
const API_BASE = `http://localhost:${PORT}/api`;
/* ─── helper: canonical public origin ─────────────────────────── */
const PUBLIC_BASE = (
process.env.APTIVA_AI_BASE // ← preferred
|| process.env.REACT_APP_API_URL // ← old name, tolerated
process.env.APTIVA_AI_BASE
|| process.env.REACT_APP_API_URL
|| ''
).replace(/\/+$/, ''); // strip trailing “/”
).replace(/\/+$/, '');
/* allowlist for redirects to block openredirect attacks */
const ALLOWED_REDIRECT_HOSTS = new Set([
new URL(PUBLIC_BASE || 'http://localhost').host
]);
function isSafeRedirect(url) {
try {
const u = new URL(url);
return ALLOWED_REDIRECT_HOSTS.has(u.host) && u.protocol === 'https:';
} catch { return false; }
}
try {
const u = new URL(url);
return ALLOWED_REDIRECT_HOSTS.has(u.host) && u.protocol === 'https:';
} catch { return false; }
}
const app = express();
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, {
apiVersion: '2024-04-10',
});
// ── Use raw pool for canary/db checks (avoid DAO wrapper noise) ──
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() {
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
app.get('/readyz', async (_req, res) => {
try {
await initEncryption(); // load/unlock DEK
await verifyCanary(pool); // DB + decrypt sentinel
await initEncryption();
await verifyCanary(db);
return res.type('text').send('OK');
} catch (e) {
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) => {
const out = {
service: process.env.npm_package_name || 'server',
service: process.env.npm_package_name || 'server3',
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 }
}
};
@ -112,8 +114,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;
@ -122,7 +123,7 @@ app.get('/healthz', async (_req, res) => {
// DB ping
const t0 = Date.now();
try {
await pool.query('SELECT 1');
await db.query('SELECT 1');
out.checks.db.ok = true;
out.checks.db.ping_ms = Date.now() - t0;
} catch (e) {
@ -131,7 +132,7 @@ app.get('/healthz', async (_req, res) => {
// canary
try {
await verifyCanary(pool);
await verifyCanary(db);
out.checks.canary.ok = true;
} catch (e) {
out.checks.canary.error = e.message;
@ -141,6 +142,9 @@ app.get('/healthz', async (_req, res) => {
return res.status(ready ? 200 : 503).json(out);
});
// …rest of your routes and app.listen(PORT)
function internalFetch(req, urlPath, opts = {}) {
return fetch(`${API_BASE}${urlPath}`, {
...opts,