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
73 lines
2.9 KiB
TypeScript
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;
|
|
}
|