diff --git a/proxy.ts b/proxy.ts index 325004c..1d81165 100644 --- a/proxy.ts +++ b/proxy.ts @@ -41,22 +41,22 @@ function sentryIngestOrigin(): string | null { } } -// Build the per-request Content-Security-Policy. `script-src` carries a -// per-request nonce instead of 'unsafe-inline'. `style-src` keeps -// 'unsafe-inline' because Radix / Tailwind / framer-motion inject inline -// styles and removing it would break the UI. Next.js reads the nonce from the -// `content-security-policy` request header and applies it to its own inline -// scripts automatically. -function buildCsp(nonce: string): string { +// Build the Content-Security-Policy. `script-src` uses 'unsafe-inline' because +// Next.js 16's Turbopack build does NOT stamp a per-request nonce onto its +// inline hydration scripts (`self.__next_f.push(...)`). A nonce-based policy +// therefore blocks those inline scripts and the app never hydrates (blank page). +// `style-src` also keeps 'unsafe-inline' (Radix / Tailwind / framer-motion inject +// inline styles). NOTE: to restore the stricter nonce-based script policy, build +// with webpack (`next build --webpack`) so Next applies the nonce to its scripts. +function buildCsp(): string { const isDev = process.env.NODE_ENV !== "production" const sentry = sentryIngestOrigin() - // In development, Next.js/React and Turbopack HMR require eval() for hot - // reloading and debugging features, and open a dev websocket. These are NOT - // added in production, where the nonce-based policy stays strict. + // Dev additionally needs 'unsafe-eval' (Turbopack HMR) plus a dev websocket + // (added to connect-src below). const scriptSrc = isDev - ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval' https://challenges.cloudflare.com` - : `script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com` + ? `script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com` + : `script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com` const connectSrc = [ "connect-src 'self'", isDev ? "ws: wss:" : "", @@ -135,19 +135,10 @@ export async function proxy(request: NextRequest) { dropStaleSessionCookie = state === "invalid" } - // Per-request CSP nonce. UUID contains only hex + dashes, so it never - // includes HTML-escape characters (which Next rejects in nonces). - const nonce = crypto.randomUUID() - const csp = buildCsp(nonce) + const csp = buildCsp() - // Forward the nonce + CSP on the request headers so Next.js can pick up the - // nonce and apply it to its own inline scripts during render. - const requestHeaders = new Headers(request.headers) - requestHeaders.set("x-nonce", nonce) - requestHeaders.set("Content-Security-Policy", csp) - - const response = NextResponse.next({ request: { headers: requestHeaders } }) - // Also set the CSP on the outgoing response so the browser enforces it. + const response = NextResponse.next() + // Set the CSP on the outgoing response so the browser enforces it. response.headers.set("Content-Security-Policy", csp) if (dropStaleSessionCookie) {