import type { NextConfig } from 'next'; const isProd = process.env.NODE_ENV === 'production'; /** * Security headers applied to every response. Per audit-pass-#3 finding: * the previous config emitted no CSP, X-Frame-Options, HSTS, or * X-Content-Type-Options — the app was open to clickjacking + MIME * sniffing. * * CSP notes: * - 'unsafe-inline' on style-src is required by Tailwind's runtime * style injection and Radix; revisit when Tailwind v4 ships a * nonce story. * - 'unsafe-eval' on script-src is dev-only — Next dev uses eval for * HMR. Production drops it. * - connect-src allows ws/wss for Socket.IO and https: for outgoing * fetches; tighten in prod via per-port branding URLs once we move * the s3 image references into a known allowlist. * - img-src https: is wide because port branding pulls from * s3.portnimara.com plus per-port image URLs configured at runtime. */ // Dev-only allow-list: react-grab (the in-page click-to-source devtool) // is fetched from unpkg, so script/style/connect must allow it. Strip // these entries in prod via the conditional below. const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com'; const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com'; const csp = [ "default-src 'self'", `script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https:", "font-src 'self' data:", `connect-src 'self' ws: wss: https:${devConnectHosts}`, "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", "object-src 'none'", ].join('; '); const securityHeaders = [ { key: 'Content-Security-Policy', value: csp }, { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(self), microphone=(), geolocation=()' }, ...(isProd ? [{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }] : []), ]; const nextConfig: NextConfig = { output: 'standalone', serverExternalPackages: [ 'pino', 'pino-pretty', 'bullmq', 'ioredis', 'minio', 'postgres', 'better-auth', 'nodemailer', ], images: { remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }], }, experimental: { typedRoutes: true, }, outputFileTracingIncludes: { // Bundle the EOI source PDF so the in-app EOI pathway can read it at // runtime in the standalone build. Reading via fs.readFile from // process.cwd() requires the file to be traced explicitly. '/api/v1/document-templates/**': ['./assets/eoi-template.pdf'], }, async headers() { return [ { source: '/:path*', headers: securityHeaders, }, ]; }, }; export default nextConfig;