import { z } from "zod"; /** * Fail fast on boot if a required secret/config value is missing — same * philosophy as the legacy's JWT_SECRET/SEED_ADMIN_* checks (eden.md §5.6). * Never fall back to an insecure default for a secret. */ export const envSchema = z.object({ DATABASE_URL: z.string().min(1, "DATABASE_URL é obrigatório"), EDEN_API_PORT: z.coerce.number().int().positive().default(8080), JWT_SIGNING_SECRET: z.string().min(32, "JWT_SIGNING_SECRET precisa ter ao menos 32 caracteres"), SIGNATURE_OTP_SECRET: z.string().min(32).optional(), EDEN_FIELD_ENCRYPTION_KEY: z .string() .min(32, "EDEN_FIELD_ENCRYPTION_KEY precisa ter ao menos 32 caracteres") .optional(), EDEN_SUPERADMIN_EMAIL: z.string().email().optional(), EDEN_SUPERADMIN_PASSWORD: z.string().min(8).optional(), SMTP_HOST: z.string().optional(), SMTP_PORT: z.coerce.number().int().positive().default(465), SMTP_USER: z.string().optional(), SMTP_PASS: z.string().optional(), SMTP_FROM: z.string().optional(), S3_ENDPOINT: z.string().optional(), S3_REGION: z.string().default("us-east-1"), S3_FORCE_PATH_STYLE: z .string() .default("true") .transform((v) => v === "true"), S3_ACCESS_KEY_ID: z.string().optional(), S3_SECRET_ACCESS_KEY: z.string().optional(), S3_BUCKET: z.string().optional(), PUBLIC_BASE_URL: z.string().optional(), /** * Comma-separated list of allowed browser origins for CORS. The API has * no public domain of its own (operator decision) — production traffic * reaches it same-origin through each frontend's reverse proxy, so this * only matters for local dev, where the Vite dev server (localhost:3001+) * and the API (localhost:8080) are different origins. Optional — when * unset, CORS stays effectively disabled (no browser origin is allowed * cross-origin). Never a wildcard: the API issues bearer tokens, and a * wildcard origin combined with credentialed requests is a known * CSRF-adjacent misconfiguration. */ EDEN_CORS_ORIGINS: z.string().optional(), /** * Chromium PDF rendering (packages/integrations/src/pdf/renderer.ts) — * ported from the legacy's chromiumPdfRenderer.js. Concurrency is capped * (a headless Chromium tab is expensive) and every render has a hard * timeout so a pathological document can never hang a request forever. */ PDF_MAX_CONCURRENCY: z.coerce.number().int().positive().default(3), PDF_RENDER_TIMEOUT_SECONDS: z.coerce.number().int().positive().default(15), }); export type EdenEnv = z.infer; export function loadEnv(source: NodeJS.ProcessEnv = process.env): EdenEnv { const result = envSchema.safeParse(source); if (!result.success) { const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n"); throw new Error(`Configuração de ambiente inválida:\n${issues}`); } return result.data; }