77 lines
3.2 KiB
TypeScript
77 lines
3.2 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 permits the
|
|
// separately hosted LiveKit SFU and Cloudflare Turnstile 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' https://challenges.cloudflare.com${isDev ? " 'unsafe-eval'" : ""}`,
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data: blob:",
|
|
"font-src 'self' data:",
|
|
"connect-src 'self' https://challenges.cloudflare.com wss: stun: turn: turns:",
|
|
"media-src 'self' blob:",
|
|
"frame-src 'self' https://challenges.cloudflare.com",
|
|
"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 });
|