// Environment configuration — singleton, loaded once at startup. function required(name: string): string { const v = process.env[name]; if (!v) throw new Error(`Missing required env var: ${name}`); return v; } function optional(name: string, fallback: string): string { return process.env[name] ?? fallback; } export interface Config { port: number; nodeEnv: string; postgres: { connectionString: string; }; erpnext: { url: string; apiKey: string; apiSecret: string; siteName: string; }; listmonk: { url: string; user: string; password: string; }; minio: { endpoint: string; port: number; accessKey: string; secretKey: string; }; stripe: { secretKey: string; webhookSecret: string; }; smtp: { host: string; port: number; user: string; pass: string; from: string; }; } // Env-var guard for production: refuse to boot with placeholder secrets. // Catches the common footgun of deploying with `change-this-in-production` // still in effect on ADMIN_JWT_SECRET, WEBHOOK_SECRET, etc. function refuseInsecureProduction(): void { if (process.env.NODE_ENV !== "production") return; const bad: string[] = []; const check = (name: string, sentinels: string[] = []) => { const v = process.env[name] ?? ""; if (!v) bad.push(`${name} is unset`); else if (sentinels.includes(v)) bad.push(`${name} is still set to a placeholder`); }; check("ADMIN_JWT_SECRET", ["change-this-in-production"]); check("WEBHOOK_SECRET", ["change-this-in-production"]); check("SHKEEPER_API_KEY"); check("STRIPE_WEBHOOK_SECRET"); if (bad.length) { throw new Error( `[config] Refusing to start in production with insecure settings:\n` + bad.map(b => ` - ${b}`).join("\n"), ); } } function loadConfig(): Config { refuseInsecureProduction(); return { port: parseInt(optional("PORT", "3001"), 10), nodeEnv: optional("NODE_ENV", "development"), postgres: { connectionString: required("DATABASE_URL"), }, erpnext: { url: optional("ERPNEXT_URL", "http://erpnext:8000"), apiKey: optional("ERPNEXT_API_KEY", ""), apiSecret: optional("ERPNEXT_API_SECRET", ""), siteName: optional("ERPNEXT_SITE_NAME", optional("ERPNEXT_HOST_HEADER", "performancewest.net")), }, listmonk: { url: optional("LISTMONK_URL", "http://listmonk:9000"), user: optional("LISTMONK_USER", "api"), password: optional("LISTMONK_PASSWORD", ""), }, minio: { endpoint: optional("MINIO_ENDPOINT", "localhost"), port: parseInt(optional("MINIO_PORT", "9000"), 10), accessKey: optional("MINIO_ACCESS_KEY", ""), secretKey: optional("MINIO_SECRET_KEY", ""), }, stripe: { secretKey: optional("STRIPE_SECRET_KEY", ""), webhookSecret: optional("STRIPE_WEBHOOK_SECRET", ""), }, smtp: { host: optional("SMTP_HOST", "mail.smtp2go.com"), port: parseInt(optional("SMTP_PORT", "587"), 10), user: optional("SMTP_USER", ""), pass: optional("SMTP_PASS", ""), from: optional("SMTP_FROM", "Performance West "), }, }; } export const config = loadConfig();