Files
anouma/next.config.ts
T

77 lines
3.1 KiB
TypeScript

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 Next App Router emits inline RSC flight/bootstrap
// scripts that are required for hydration. 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' 'unsafe-inline'${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 });