new-site/api/src/config.ts
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

111 lines
3.2 KiB
TypeScript

// 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 <noreply@performancewest.net>"),
},
};
}
export const config = loadConfig();