export interface RuntimeUrls { appBaseUrl: URL; adminBaseUrl: URL; nanoBananaApiBaseUrl: URL; s3Endpoint: URL; } export interface DatabaseConfig { url: string; } export interface AuthConfig { sessionSecret: string; passwordPepper: string; } export interface ProviderConfig { nanoBananaDefaultModel: string; } export interface PaymentConfig { provider: string; apiKey: string; webhookSecret: string; } export interface StorageConfig { region: string; bucket: string; accessKey: string; secretKey: string; forcePathStyle: boolean; } export interface TelegramConfig { botToken: string; botMode: "polling"; } export interface EmailConfig { provider: string; from: string; apiKey: string; } export interface KeyPoolConfig { cooldownMinutes: number; failuresBeforeManualReview: number; balancePollSeconds: number; } export interface AppRuntimeConfig { nodeEnv: string; urls: RuntimeUrls; database: DatabaseConfig; auth: AuthConfig; provider: ProviderConfig; payment: PaymentConfig; storage: StorageConfig; telegram: TelegramConfig; email: EmailConfig; keyPool: KeyPoolConfig; } export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppRuntimeConfig { return { nodeEnv: readString(env, "NODE_ENV"), urls: { appBaseUrl: readUrl(env, "APP_BASE_URL"), adminBaseUrl: readUrl(env, "ADMIN_BASE_URL"), nanoBananaApiBaseUrl: readUrl(env, "NANO_BANANA_API_BASE_URL"), s3Endpoint: readUrl(env, "S3_ENDPOINT"), }, database: { url: readString(env, "DATABASE_URL"), }, auth: { sessionSecret: readString(env, "SESSION_SECRET"), passwordPepper: readString(env, "PASSWORD_PEPPER"), }, provider: { nanoBananaDefaultModel: readString(env, "NANO_BANANA_DEFAULT_MODEL"), }, payment: { provider: readString(env, "PAYMENT_PROVIDER"), apiKey: readString(env, "PAYMENT_PROVIDER_API_KEY"), webhookSecret: readString(env, "PAYMENT_PROVIDER_WEBHOOK_SECRET"), }, storage: { region: readString(env, "S3_REGION"), bucket: readString(env, "S3_BUCKET"), accessKey: readString(env, "S3_ACCESS_KEY"), secretKey: readString(env, "S3_SECRET_KEY"), forcePathStyle: readBoolean(env, "S3_FORCE_PATH_STYLE"), }, telegram: { botToken: readString(env, "TELEGRAM_BOT_TOKEN"), botMode: readTelegramMode(env, "TELEGRAM_BOT_MODE"), }, email: { provider: readString(env, "EMAIL_PROVIDER"), from: readString(env, "EMAIL_FROM"), apiKey: readString(env, "EMAIL_API_KEY"), }, keyPool: { cooldownMinutes: readInteger(env, "KEY_COOLDOWN_MINUTES"), failuresBeforeManualReview: readInteger(env, "KEY_FAILURES_BEFORE_MANUAL_REVIEW"), balancePollSeconds: readInteger(env, "KEY_BALANCE_POLL_SECONDS"), }, }; } function readString(env: NodeJS.ProcessEnv, key: string): string { const value = env[key]; if (!value) { throw new Error(`Missing required environment variable: ${key}`); } return value; } function readInteger(env: NodeJS.ProcessEnv, key: string): number { const value = readString(env, key); const parsed = Number.parseInt(value, 10); if (!Number.isInteger(parsed)) { throw new Error(`Environment variable ${key} must be an integer`); } return parsed; } function readBoolean(env: NodeJS.ProcessEnv, key: string): boolean { const value = readString(env, key).toLowerCase(); if (value === "true") { return true; } if (value === "false") { return false; } throw new Error(`Environment variable ${key} must be "true" or "false"`); } function readUrl(env: NodeJS.ProcessEnv, key: string): URL { return new URL(readString(env, key)); } function readTelegramMode(env: NodeJS.ProcessEnv, key: string): "polling" { const value = readString(env, key); if (value !== "polling") { throw new Error(`Environment variable ${key} must be "polling" for MVP`); } return value; }