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.
This commit is contained in:
Zac Gaetano 2026-05-27 14:11:09 -04:00
parent a094df03ea
commit 88c3aa5149

View file

@ -46,7 +46,10 @@ app.use(cors({
// No Origin header (same-origin or curl) — allow. // No Origin header (same-origin or curl) — allow.
if (!origin) return cb(null, true); if (!origin) return cb(null, true);
if (allowedOrigins.length === 0 || allowedOrigins.includes(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, credentials: true,
})); }));
@ -55,9 +58,17 @@ app.use(express.json({ limit: '50mb' }));
// Trust the reverse proxy only when explicitly told to (production HTTPS). // Trust the reverse proxy only when explicitly told to (production HTTPS).
if (process.env.TRUST_PROXY === 'true') app.set('trust proxy', 1); 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. // Session — actually wired this time. See specs/2026-05-27-auth-system-design.md.
app.use(session({ 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, secret: process.env.SESSION_SECRET,
name: 'dragonflight.sid', name: 'dragonflight.sid',
cookie: { cookie: {