Files
sar/apps/api/src/app/config/env.schema.ts
julian 055f9f98f0 feat(api): foundation canon JCS — Pino+CLS+RFC9457+health+ping (Frente A)
Estabelece a fundação operacional de apps/api conforme STACK.md v2.2 e
CODING-RULES.md v2.0, substituindo o hello-world scaffolded.

- tracing.ts primeiro import (PGD-OBS-001) — stub OTel ativável por env
- EnvSchema Zod 4 com fail-fast (superRefine guarda prod) + EnvModule global
- nestjs-pino com redact LGPD (*.cpf|*.cardNumber|*.password|auth|cookie)
- main.ts hardenizado: helmet, CORS por env, compression, versionamento URI
  /api/v1, graceful shutdown
- ProblemDetailsFilter global (RFC 9457 application/problem+json), Zod -> 422
- Health endpoints /api/v1/health/{live,ready} com memory.checkHeap(350MB)
  (ready skeleton documenta K=3 LRU pool conforme PGD-OBS-003)
- WorkspaceModule via ClsModule.forRootAsync — requestId+workspaceId no CLS,
  idGenerator alinhado com pino-http para mesmo UUID em header e body
- GET /api/v1/ping retornando workspaceId+requestId (alvo de smoke test e
  futuro healthcheck do docker compose)
- ZodValidationPipe (nestjs-zod) como APP_PIPE global
- tsconfig.app.json target ES2023 (alinhado ao base)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:50:42 +00:00

99 lines
3.6 KiB
TypeScript

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<typeof EnvSchema>;
export function validateEnv(raw: Record<string, unknown>): 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;
}