diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index 12e7a62a..5bbbc186 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -4138,8 +4138,13 @@ export const REACTIVE_RUNTIME = String.raw` } function callServerFunction(component, functionName, args) { + var csrfMeta = document.querySelector('meta[name="wrnexus-csrf"]'); var csrfMatch = /(?:^|;\s*)wire-csrf=([^;]+)/.exec(document.cookie || ""); - var csrf = csrfMatch ? decodeURIComponent(csrfMatch[1]) : ""; + var csrf = csrfMeta + ? csrfMeta.getAttribute("content") || "" + : csrfMatch + ? decodeURIComponent(csrfMatch[1]) + : ""; return fetch("/__wrnexus/rpc", { method: "POST", credentials: "same-origin", diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts index e0fe8683..964e982e 100644 --- a/packages/csr/test/reactive.test.ts +++ b/packages/csr/test/reactive.test.ts @@ -828,6 +828,34 @@ test("component server calls send the CSRF cookie in the RPC header", async () = expect(new Headers(request?.headers).get("x-csrf-token")).toBe("rpc token"); }); +test("component server calls prefer the rendered CSRF token over stale cookies", async () => { + const win = mount( + `` + + `
` + + `` + + `
`, + ); + Object.defineProperty(win.document, "cookie", { + configurable: true, + value: "wire-csrf=stale-token", + }); + let request: RequestInit | undefined; + const globals = globalThis as Record; + const originalFetch = globals.fetch; + globals.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => { + request = init; + return Response.json({ ok: true }); + }; + try { + (win.document.querySelector("button") as unknown as HTMLElement).click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + } finally { + globals.fetch = originalFetch; + } + + expect(new Headers(request?.headers).get("x-csrf-token")).toBe("current-token"); +}); + test("development runtime warns when a component binding names a missing function", () => { const win = new Window() as unknown as Window & Record; win.document.body.innerHTML = diff --git a/packages/dev-server/src/runtime.ts b/packages/dev-server/src/runtime.ts index dc31eeb6..a6a96fa9 100644 --- a/packages/dev-server/src/runtime.ts +++ b/packages/dev-server/src/runtime.ts @@ -1873,6 +1873,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers { body: renderedBody, scripts, extraHead: [ + ``, preserve ? `` : "", pwaEnabled ? `` : "", pwaEnabled ? `` : "", diff --git a/packages/dev-server/test/actions-runtime.test.ts b/packages/dev-server/test/actions-runtime.test.ts index 4dbd26b1..163ce66b 100644 --- a/packages/dev-server/test/actions-runtime.test.ts +++ b/packages/dev-server/test/actions-runtime.test.ts @@ -51,6 +51,7 @@ test("server actions validate, enforce CSRF, invalidate, and progressively enhan const page = await handlers.fetch(new Request("https://example.test/users"), server); const html = await page!.text(); expect(html).toContain('name="_csrf"'); + expect(html).toContain('name="wrnexus-csrf"'); expect(html).toContain("/__wrnexus/actions.js"); const cookie = page!.headers.get("set-cookie")!; const token = /wire-csrf=([^;]+)/.exec(cookie)?.[1];