import type { NextConfig } from "next"; // Enforcing, but deliberately permissive on script-src. // // A nonce-based script-src would mean reading headers() in the root layout, // which opts every route out of static rendering — today `/` is served from // cache with s-maxage=31536000. That trade is not worth it while the only // inline script is the theme FOUC guard in src/app/layout.tsx and the XSS sink // it would defend has already been removed (C-4 in .planning/SECURITY-SCAN.md). // // The remaining directives cost nothing and still close real avenues: no // plugins, no hijacking, no framing, no posting form data off-site, and // no exfiltration channel to an arbitrary host. const contentSecurityPolicy = [ "default-src 'self'", "script-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self'", "object-src 'none'", "base-uri 'self'", "form-action 'self'", "frame-ancestors 'none'", "upgrade-insecure-requests", ].join("; "); const securityHeaders = [ { 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=(), microphone=(), geolocation=()" }, { key: "X-DNS-Prefetch-Control", value: "on" }, // Client dashboard tokens travel in the URL path, so an active downgrade // would expose them in cleartext — HSTS matters more here than usual. { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload", }, { key: "Content-Security-Policy", value: contentSecurityPolicy }, ]; const nextConfig: NextConfig = { output: "standalone", async headers() { return [{ source: "/(.*)", headers: securityHeaders }]; }, }; export default nextConfig;