// backend/src/config/configuration.ts /** Matches values accepted by jsonwebtoken `expiresIn` (via ms), e.g. 7d, 15m, or plain seconds. */ const JWT_TIMESPAN_PATTERN = /^\d+(\.\d+)?(ms|s|m|h|d|w|y)?$/i; function assertJwtSecret(value: string, envKey: string): string { const trimmed = value.trim(); if (!trimmed) { throw new Error(`❌ Environment variable ${envKey} is required but not set`); } if (trimmed.length < 16) { throw new Error(`❌ ${envKey} must be at least 16 characters`); } if (/CHANGE_ME/i.test(trimmed)) { throw new Error(`❌ ${envKey} must be changed from the placeholder value`); } return trimmed; } function assertJwtTimespan(value: string, envKey: string): string { const trimmed = value.trim(); if (!trimmed) { throw new Error(`❌ Environment variable ${envKey} is required but not set`); } if (!/^\d+$/.test(trimmed) && !JWT_TIMESPAN_PATTERN.test(trimmed)) { throw new Error( `❌ ${envKey}="${value}" is invalid. Use a duration like 7d, 15m, 30d, or a number of seconds.`, ); } return trimmed; } export interface Config { port: number; database: { url: string; }; jwt: { secret: string; expiresIn: string; refreshSecret: string; refreshExpiresIn: string; }; throttle: { ttl: number; limit: number; }; sms: { apiKey: string | null; templateId: number; }; } export default (): Config => { // Helper function to get required env var with type safety const getEnvVar = (key: string): string => { const value = process.env[key]; if (!value) { throw new Error(`❌ Environment variable ${key} is required but not set`); } return value; }; // Helper for optional env vars with defaults (whitespace-only counts as unset) const getEnvVarWithDefault = (key: string, defaultValue: string): string => { const value = process.env[key]?.trim(); return value ? value : defaultValue; }; const getEnvVarAsNumber = (key: string, defaultValue: number): number => { const value = process.env[key]; if (!value) return defaultValue; const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed; }; const jwtSecret = assertJwtSecret(getEnvVar('JWT_SECRET'), 'JWT_SECRET'); const jwtExpiresIn = assertJwtTimespan( getEnvVarWithDefault('JWT_EXPIRES_IN', '7d'), 'JWT_EXPIRES_IN', ); const jwtRefreshSecret = assertJwtSecret( getEnvVarWithDefault('JWT_REFRESH_SECRET', jwtSecret), 'JWT_REFRESH_SECRET', ); const jwtRefreshExpiresIn = assertJwtTimespan( getEnvVarWithDefault('JWT_REFRESH_EXPIRES_IN', '30d'), 'JWT_REFRESH_EXPIRES_IN', ); return { port: getEnvVarAsNumber('PORT', 3000), database: { url: getEnvVar('DATABASE_URL'), }, jwt: { secret: jwtSecret, expiresIn: jwtExpiresIn, refreshSecret: jwtRefreshSecret, refreshExpiresIn: jwtRefreshExpiresIn, }, throttle: { ttl: getEnvVarAsNumber('THROTTLE_TTL', 60), limit: getEnvVarAsNumber('THROTTLE_LIMIT', 100), }, sms: { apiKey: process.env.SMS_IR_API_KEY?.trim() || null, templateId: getEnvVarAsNumber('SMS_IR_TEMPLATE_ID', 123456), }, }; };