import { z } from 'zod'; /** * The process refuses to boot with an invalid environment. * * Failing at startup — loudly, with every problem listed at once — is the only * acceptable behaviour. A missing JWT secret discovered at 2am by a customer * hitting login is not. */ const durationSchema = z.string().regex(/^\d+(ms|s|m|h|d)$/, 'Use a duration like 15m, 24h or 30d'); export const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.coerce.number().int().min(1).max(65535).default(4000), API_GLOBAL_PREFIX: z.string().default('api'), APP_VERSION: z.string().default('0.0.0'), CORS_ORIGINS: z .string() .default('') .transform((value) => value .split(',') .map((origin) => origin.trim()) .filter(Boolean), ), DATABASE_URL: z.string().startsWith('postgresql://'), REDIS_URL: z.string().startsWith('redis'), REDIS_KEY_PREFIX: z.string().default('sport:'), JWT_ACCESS_SECRET: z.string().min(32, 'Use at least 32 characters'), JWT_REFRESH_SECRET: z.string().min(32, 'Use at least 32 characters'), JWT_ACCESS_TTL: durationSchema.default('15m'), JWT_REFRESH_TTL: durationSchema.default('30d'), JWT_ISSUER: z.string().default('sport-store'), STORAGE_ENDPOINT: z.url(), STORAGE_REGION: z.string().default('auto'), STORAGE_BUCKET: z.string().min(1), STORAGE_ACCESS_KEY_ID: z.string().min(1), STORAGE_SECRET_ACCESS_KEY: z.string().min(1), STORAGE_FORCE_PATH_STYLE: z.stringbool().default(false), STORAGE_PUBLIC_URL: z.url(), RATE_LIMIT_TTL_SECONDS: z.coerce.number().int().positive().default(60), RATE_LIMIT_MAX: z.coerce.number().int().positive().default(120), LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), LOG_PRETTY: z.stringbool().default(false), }); export type Env = z.infer; export function validateEnv(raw: Record): Env { const result = envSchema.safeParse(raw); if (!result.success) { const details = result.error.issues .map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`) .join('\n'); throw new Error(`Invalid environment configuration:\n${details}`); } if (result.data.JWT_ACCESS_SECRET === result.data.JWT_REFRESH_SECRET) { throw new Error('JWT_ACCESS_SECRET and JWT_REFRESH_SECRET must be different values.'); } return result.data; }