import type { NextConfig } from "next"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.dirname(fileURLToPath(import.meta.url)); function hostOf(value: string | undefined): string | null { if (!value) return null; try { return new URL(value).origin; } catch { return null; } } /** * Content-Security-Policy. `unsafe-inline` em script-src é necessário para o * bootstrap do App Router sem nonce; o restante é fechado. `connect-src` só * aceita o próprio host, o Supabase do ambiente e o Sentry. */ function contentSecurityPolicy(): string { const supabase = hostOf(process.env.NEXT_PUBLIC_SUPABASE_URL); const connect = ["'self'", supabase, supabase?.replace("https://", "wss://"), "https://*.sentry.io"].filter( Boolean, ) as string[]; return [ "default-src 'self'", "base-uri 'self'", "form-action 'self'", "frame-ancestors 'none'", "object-src 'none'", "img-src 'self' data: blob:", "font-src 'self' data:", "style-src 'self' 'unsafe-inline'", "script-src 'self' 'unsafe-inline'", `connect-src ${connect.join(" ")}`, "worker-src 'self' blob:", "upgrade-insecure-requests", ].join("; "); } const securityHeaders = [ { key: "X-Content-Type-Options", value: "nosniff" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "X-Frame-Options", value: "DENY" }, { key: "Content-Security-Policy", value: contentSecurityPolicy() }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), payment=(), usb=()" }, { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, { key: "X-DNS-Prefetch-Control", value: "off" }, // HSTS só faz sentido atrás de TLS. O Nginx do ecossistema termina o TLS. { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" }, ]; const nextConfig: NextConfig = { turbopack: { root }, // Saída autocontida para a imagem de runtime. output: "standalone", outputFileTracingRoot: root, // `migrations/` é lido em runtime pelo migrate e pelo health check. outputFileTracingIncludes: { "/api/health/ready": ["./migrations/**/*.sql"], }, serverExternalPackages: [ "exceljs", "@react-pdf/renderer", "postgres", "ioredis", "@electric-sql/pglite", ], poweredByHeader: false, async headers() { return [ { source: "/:path*", headers: securityHeaders }, { source: "/api/:path*", headers: [...securityHeaders, { key: "Cache-Control", value: "no-store" }], }, ]; }, }; /** * Upload de sourcemap e release só acontece quando a organização, o projeto e o * token existem no ambiente de build. Sem isto o build local e o CI continuam * funcionando, e o stack trace do servidor chega legível ao Sentry quando o * pipeline fornece os segredos. */ function withSentryIfConfigured(config: NextConfig): NextConfig { const org = process.env.SENTRY_ORG; const project = process.env.SENTRY_PROJECT; if (!org || !project || !process.env.SENTRY_AUTH_TOKEN) return config; const nodeRequire = createRequire(import.meta.url); const { withSentryConfig } = nodeRequire("@sentry/nextjs") as typeof import("@sentry/nextjs"); return withSentryConfig(config, { org, project, authToken: process.env.SENTRY_AUTH_TOKEN, silent: true, widenClientFileUpload: true, disableLogger: true, sourcemaps: { deleteSourcemapsAfterUpload: true }, }); } export default withSentryIfConfigured(nextConfig);