Files
eden/packages/config/src/env-schema.ts
Matheus (Handix) 29430d0d96 feat(documents): renderização real de PDF via Chromium headless
Substitui a geração de PDF simulada (pdf_s3_key/pdf_sha256/pdf_size_bytes
sempre nulos) por Chromium real (Playwright), portado do
chromiumPdfRenderer.js do legado: SSRF impossível por construção
(setContent + bloqueio de toda requisição de rede da página), browser
único reaproveitado com relançamento automático, semáforo de
concorrência + timeout duro.

Infra: MinIO (compose.yaml, primeiro consumidor real do adapter S3) e
instalação condicional do Chromium só na imagem da API
(Dockerfile.node). DocumentsService.createGeneration agora renderiza,
faz upload e persiste hash/tamanho de verdade; novo endpoint de
download GET /v1/documents/generations/:id/pdf.

Verificado ao vivo através do container real: PDF gerado, baixado, e
SHA-256 conferido byte a byte contra o valor persistido no Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
2026-09-04 09:26:31 -03:00

73 lines
2.9 KiB
TypeScript

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<typeof envSchema>;
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;
}