import { z } from 'zod'; // CODING-RULES.md §08: validação Zod no boot (fail-fast). Sem default em prod. // Em dev, defaults razoáveis facilitam o setup; em prod o Vault Agent injeta tudo. const NodeEnv = z.enum(['development', 'test', 'staging', 'production']); export const EnvSchema = z .object({ NODE_ENV: NodeEnv.default('development'), // API API_PORT: z.coerce.number().int().positive().default(3000), API_HOST: z.string().min(1).default('0.0.0.0'), API_GLOBAL_PREFIX: z.string().min(1).default('api'), API_VERSION: z.string().regex(/^v\d+$/).default('v1'), // CORS — origens permitidas (Web em dev: http://localhost:4200) CORS_ORIGINS: z .string() .default('http://localhost:4200') .transform((s) => s .split(',') .map((o) => o.trim()) .filter(Boolean), ), // Master-login (DEV stub — IdP real virá na próxima sessão) MASTER_LOGIN_URL: z.url().default('http://localhost:3000/auth/dev'), MASTER_LOGIN_JWT_SECRET: z.string().min(32).default('dev_jwt_secret_change_in_prod_use_vault_xxxxx'), JWT_ACCESS_EXPIRATION: z.coerce.number().int().positive().default(900), JWT_REFRESH_EXPIRATION: z.coerce.number().int().positive().default(2_592_000), // Multi-tenancy — workspace de dev (até master-login real entrar) DEFAULT_WORKSPACE_ID: z.string().min(1).default('dev-workspace'), // Postgres (Prisma virá depois) DATABASE_URL: z.string().optional(), MIGRATION_DATABASE_URL: z.string().optional(), // Valkey / BullMQ (entrarão com filas) REDIS_URL: z.url().default('redis://localhost:6379'), // MinIO (entrará com uploads) S3_ENDPOINT: z.url().optional(), S3_REGION: z.string().optional(), S3_ACCESS_KEY: z.string().optional(), S3_SECRET_KEY: z.string().optional(), S3_BUCKET: z.string().optional(), // SMTP (Mailpit dev / Resend prod) SMTP_HOST: z.string().default('localhost'), SMTP_PORT: z.coerce.number().int().positive().default(1025), SMTP_FROM: z.email().default('noreply@sar.dev'), // Telemetry OTEL_SERVICE_NAME: z.string().default('sar-api'), OTEL_EXPORTER_OTLP_ENDPOINT: z.url().optional(), OTEL_TRACES_SAMPLER: z.string().default('parentbased_traceidratio'), OTEL_TRACES_SAMPLER_ARG: z.coerce.number().min(0).max(1).default(1.0), SENTRY_DSN: z.string().optional(), // Feature flags GROWTHBOOK_API_HOST: z.string().optional(), GROWTHBOOK_CLIENT_KEY: z.string().optional(), // Logger LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), }) .superRefine((env, ctx) => { if (env.NODE_ENV === 'production') { if (env.MASTER_LOGIN_JWT_SECRET.startsWith('dev_jwt_secret')) { ctx.addIssue({ code: 'custom', path: ['MASTER_LOGIN_JWT_SECRET'], message: 'JWT secret de DEV não pode ser usada em produção (CODING-RULES §08, PGD-SEC-002).', }); } if (!env.DATABASE_URL) { ctx.addIssue({ code: 'custom', path: ['DATABASE_URL'], message: 'obrigatório em produção' }); } } }); export type Env = z.infer; export function validateEnv(raw: Record): Env { const result = EnvSchema.safeParse(raw); if (!result.success) { // Fail-fast: agrupa erros legíveis antes do crash. const issues = result.error.issues .map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`) .join('\n'); throw new Error(`[env] validação falhou — corrija .env:\n${issues}`); } return result.data; }