diff --git a/lib/networking/__tests__/getCorsHeaders.test.ts b/lib/networking/__tests__/getCorsHeaders.test.ts new file mode 100644 index 00000000..e729bf1c --- /dev/null +++ b/lib/networking/__tests__/getCorsHeaders.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; + +describe("getCorsHeaders", () => { + it("keeps the existing allow-* headers", () => { + const h = getCorsHeaders(); + expect(h["Access-Control-Allow-Origin"]).toBe("*"); + expect(h["Access-Control-Allow-Methods"]).toContain("GET"); + expect(h["Access-Control-Allow-Headers"]).toContain("x-api-key"); + }); + + // Browsers hide every non-safelisted response header from cross-origin JS + // unless it is named here. `x-workflow-run-id` has been documented as part + // of the 200 on the chat endpoints since the workflow cutover and has never + // been readable by chat.recoupable.dev; a live read of + // `x-workflow-stream-tail-index` returned null for the same reason + // (chat#1923). + it("exposes the workflow stream headers to cross-origin JS", () => { + const exposed = getCorsHeaders()["Access-Control-Expose-Headers"]; + expect(exposed).toBeDefined(); + expect(exposed).toContain("x-workflow-run-id"); + expect(exposed).toContain("x-workflow-stream-tail-index"); + }); +}); diff --git a/lib/networking/getCorsHeaders.ts b/lib/networking/getCorsHeaders.ts index 233b32df..517a4be6 100644 --- a/lib/networking/getCorsHeaders.ts +++ b/lib/networking/getCorsHeaders.ts @@ -8,5 +8,13 @@ export function getCorsHeaders(): Record { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH", "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, x-api-key", + // Browsers hide every non-safelisted response header from cross-origin JS + // unless it is named here. Without this `x-workflow-run-id` — documented + // as part of the 200 on the chat endpoints since the workflow cutover — + // has never been readable by chat.recoupable.dev, and a live read of + // `x-workflow-stream-tail-index` returns null (chat#1923). The AI SDK's + // WorkflowChatTransport reads the latter to anchor relative resume + // positions, so it would fail silently against us today. + "Access-Control-Expose-Headers": "x-workflow-run-id, x-workflow-stream-tail-index", }; }