fix(canvas): make root layout dynamic so CSP nonce reaches Next scripts
Tenant page loads were failing with repeated CSP violations: Executing inline script violates ... script-src 'self' 'nonce-M2M4YTVh...' 'strict-dynamic'. ... because Next.js's bootstrap inline scripts were emitted without a nonce attribute. The middleware was generating per-request nonces correctly and sending them via `x-nonce` — but the layout was fully static, so Next.js cached the HTML once and served that cached bundle (no nonces baked in) for every request. Fix: call `await headers()` in the root layout. That opts the tree into dynamic rendering AND signals Next.js to propagate the x-nonce value to its own generated <script> tags. The `nonce` return value is intentionally unused — the framework handles its bootstrap scripts automatically once the read happens. Future code that adds third-party <Script> components (analytics, etc.) should pass the returned nonce explicitly. Verified against live tenant: before this change every /_next/ chunk script tag in the HTML had no nonce attribute; expected after deploy is `<script nonce="..." src="/_next/...">` on each. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1eea9e04ba
commit
1af6f696a2
@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
import "./globals.css";
|
||||
import { AuthGate } from "@/components/AuthGate";
|
||||
import { CookieConsent } from "@/components/CookieConsent";
|
||||
@ -8,11 +9,35 @@ export const metadata: Metadata = {
|
||||
description: "AI Org Chart Canvas",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// Read the per-request CSP nonce that middleware.ts sets via the
|
||||
// `x-nonce` request header. This call is load-bearing for TWO
|
||||
// independent reasons:
|
||||
//
|
||||
// 1. It opts the root layout into dynamic rendering. Without a
|
||||
// `headers()` / `cookies()` / `noStore()` call, Next.js treats
|
||||
// the layout as statically pre-rendered and serves the SAME
|
||||
// HTML for every request — which means the Next.js bootstrap
|
||||
// <script> tags bake into the HTML without any nonce. The
|
||||
// browser then rejects every one with a CSP violation because
|
||||
// the header demands nonce-only script execution.
|
||||
//
|
||||
// 2. Next.js 15 propagates the nonce to its own generated inline
|
||||
// scripts (the __next_f chunk push frames) ONLY when the header
|
||||
// is actually read via `headers()`. The header's existence on
|
||||
// the request isn't enough — Next.js watches for the read.
|
||||
//
|
||||
// Keeping the `nonce` variable unused is intentional: we don't need
|
||||
// to pass it to any custom <Script nonce={...}> tags right now, the
|
||||
// framework takes care of its own bootstrap scripts once the read
|
||||
// happens. Destructuring via `await` + `.get()` is the minimum shape
|
||||
// Next.js recognizes as "dynamic server-side access".
|
||||
await headers();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="bg-zinc-950 text-white">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user