import type { NextConfig } from "next"; // Runs when the Node server process starts inside the container, so this // correctly reads the deployment's actual runtime API_URL (see // src/lib/config/runtime-config.ts for why API_URL is a runtime env var // rather than baked into the client bundle at build time) - this is server // startup, not the browser bundle, so it isn't subject to that constraint. function apiOrigin(): string { try { return new URL(process.env.API_URL ?? "http://localhost:3001").origin; } catch { return ""; } } // No CSP nonce here on purpose: nonce-based CSP requires every page to be // dynamically rendered (Next.js CSP guide, "Static vs Dynamic Rendering"), // which would drop this app's static optimization across the board. This // policy still blocks the things that matter for a mostly-same-origin app - // cross-origin exfiltration, clickjacking, base-tag/form hijacking - via // 'unsafe-inline' only for script/style (needed for Next's own hydration // data and Tailwind's inline styles), matching the same trade-off the // backend's own helmet CSP already makes for style-src. function cspHeader(): string { return ` default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self'; connect-src 'self' ${apiOrigin()}; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests; ` .replace(/\s{2,}/g, " ") .trim(); } const nextConfig: NextConfig = { output: "standalone", async headers() { return [ { // Browsers are supposed to bypass HTTP cache when checking for a new // service worker, but that's not reliably honored everywhere - an // explicit no-cache header here is the only way to guarantee a sw.js // fix actually reaches already-open tabs promptly after a deploy, // instead of possibly sitting cached for hours. source: "/sw.js", headers: [{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" }], }, { source: "/:path*", headers: [ { key: "Content-Security-Policy", value: cspHeader() }, { key: "X-Frame-Options", value: "SAMEORIGIN" }, { key: "X-Content-Type-Options", value: "nosniff" }, { key: "Referrer-Policy", value: "no-referrer" }, { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" }, ], }, ]; }, }; export default nextConfig;