/** * Environment variables parser. * Parsed once at startup — all env access should go through this module. */ export interface Env { IMMICH_API_URL: string; IMMICH_API_KEY: string; SITE_TITLE: string; SITE_SUBTITLE: string; CACHE_TTL: number; RATE_LIMIT_RPM: number; AUTH_SECRET?: string; } function parseEnv(): Env { const errors: string[] = []; const urlRaw = process.env.IMMICH_API_URL; let apiUrl = ''; if (!urlRaw) { errors.push('IMMICH_API_URL is required'); } else { try { apiUrl = new URL(urlRaw).toString().replace(/\/+$/, ''); } catch { errors.push('IMMICH_API_URL must be a valid URL'); } } const apiKey = process.env.IMMICH_API_KEY; if (!apiKey) { errors.push('IMMICH_API_KEY is required'); } if (errors.length > 0) { const formatted = errors.map((err) => ` āœ— ${err}`).join('\n'); throw new Error(`\nāŒ Environment validation failed:\n${formatted}\n`); } const cacheTtlStr = process.env.CACHE_TTL; const cacheTtl = cacheTtlStr && !isNaN(parseInt(cacheTtlStr, 10)) ? parseInt(cacheTtlStr, 10) : 300; const rateLimitStr = process.env.RATE_LIMIT_RPM; const rateLimit = rateLimitStr && !isNaN(parseInt(rateLimitStr, 10)) ? parseInt(rateLimitStr, 10) : 600; return { IMMICH_API_URL: apiUrl, IMMICH_API_KEY: apiKey as string, SITE_TITLE: process.env.SITE_TITLE || 'Gallery', SITE_SUBTITLE: process.env.SITE_SUBTITLE || '', CACHE_TTL: Math.max(0, cacheTtl), RATE_LIMIT_RPM: Math.max(1, rateLimit), AUTH_SECRET: process.env.AUTH_SECRET, }; } /** Validated, typed environment variables. */ export const env = parseEnv();