import { withPayload } from "@payloadcms/next/withPayload"; import type { NextConfig } from "next"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const dirname = path.dirname(__filename); // Applied to every route, including /admin and /api — none of these // constrain what the page can load/render, so they can't break the Payload // admin bundle. Camera/microphone/display-capture are allowed for the // site's own origin because the WebRTC call pages (app/(call)/**) need them. const baseSecurityHeaders = [ { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "SAMEORIGIN" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "Permissions-Policy", value: "camera=(self), microphone=(self), display-capture=(self), geolocation=(), payment=()" }, // Browsers only ever honor this over an actual HTTPS connection, so it's // harmless to send in local http:// dev too. { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" }, ]; // Deliberately NOT applied to /admin or /api: Payload's admin bundle isn't // audited here for CSP compatibility, and API routes serve data, not HTML, // so a content policy doesn't apply to them anyway. connect-src stays broad // because the WebRTC call pages connect to the separately hosted LiveKit SFU // at runtime. style-src needs // 'unsafe-inline' because framer-motion drives its animations through // inline style attributes. script-src allows 'unsafe-eval' only in dev, // matching Next's own documented CSP guidance — React uses eval there for // server-error-stack reconstruction; neither React nor Next use it in a // production build. const isDev = process.env.NODE_ENV === "development"; const frontendCSP = [ "default-src 'self'", `script-src 'self'${isDev ? " 'unsafe-eval'" : ""}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self' data:", "connect-src 'self' https: wss: stun: turn: turns:", "media-src 'self' blob:", "frame-ancestors 'self'", "base-uri 'self'", "form-action 'self'", "object-src 'none'", "upgrade-insecure-requests", ].join("; "); const nextConfig: NextConfig = { // NOT "standalone": this project uses a custom server (server.ts), and // standalone output can't be combined with // a custom server. The Dockerfile runs the full app via `npm start` instead. images: { localPatterns: [ { pathname: "/api/media/file/**", }, ], }, // Two root layouts exist — app/(frontend) and app/(payload) — so a single // not-found.tsx can't compose a 404; see app/global-not-found.tsx. experimental: { globalNotFound: true, }, turbopack: { root: path.resolve(dirname), }, async headers() { return [ { source: "/:path*", headers: baseSecurityHeaders }, { source: "/((?!admin|api).*)", headers: [{ key: "Content-Security-Policy", value: frontendCSP }] }, ]; }, }; export default withPayload(nextConfig, { devBundleServerPackages: false });