/** * Environment-variable validation. Called at module load time by routes * that need real Civi credentials. In stub mode, no env is required. * * Centralizing here means we can: * - Fail fast at boot in production if a required var is missing * - Avoid leaking secrets to error responses (we redact in error paths) * - Reuse the "is stub mode?" check in one place */ export interface AppEnv { isStub: boolean; isProduction: boolean; civiBaseUrl?: string; civiApiKey?: string; civiSiteKey?: string; basicAuthUser?: string; basicAuthPass?: string; /** * Optional admin token to gate /api/health in production. If unset in prod, * /api/health is disabled entirely (returns 404). Set this to a long random * string and pass via `?token=<>` to the health endpoint to read diagnostics. */ healthToken?: string; /** * Same-origin host this app runs at. Used to build `frame-ancestors 'self'` * and any absolute self-links. Defaults to inferring from the request. */ publicOrigin?: string; } let cached: AppEnv | undefined; export function appEnv(): AppEnv { if (cached) return cached; const e: AppEnv = { isStub: !( process.env.CIVI_BASE_URL && process.env.CIVI_API_KEY && process.env.CIVI_SITE_KEY ), isProduction: process.env.NODE_ENV === "production", civiBaseUrl: process.env.CIVI_BASE_URL, civiApiKey: process.env.CIVI_API_KEY, civiSiteKey: process.env.CIVI_SITE_KEY, basicAuthUser: process.env.CIVI_HTTP_AUTH_USER, basicAuthPass: process.env.CIVI_HTTP_AUTH_PASS, healthToken: process.env.HEALTH_TOKEN, publicOrigin: process.env.PUBLIC_ORIGIN, }; // In production, refuse to start in stub mode — that's almost certainly a // misconfiguration. Better to crash loudly than silently serve stub data. if (e.isProduction && e.isStub) { throw new Error( "Refusing to run in production without CIVI_BASE_URL, CIVI_API_KEY, and CIVI_SITE_KEY set. " + "Either provide all three, or set NODE_ENV != 'production'.", ); } cached = e; return e; } /** Strip secrets from any string before logging or echoing in responses. */ export function redact(s: string): string { let out = s; const e = appEnv(); for (const v of [e.civiApiKey, e.civiSiteKey, e.basicAuthPass, e.healthToken]) { if (v && v.length >= 6) out = out.split(v).join("«redacted»"); } return out; }