feat(config): boot-time env validation — fail-fast + prod key warning (Pillar D)
utils/env.cts `validateEnv()` runs first in main(): exits 1 on clearly invalid config (non-integer PORT / SESSION_CLEANUP_INTERVAL_MS, too-short TOKEN_ENCRYPTION_KEY), and warns loudly in production when TOKEN_ENCRYPTION_KEY is unset (secrets fall back to a DB-stored key). Resolves the standing prod-hardening TODO. Verified: bad PORT → exit 1; prod without key → warns but boots (health 200). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7390c458ed
commit
26d976e272
|
|
@ -364,6 +364,9 @@ function gracefulShutdown(signal, server) {
|
||||||
|
|
||||||
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||||
async function main() {
|
async function main() {
|
||||||
|
// Fail fast on invalid configuration; warn on security-relevant gaps.
|
||||||
|
require('./utils/env.cts').validateEnv();
|
||||||
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
|
|
||||||
// Migrate DB-key-encrypted secrets to env key when TOKEN_ENCRYPTION_KEY is set
|
// Migrate DB-key-encrypted secrets to env key when TOKEN_ENCRYPTION_KEY is set
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Boot-time environment validation. Fails fast on clearly-invalid values so a
|
||||||
|
// misconfigured deployment stops loudly instead of misbehaving later, and warns
|
||||||
|
// on security-relevant gaps in production. Keep this the single place that
|
||||||
|
// asserts config invariants at startup.
|
||||||
|
|
||||||
|
// Leading import so Node's type-stripping activates for `node --check`.
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
|
const isProd = () => process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
function isIntString(v: string | undefined): boolean {
|
||||||
|
return v === undefined || /^\d+$/.test(v.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate environment configuration. Throws (exit 1 in the caller) on fatal
|
||||||
|
* misconfiguration; logs warnings for security-relevant gaps.
|
||||||
|
*/
|
||||||
|
function validateEnv(): void {
|
||||||
|
const fatal: string[] = [];
|
||||||
|
|
||||||
|
if (!isIntString(process.env.PORT)) {
|
||||||
|
fatal.push(`PORT must be an integer (got "${process.env.PORT}")`);
|
||||||
|
}
|
||||||
|
if (!isIntString(process.env.SESSION_CLEANUP_INTERVAL_MS)) {
|
||||||
|
fatal.push(
|
||||||
|
`SESSION_CLEANUP_INTERVAL_MS must be an integer (got "${process.env.SESSION_CLEANUP_INTERVAL_MS}")`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = process.env.TOKEN_ENCRYPTION_KEY?.trim();
|
||||||
|
if (key && key.length < 16) {
|
||||||
|
fatal.push(
|
||||||
|
'TOKEN_ENCRYPTION_KEY is set but too short (use a long random string, e.g. 48 random bytes hex)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fatal.length) {
|
||||||
|
console.error('[config] FATAL — invalid configuration:');
|
||||||
|
for (const f of fatal) console.error(` ✗ ${f}`);
|
||||||
|
throw new Error('Invalid environment configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security-relevant warnings (do NOT fail — existing deployments intentionally
|
||||||
|
// run on the DB-stored auto-key).
|
||||||
|
if (isProd() && !key) {
|
||||||
|
console.warn(
|
||||||
|
'[config] WARNING: TOKEN_ENCRYPTION_KEY is not set in production. Encrypted secrets ' +
|
||||||
|
'(SimpleFIN token, SMTP/OIDC secrets) fall back to a key stored inside the database — ' +
|
||||||
|
'anyone with DB/backup read access can decrypt them. Set TOKEN_ENCRYPTION_KEY (outside ' +
|
||||||
|
'the DB) to a long random string.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateEnv };
|
||||||
Loading…
Reference in New Issue