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>
99 lines
3.6 KiB
TypeScript
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;
|
|
}
|