Repository navigation
fix(#1195): CORS allows the Idempotency-Key that create-order requires - #1196
Merged
Merged
Conversation
/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>
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1195
/checkouthas never completed a purchase from a browser. Your first real attempt aftergo-live died on
Failed to fetch.create-orderrequires a header its own CORS policy forbade:_shared/cors.tsauthorization, content-type, x-client-info, apikeycreate-order/index.ts:8Idempotency-Keyas part of its contractcreate-order/resolve.tscheckout/page.tsx:221,offline-queue/payment-adapter.ts:200A browser asks permission first, sees an
Access-Control-Allow-Headerswithoutidempotency-key, and refuses to send the POST.fetch()rejects withTypeError: 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:
ordersholds 3 rows, all 2026-08-07, all the e2e user — beforeccd75f34 feat(#558): /checkoutintroduced the header. Nothing has been ordered through the pagesince it shipped.
Guarded by class, not instance
scripts/__tests__/cors-allows-what-clients-send.test.jsscans client source for headers sent to/functions/v1/and asserts each is in the allow-list, under the requiredTest (20.x)check. Aruntime 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 theheaders: { … }match early, and substituting${…}with the letterXturned`${a}:${b}`into`X:X`soX:matched as a headername. The anti-vacuity assertion caught the first; the second surfaced as a bogus failure. Both are
recorded in the file.
Verification
test:scripts,deno checkclean.idempotency-keyfrom the allow-list reddens 2 tests.🤖 Generated with Claude Code