import { z } from 'zod'; const Env = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), DATABASE_URL: z.string(), REDIS_URL: z.string().default('redis://localhost:6379'), PORT: z.coerce.number().default(4000), NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3001'), OAUTH_KEY_DIR: z.string().default('./keys'), ANTHROPIC_API_KEY: z.string().optional(), SECRETS_ENCRYPTION_KEY: z .string() .min(64, '32 bytes hex required') .default('0000000000000000000000000000000000000000000000000000000000000000'), CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'), ADMIN_EMAIL: z.string().email().optional(), ADMIN_PASSWORD: z.string().min(8).optional(), ADMIN_NAME: z.string().optional(), }); export const config = Env.parse({ NODE_ENV: process.env.NODE_ENV, DATABASE_URL: process.env.DATABASE_URL, REDIS_URL: process.env.REDIS_URL, PORT: process.env.PORT, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, OAUTH_KEY_DIR: process.env.OAUTH_KEY_DIR, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, SECRETS_ENCRYPTION_KEY: process.env.SECRETS_ENCRYPTION_KEY, CONTROL_PLANE_PUBLIC_URL: process.env.CONTROL_PLANE_PUBLIC_URL, ADMIN_EMAIL: process.env.ADMIN_EMAIL, ADMIN_PASSWORD: process.env.ADMIN_PASSWORD, ADMIN_NAME: process.env.ADMIN_NAME, }); // INFRA-001: refuse to boot in production with the placeholder encryption key. // The zero-key passes the min(64) length check but would render every stored // secret effectively plaintext. const ZERO_KEY = '0'.repeat(64); if (config.NODE_ENV === 'production' && config.SECRETS_ENCRYPTION_KEY === ZERO_KEY) { throw new Error( 'SECRETS_ENCRYPTION_KEY is the all-zero placeholder. Set a real 32-byte hex key ' + '(openssl rand -hex 32) before running in production.', ); } export type Config = z.infer;