From 88c3aa514928056033288c8c58871e06a34bcf14 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Wed, 27 May 2026 14:11:09 -0400 Subject: [PATCH] fix(mam-api): SESSION_SECRET boot guard + cleaner CORS rejection Code-review feedback: - Hard-fail boot when AUTH_ENABLED=true and SESSION_SECRET is unset, so express-session can't silently use an in-memory random secret that invalidates sessions on restart and breaks multi-node clusters. - CORS rejection now returns cb(null, false) instead of cb(new Error) so misconfigured origins surface as clean CORS errors in the browser instead of HTTP 500s. Log a warn line for operator visibility. - pruneSessionInterval units comment. --- services/mam-api/src/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/services/mam-api/src/index.js b/services/mam-api/src/index.js index edec2cc..2adf7b8 100644 --- a/services/mam-api/src/index.js +++ b/services/mam-api/src/index.js @@ -46,7 +46,10 @@ app.use(cors({ // No Origin header (same-origin or curl) — allow. if (!origin) return cb(null, true); if (allowedOrigins.length === 0 || allowedOrigins.includes(origin)) return cb(null, true); - return cb(new Error('CORS: origin not allowed: ' + origin)); + // Reject cleanly: omit the Allow-Origin header so the browser surfaces + // a real CORS error instead of a 500 from a thrown Error in the callback. + console.warn('[cors] rejected origin:', origin); + return cb(null, false); }, credentials: true, })); @@ -55,9 +58,17 @@ app.use(express.json({ limit: '50mb' })); // Trust the reverse proxy only when explicitly told to (production HTTPS). if (process.env.TRUST_PROXY === 'true') app.set('trust proxy', 1); +// Hard-fail when production-mode auth has no stable session secret. Without +// this, express-session falls back to an in-memory random secret which +// invalidates every session on restart and breaks multi-node deployments. +if (process.env.AUTH_ENABLED === 'true' && !process.env.SESSION_SECRET) { + console.error('[fatal] SESSION_SECRET is required when AUTH_ENABLED=true'); + process.exit(1); +} + // Session — actually wired this time. See specs/2026-05-27-auth-system-design.md. app.use(session({ - store: new PgStore({ pool, tableName: 'sessions', pruneSessionInterval: 60 * 15 }), + store: new PgStore({ pool, tableName: 'sessions', pruneSessionInterval: 60 * 15 /* seconds = 15 min */ }), secret: process.env.SESSION_SECRET, name: 'dragonflight.sid', cookie: {