Skip to content

fix(#1195): CORS allows the Idempotency-Key that create-order requires - #1196

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/cors-idempotency-key
Sep 18, 2026
Merged

TortoiseWolfe merged 1 commit into
mainfrom
fix/cors-idempotency-key

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Closes #1195

/checkout has never completed a purchase from a browser. Your first real attempt after
go-live died on Failed to fetch.

create-order requires a header its own CORS policy forbade:

_shared/cors.ts allowed authorization, content-type, x-client-info, apikey
create-order/index.ts:8 documents Idempotency-Key as part of its contract
create-order/resolve.ts reads it, enforces replay protection on it
checkout/page.tsx:221, offline-queue/payment-adapter.ts:200 send it

A browser asks permission first, sees an Access-Control-Allow-Headers without
idempotency-key, and refuses to send the POST. fetch() rejects with TypeError: Failed to fetch — no status, no body, nothing in the function logs, because the request never happened.

Why nothing caught it

curl does not enforce CORS. Probed by hand the preflight returns 204 with correct-looking
headers and the POST succeeds. That response is the bug, and it reads as success. It survived a
full adversarial go-live review in which these endpoints were probed directly, because every probe
was made by a client that doesn't check.

It also explains the data: orders holds 3 rows, all 2026-08-07, all the e2e user — before
ccd75f34 feat(#558): /checkout introduced the header. Nothing has been ordered through the page
since it shipped.

Guarded by class, not instance

scripts/__tests__/cors-allows-what-clients-send.test.js scans client source for headers sent to
/functions/v1/ and asserts each is in the allow-list, under the required Test (20.x) check. A
runtime check can't do this — the runtime is the thing that can't see the problem.

Two false readings from writing that guard, both of which made it pass while finding nothing:
Bearer ${token} contains a } that ended the headers: { … } match early, and substituting
${…} with the letter X turned `${a}:${b}` into `X:X` so X: matched as a header
name. The anti-vacuity assertion caught the first; the second surfaced as a bogus failure. Both are
recorded in the file.

Verification

  • 4/4 new, 989/989 test:scripts, deno check clean.
  • Mutation bites: removing idempotency-key from the allow-list reddens 2 tests.
  • After merge the Edge Functions need redeploying — the site build doesn't ship them.

🤖 Generated with Claude Code

/checkout has never completed a purchase from a browser. create-order documents
`Idempotency-Key` in its contract (create-order/index.ts:8), reads it, and enforces replay
protection on it; src/app/checkout/page.tsx:221 and src/lib/offline-queue/payment-adapter.ts:200
both send it — and _shared/cors.ts did not allow it.

A browser asks permission first, sees an Access-Control-Allow-Headers without idempotency-key,
and refuses to send the POST. fetch() rejects with "TypeError: Failed to fetch": no status, no
body, nothing in the function logs, because the request never happened.

CURL DOES NOT ENFORCE CORS, which is why nothing caught this. Probed by hand the preflight
returns 204 with correct-looking headers and the POST succeeds. That response IS the bug and it
reads as success. It survived a full adversarial go-live review in which these endpoints were
probed directly. It also explains the data: orders holds 3 rows, all 2026-08-07, all the e2e
user — before ccd75f3 introduced the header.

Guarded by class, not instance: scripts/__tests__/cors-allows-what-clients-send.test.js scans
client source for headers sent to /functions/v1/ and asserts each is allowed, under the required
Test (20.x) check.

Two false readings from writing that guard, both of which made it pass while finding nothing:
`Bearer ${token}` contains a closing brace that ended the headers-object match early, and
substituting ${...} with the letter X turned `${a}:${b}` into `X:X` so "X:" matched as a
header name. The anti-vacuity assertion caught the first; the second surfaced as a bogus failure.

Verified: 4/4 new, 989/989 test:scripts, deno check clean, and the mutation bites — removing
idempotency-key from the allow-list reddens 2 tests.

Closes #1195
Refs #629, #1185

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TortoiseWolfe
TortoiseWolfe merged commit 0985534 into main Sep 18, 2026
26 checks passed
@TortoiseWolfe
TortoiseWolfe deleted the fix/cors-idempotency-key branch September 18, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/checkout cannot take money from a browser: create-order requires a header its own CORS policy forbids

2 participants