Copilot pipeline fix v5
This commit is contained in:
parent
2f4c8fb2cf
commit
5e277b9f94
@ -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\"'
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// ─── server3.js ────────────────────────────────────────────────────────────
|
// ─── server3.js ────────────────────────────────────────────────────────────
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
@ -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,51 +25,53 @@ 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;
|
||||||
const API_BASE = `http://localhost:${PORT}/api`;
|
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(/\/+$/, '');
|
||||||
|
|
||||||
/* allow‑list for redirects to block open‑redirect 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,
|
||||||
|
Loading…
Reference in New Issue
Block a user