diff --git a/docs/api-reference/veryfront/chat.md b/docs/api-reference/veryfront/chat.md index 9fdbc4ac81..cd7ceacea4 100644 --- a/docs/api-reference/veryfront/chat.md +++ b/docs/api-reference/veryfront/chat.md @@ -246,7 +246,7 @@ Result returned from use agent. | `useAgent` | React hook for agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agent.ts#L48) | | `useAgentMetadata` | React hook for browser-safe source-defined agent metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agent-metadata.ts#L201) | | `useAgents` | React hook that lists the browser-safe agents a project exposes, via `GET /api/agents`. Companion to `useAgentMetadata` (single agent) - use it to drive an agent switcher, e.g. only rendering a picker when `agents.length > 1`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agents.ts#L53) | -| `useAttachments` | `useAttachments` - the headless state hook for chat attachments: a persistent, cross-conversation registry of uploaded files with the upload / remove / list actions. This is the domain primitive; render any UI on top of it (the `AttachmentsPanel` / `AttachmentPill` components are one skin - bring your own). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L352) | +| `useAttachments` | `useAttachments` - the headless state hook for chat attachments: a persistent, cross-conversation registry of uploaded files with the upload / remove / list actions. This is the domain primitive; render any UI on top of it (the `AttachmentsPanel` / `AttachmentPill` components are one skin - bring your own). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L353) | | `useChat` | The core chat session hook: manages messages, streaming status, input, submit, regenerate, and branch navigation for a conversation. Powers `` (L1) and is the L3 headless entry point for building a fully custom chat UI. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/use-chat.ts#L132) | | `useChatContextOptional` | React hook for chat context optional. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L101) | | `useChatErrorHandler` | Handler for use chat error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/error-boundary.tsx#L92) | @@ -264,8 +264,8 @@ Result returned from use agent. | `useMessageParts` | `useMessageParts` - read the current message's parts as data, so a consumer can render them however they like (the headless access point to parts; `Message.Part` is the leaf and `Message.Content` provides the default rendering). Throws outside a `Message`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/message-context.tsx#L117) | | `useStickToBottom` | Track and maintain "stick to bottom" for a scroll container. Attach `scrollRef` to the scrollable container and `contentRef` to the element that grows as messages / tokens arrive; the hook follows that growth while the user is pinned to the bottom. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-stick-to-bottom.ts#L54) | | `useStreaming` | React hook for streaming. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-streaming.ts#L41) | -| `useUpload` | Drive file uploads and expose the resulting attachment lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-upload.ts#L191) | -| `useUploadsRegistry` | `useAttachments` - the headless state hook for chat attachments: a persistent, cross-conversation registry of uploaded files with the upload / remove / list actions. This is the domain primitive; render any UI on top of it (the `AttachmentsPanel` / `AttachmentPill` components are one skin - bring your own). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L352) | +| `useUpload` | Drive file uploads and expose the resulting attachment lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-upload.ts#L192) | +| `useUploadsRegistry` | `useAttachments` - the headless state hook for chat attachments: a persistent, cross-conversation registry of uploaded files with the upload / remove / list actions. This is the domain primitive; render any UI on top of it (the `AttachmentsPanel` / `AttachmentPill` components are one skin - bring your own). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L353) | | `useVoiceInput` | Input payload for use voice. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-voice-input.ts#L102) | ### Classes @@ -466,10 +466,10 @@ Result returned from use agent. | `UseAgentResult` | Result returned from use agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agent.ts#L21) | | `UseAgentsOptions` | Options accepted by `useAgents`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agents.ts#L8) | | `UseAgentsResult` | Result returned from `useAgents`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agents.ts#L17) | -| `UseAttachmentsOptions` | Options for `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L35) | -| `UseAttachmentsRequestState` | Additive remote-request status returned by `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L82) | -| `UseAttachmentsResult` | Result of `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L50) | -| `UseAttachmentsStorageState` | Additive cache-status contract returned by `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L74) | +| `UseAttachmentsOptions` | Options for `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L36) | +| `UseAttachmentsRequestState` | Additive remote-request status returned by `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L83) | +| `UseAttachmentsResult` | Result of `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L51) | +| `UseAttachmentsStorageState` | Additive cache-status contract returned by `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L75) | | `UseChatInputResult` | Result of `useChatInput`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-chat-input.ts#L112) | | `UseChatOptions` | Options accepted by use chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L58) | | `UseChatResult` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L94) | @@ -492,10 +492,10 @@ Result returned from use agent. | `UseStickToBottomResult` | Result of `useStickToBottom`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-stick-to-bottom.ts#L28) | | `UseStreamingOptions` | Options accepted by use streaming. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-streaming.ts#L5) | | `UseStreamingResult` | Result returned from use streaming. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-streaming.ts#L20) | -| `UseUploadOptions` | Options for `useUpload`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-upload.ts#L29) | -| `UseUploadResult` | Result of `useUpload`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-upload.ts#L40) | -| `UseUploadsRegistryOptions` | Options for `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L35) | -| `UseUploadsRegistryResult` | Result of `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L50) | +| `UseUploadOptions` | Options for `useUpload`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-upload.ts#L30) | +| `UseUploadResult` | Result of `useUpload`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-upload.ts#L41) | +| `UseUploadsRegistryOptions` | Options for `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L36) | +| `UseUploadsRegistryResult` | Result of `useAttachments`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L51) | | `UseVoiceInputOptions` | Options accepted by use voice input. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-voice-input.ts#L5) | | `UseVoiceInputResult` | Result returned from use voice input. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-voice-input.ts#L29) | diff --git a/src/react/components/chat/chat/hooks/attachment-csrf.test.tsx b/src/react/components/chat/chat/hooks/attachment-csrf.test.tsx new file mode 100644 index 0000000000..59f15fad33 --- /dev/null +++ b/src/react/components/chat/chat/hooks/attachment-csrf.test.tsx @@ -0,0 +1,330 @@ +/** + * Chat attachments must satisfy CSRF in a deployed environment. + * + * A production build defaults `security.csrf` to on, so the server rejects any + * non-GET request that lacks an `x-csrf-token` header matching the + * `__Host-vf_csrf` cookie. #3611 fixed the AG-UI chat turn itself, but + * attachments travel over two *other* transports, and neither sent the token: + * + * - `useUpload` — the one `` actually wires up + * (`app-mode-chat.tsx`, `controlled-chat.tsx`). Uploads over + * `XMLHttpRequest`, because fetch has no upload-progress event. + * - `useAttachments` — the durable uploads registry, exported from + * `veryfront/chat` for an "Uploads" surface. `POST {url}` to upload, + * `DELETE {url}?id=` to remove. + * + * So a deployed chat *with attachments* still answered 403 after #3611 — while + * `veryfront dev` (CSRF off) kept working and hid the break. + * + * These tests drive the real hooks and pipe whatever they emit through the real + * `CsrfHandler`, so they fail on an actual 403 rather than on a header + * assertion. The registry's list `GET` is a safe method and needs no token. + */ +import { createRoot } from "react-dom/client"; +import { flushSync } from "react-dom"; +import { JSDOM } from "npm:jsdom@28.0.0"; +import { unmountReactRoot } from "#veryfront/react/react-root.test-helpers.ts"; +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { waitFor } from "#veryfront/testing/deno-compat.ts"; +import { CsrfHandler } from "#veryfront/security/http/csrf/csrf-handler.ts"; +import { applyCsrfCookie } from "#veryfront/security/csrf/helpers.ts"; +import type { HandlerContext } from "#veryfront/types"; +import { useAttachments, type UseAttachmentsResult } from "./use-uploads-registry.ts"; +import { useUpload, type UseUploadResult } from "./use-upload.ts"; + +const ORIGIN = "https://acme.veryfront.com"; + +function installDom(): () => void { + const dom = new JSDOM('
', { + url: `${ORIGIN}/`, + }); + const window = dom.window; + const keys = [ + "window", + "document", + "navigator", + "self", + "Node", + "Element", + "HTMLElement", + "localStorage", + "File", + "FormData", + ] as const; + const previous = new Map(); + for (const key of keys) { + previous.set(key, Object.getOwnPropertyDescriptor(globalThis, key)); + Object.defineProperty(globalThis, key, { + configurable: true, + enumerable: true, + value: (window as unknown as Record)[key], + writable: true, + }); + } + window.localStorage.clear(); + return () => { + for (const key of keys) { + const descriptor = previous.get(key); + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else delete (globalThis as unknown as Record)[key]; + } + dom.window.close(); + }; +} + +function csrfCtx(): HandlerContext { + return { + projectDir: "/tmp/test", + adapter: { env: { get: () => undefined } } as unknown as HandlerContext["adapter"], + securityConfig: { csrf: true }, + } as HandlerContext; +} + +/** + * The document load that hands the browser its readable CSRF cookie. This runs + * the real `applyCsrfCookie`, so the cookie the client reads here is the one + * production actually issues — and if anyone ever flips that cookie to + * HttpOnly, this throws instead of the fix silently becoming a no-op. + */ +function loadDocumentCookie(): void { + const headers = new Headers(); + applyCsrfCookie(new Request(`${ORIGIN}/`, { headers: { accept: "text/html" } }), headers, true); + const setCookie = headers.get("set-cookie") ?? ""; + if (/httponly/i.test(setCookie)) { + throw new Error(`CSRF cookie is HttpOnly, client JS cannot read it: ${setCookie}`); + } + // jsdom enforces the `__Host-` prefix rules (Secure + Path=/ + no Domain) + // exactly as a browser does, so the full attribute list matters. + document.cookie = setCookie; +} + +/** + * Stand in for the browser + server edge: attaches the document's cookies the + * way a same-origin `fetch` would, then runs the request through the real CSRF + * handler before the upload endpoint would ever see it. + */ +function installCsrfEdge( + endpoint: (req: Request) => Response, +): { restore: () => void; statuses: Map } { + const originalFetch = globalThis.fetch; + const statuses = new Map(); + const handler = new CsrfHandler(); + + globalThis.fetch = async (input, init) => { + const url = new URL(String(input), document.baseURI); + const headers = new Headers(init?.headers); + if (document.cookie) headers.set("cookie", document.cookie); + const req = new Request(url, { + method: init?.method ?? "GET", + headers, + body: init?.body as BodyInit | undefined, + }); + + const result = await handler.handle(req, csrfCtx()); + const response = result.response ?? endpoint(req); + statuses.set(`${req.method} ${url.pathname}`, response.status); + return response; + }; + + return { restore: () => (globalThis.fetch = originalFetch), statuses }; +} + +/** + * The same edge, for the `XMLHttpRequest` transport `useUpload` uses. Whatever + * headers the hook sets are replayed into a real `Request` and run through the + * real CSRF handler, so a missing token shows up as an actual 403 status on the + * XHR rather than as a header assertion. + */ +function installXhrCsrfEdge(): { + restore: () => void; + statuses: Map; + tokens: Map; +} { + const originalXhr = globalThis.XMLHttpRequest; + const statuses = new Map(); + const tokens = new Map(); + const handler = new CsrfHandler(); + + class EdgeXhr { + #method = "GET"; + #url = ""; + #headers = new Headers(); + status = 0; + responseText = ""; + responseURL = ""; + upload: { onprogress: (() => void) | null } = { onprogress: null }; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + onabort: (() => void) | null = null; + + open(method: string, url: string): void { + this.#method = method; + this.#url = url; + } + setRequestHeader(key: string, value: string): void { + this.#headers.set(key, value); + } + abort(): void {} + send(body?: BodyInit): void { + void (async () => { + const url = new URL(this.#url, document.baseURI); + const headers = new Headers(this.#headers); + tokens.set(`${this.#method} ${url.pathname}`, headers.get("x-csrf-token")); + if (document.cookie) headers.set("cookie", document.cookie); + const req = new Request(url, { method: this.#method, headers, body }); + + const result = await handler.handle(req, csrfCtx()); + this.status = result.response?.status ?? 200; + this.responseURL = url.href; + this.responseText = result.response + ? await result.response.text() + : JSON.stringify({ id: "up-1", url: "/files/a.txt" }); + statuses.set(`${this.#method} ${url.pathname}`, this.status); + this.onload?.(); + })(); + } + } + + globalThis.XMLHttpRequest = EdgeXhr as unknown as typeof XMLHttpRequest; + return { restore: () => (globalThis.XMLHttpRequest = originalXhr), statuses, tokens }; +} + +function renderUpload( + options: Parameters[0], +): { upload: () => UseUploadResult; unmount: () => void } { + let latest: UseUploadResult | null = null; + function Capture(): null { + latest = useUpload(options); + return null; + } + const root = createRoot(document.getElementById("root")!); + flushSync(() => root.render()); + return { upload: () => latest!, unmount: () => unmountReactRoot(root) }; +} + +function renderAttachments( + url: string, +): { attachments: () => UseAttachmentsResult; unmount: () => void } { + let latest: UseAttachmentsResult | null = null; + function Capture(): null { + latest = useAttachments({ url }); + return null; + } + const root = createRoot(document.getElementById("root")!); + flushSync(() => root.render()); + return { + attachments: () => latest!, + unmount: () => unmountReactRoot(root), + }; +} + +describe("chat attachment CSRF", () => { + // `` wires exactly this hook, so this is the transport the + // advertised chat-with-attachments flow actually 403s on. + it("sends the double-submit token on a upload", async () => { + const restoreDom = installDom(); + loadDocumentCookie(); + const edge = installXhrCsrfEdge(); + const view = renderUpload({ api: "/api/uploads" }); + try { + view.upload().upload([new File(["a"], "a.txt", { type: "text/plain" })]); + await waitFor(() => edge.statuses.has("POST /api/uploads")); + + assertEquals(edge.statuses.get("POST /api/uploads"), 200); + } finally { + view.unmount(); + edge.restore(); + restoreDom(); + } + }); + + it("does not leak the page CSRF token from cross-origin", async () => { + const restoreDom = installDom(); + loadDocumentCookie(); + const edge = installXhrCsrfEdge(); + const view = renderUpload({ api: "https://uploads.example.net/api/uploads" }); + try { + view.upload().upload([new File(["a"], "a.txt", { type: "text/plain" })]); + await waitFor(() => edge.tokens.size > 0); + + assertEquals(edge.tokens.get("POST /api/uploads"), null); + } finally { + view.unmount(); + edge.restore(); + restoreDom(); + } + }); + + it("sends the double-submit token on an attachment upload", async () => { + const restoreDom = installDom(); + loadDocumentCookie(); + const edge = installCsrfEdge((req) => + req.method === "GET" + ? Response.json({ items: [] }) + : Response.json({ id: "up-1", name: "a.txt", url: "/files/a.txt", size: 1 }) + ); + const view = renderAttachments("/api/uploads"); + try { + view.attachments().upload([new File(["a"], "a.txt", { type: "text/plain" })]); + await waitFor(() => edge.statuses.has("POST /api/uploads")); + + assertEquals(edge.statuses.get("POST /api/uploads"), 200); + } finally { + view.unmount(); + edge.restore(); + restoreDom(); + } + }); + + it("sends the double-submit token on an attachment removal", async () => { + const restoreDom = installDom(); + loadDocumentCookie(); + const edge = installCsrfEdge((req) => + req.method === "GET" + ? Response.json({ + items: [{ id: "up-1", name: "a.txt", url: "/files/a.txt", size: 1 }], + }) + : new Response(null, { status: 204 }) + ); + const view = renderAttachments("/api/uploads"); + try { + await view.attachments().remove("up-1"); + await waitFor(() => edge.statuses.has("DELETE /api/uploads")); + + assertEquals(edge.statuses.get("DELETE /api/uploads"), 204); + } finally { + view.unmount(); + edge.restore(); + restoreDom(); + } + }); + + it("does not leak the page CSRF token to a cross-origin upload endpoint", async () => { + const restoreDom = installDom(); + loadDocumentCookie(); + const originalFetch = globalThis.fetch; + let sentToken: string | null | undefined; + + globalThis.fetch = (input, init) => { + const method = init?.method ?? "GET"; + if (method === "POST") sentToken = new Headers(init?.headers).get("x-csrf-token"); + return Promise.resolve( + method === "GET" + ? Response.json({ items: [] }) + : Response.json({ id: "up-1", name: "a.txt", url: `${String(input)}/a.txt`, size: 1 }), + ); + }; + const view = renderAttachments("https://uploads.example.net/api/uploads"); + try { + view.attachments().upload([new File(["a"], "a.txt", { type: "text/plain" })]); + await waitFor(() => sentToken !== undefined); + + assertEquals(sentToken, null); + } finally { + view.unmount(); + globalThis.fetch = originalFetch; + restoreDom(); + } + }); +}); diff --git a/src/react/components/chat/chat/hooks/use-upload.ts b/src/react/components/chat/chat/hooks/use-upload.ts index b2efb66627..723b2a6e9c 100644 --- a/src/react/components/chat/chat/hooks/use-upload.ts +++ b/src/react/components/chat/chat/hooks/use-upload.ts @@ -13,6 +13,7 @@ * @module react/components/chat/hooks/use-upload */ import * as React from "react"; +import { csrfMutationHeaders } from "#veryfront/security/csrf/browser-mutation-headers.ts"; import { scopeCommitRequiresRenderPublication, useScopeCommitEffect, @@ -332,7 +333,11 @@ export function useUpload( const xhr = new XMLHttpRequest(); operation.xhr = xhr; xhr.open("POST", api); - for (const [key, value] of headerEntries) { + // A production build turns `security.csrf` on by default, so this POST + // has to echo the `__Host-vf_csrf` cookie back or the server answers + // 403 — dev, where CSRF is off, would never show it. The helper keeps + // any caller-supplied header and skips cross-origin endpoints. + for (const [key, value] of csrfMutationHeaders(api, [...headerEntries])) { xhr.setRequestHeader(key, value); } xhr.upload.onprogress = (event) => { diff --git a/src/react/components/chat/chat/hooks/use-uploads-registry.ts b/src/react/components/chat/chat/hooks/use-uploads-registry.ts index 6bcd88cd42..c720392d90 100644 --- a/src/react/components/chat/chat/hooks/use-uploads-registry.ts +++ b/src/react/components/chat/chat/hooks/use-uploads-registry.ts @@ -23,6 +23,7 @@ * @module react/components/chat/hooks/use-uploads-registry */ import * as React from "react"; +import { csrfMutationHeaders } from "#veryfront/security/csrf/browser-mutation-headers.ts"; import { isBrowserEnvironment } from "#veryfront/platform/compat/runtime.ts"; import type { UploadedFile } from "../components/attachments-panel.tsx"; import { isSafeUploadId, isSafeUploadUrl } from "../upload-url.ts"; @@ -506,7 +507,10 @@ export function useAttachments( const response = await fetch(endpoint, { method: "POST", body: form, - headers: headersRef.current, + // A production build turns `security.csrf` on by default, so this + // POST has to echo the `__Host-vf_csrf` cookie back or the server + // answers 403 — dev, where CSRF is off, would never show it. + headers: csrfMutationHeaders(endpoint, headersRef.current), signal: controller.signal, }); if (!response.ok) throw new Error(`Upload failed: ${response.status}`); @@ -549,9 +553,12 @@ export function useAttachments( activeRemovalsRef.current.set(id, active); controllersRef.current.add(controller); try { - const response = await fetch(setQueryParameter(endpoint, "id", id), { + const target = setQueryParameter(endpoint, "id", id); + const response = await fetch(target, { method: "DELETE", - headers: headersRef.current, + // Same CSRF requirement as the upload POST above. The token is keyed + // off the request target, so pass the `?id=` URL actually being hit. + headers: csrfMutationHeaders(target, headersRef.current), signal: controller.signal, }); if (!response.ok) { diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 9067207e49..1eae26e10b 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -928,6 +928,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "4.9107", "4.9807", "400", + "403", "40403F", "40;", "4122", @@ -1838,6 +1839,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "COPY_ICON_ELEMENTS);", "COPY_ICON_ELEMENTS:", "CSP", + "CSRF", "CSS", "CSS-only", "CSS-source", @@ -3291,6 +3293,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Hook", "Hooks", "Horizontal", + "Host-vf_csrf", "Hover", "How", "Human", @@ -6291,6 +6294,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "[...dirty])", "[...event.currentTarget.querySelectorAll(", "[...event.key].length", + "[...headerEntries]))", "[...ids].join(", "[...incoming];", "[...items.entries()];", @@ -6674,6 +6678,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "about", "above", "above).", + "above.", "absence", "absent", "absent.", @@ -7090,6 +7095,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "another", "answer", "answerParts", + "answers", "anthropic", "anthropic:", "any", @@ -9162,6 +9168,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "conversations?.save;", "conversations?:", "converts", + "cookie", "coordinate", "coordinate.", "coordinates", @@ -9293,6 +9300,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "cross-context", "cross-conversation", "cross-namespace", + "cross-origin", "crosses", "crypto.getRandomValues()", "crypto.randomUUID()", @@ -9302,6 +9310,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "cryptoProvider?.getRandomValues", "cryptoProvider?.randomUUID", "csp-nonce.ts", + "csrfMutationHeaders", + "csrfMutationHeaders(api,", + "csrfMutationHeaders(endpoint,", + "csrfMutationHeaders(target,", "csv:", "ctx", "ctx)}", @@ -9942,6 +9954,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "determinate", "deterministic", "deterministic.", + "dev,", "dev;", "development", "device).", @@ -10266,6 +10279,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ease-out", "ease-out,", "easing", + "echo", "edge", "edge,", "edge-fade", @@ -10441,6 +10455,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "endpoint/header/cache", "endpoint:", "endpoint;", + "endpoints.", "ends", "ends.", "engine", @@ -10842,7 +10857,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "fetch", "fetch(endpoint,", "fetch(requestUrl,", - "fetch(setQueryParameter(endpoint,", + "fetch(target,", "fetched", "fetched.", "fetches", @@ -11642,7 +11657,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "header:", "header;", "headerEntries", - "headerEntries)", "headerEntries,", "headers", "headers.", @@ -11654,6 +11668,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "headersKey],", "headersRef", "headersRef.current", + "headersRef.current),", "headersRef.current,", "header}", "heading", @@ -11715,6 +11730,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "history.", "hit", "hit)", + "hit.", "hit;", "hne", "hne,", @@ -11845,6 +11861,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "id.length", "id:", "id;", + "id=", "id={fallbackHeadingId}>", "id={fieldId}", "id={headingId}", @@ -14008,6 +14025,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "occurred", "of", "off", + "off,", "off-by-one", "off-canvas", "off-screen.", @@ -16340,6 +16358,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "requireTransactionReservation(storage:", "required", "required);", + "requirement", "requires", "requiring", "rerenders.", @@ -16886,6 +16905,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "sections?:", "sections]);", "secure", + "security.csrf", "see", "seed", "seed)", @@ -18275,6 +18295,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "taller", "target", "target)", + "target,", "target.", "target.closest(", "target.getAttribute(", @@ -19820,6 +19841,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "veryfront/react/primitives/input-box.tsx", "veryfront/react/server-render-context.ts", "veryfront/security/client/html-sanitizer.ts", + "veryfront/security/csrf/browser-mutation-headers.ts", "veryfront/ui", "veryfront/ui/adapter", "veryfront/utils/clsx.ts",