Repository navigation
fix(#1229): the live webhook verifies with one secret, chosen by deployment - #1230
Merged
Merged
Conversation
…oyment The function built a LIST of both signing secrets and tried each in turn. Both are configured in production and BOTH Stripe endpoints point at the same function URL: live we_1U5ZAe... signed with STRIPE_WEBHOOK_SECRET_LIVE test we_1U1geo... signed with STRIPE_WEBHOOK_SECRET So an event signed with the TEST secret verified against the LIVE deployment, and the handler never learned which secret matched. A fabricated payment_intent.succeeded would be processed against real rows — an order marked paid with no card and no money. A test-mode signing secret is ordinarily treated as low-sensitivity: shared with development, pasted into local .env files, printed in transcripts. That assumption is exactly what this shape broke. It broke concretely on 2026-09-23, when this session printed the value into an agent transcript; the owner is rotating it, but rotation replaces one value and does not fix a shape. THE RULE. A deployment that HAS a live secret IS the live deployment and accepts only live signatures; anything else accepts only test signatures. Exactly one secret is eligible, decided before any signature is examined. That makes a test-endpoint secret inert against production by construction rather than by remembering to keep it secret. The livemode assertion is kept even though the selection already picks one secret, because the two answer different questions — which key signed this, and which mode Stripe sent it from — and #1180 is the standing example of those disagreeing for 31 days while everything looked configured. It runs BEFORE the Supabase client is constructed, so a mismatch cannot reach a row. NOT fixed by deleting STRIPE_WEBHOOK_SECRET from Supabase. That works today and silently removes test-webhook handling from any environment sharing those secrets, with nothing recording why. The code refuses it; the value's existence stays a deployment concern. The decision lives in a dependency-free resolve.ts because nothing in CI executes an Edge Function — tsconfig excludes supabase/, Vitest excludes supabase/functions/**, and edge-functions.yml is not required (#1153; it cannot even run here today, failing on @types/node). tests/unit/ imports it directly, the create-order pattern. Verified by mutation, each red on its own: restore the defect (prefer test over live) -> 2 fail accept the `no-secret-provided` literal (#562) -> 1 fail livemode by truthiness instead of typeof -> 1 fail Baseline and restored 11/11. type-check clean, 921/921 unit, 1039/1039 script. `deno check` passes on resolve.ts. KNOWN CONSEQUENCE: once deployed, the test-mode endpoint stops working against production — deliveries to we_1U1geo... will 400. That is correct for a live store and is the point of the change, but it is a capability removed. Closes #1229 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #1229.
A test-mode signature verified against the live store
The function built a list of both signing secrets and tried each in turn. Both are
configured in production, and both Stripe endpoints point at the same function URL:
we_1U5ZAe…STRIPE_WEBHOOK_SECRET_LIVEwe_1U1geo…STRIPE_WEBHOOK_SECRETSo an event signed with the test secret verified against the live deployment, and the
handler never learned which secret matched. A fabricated
payment_intent.succeededwould beprocessed against real rows — an order marked paid, with no card and no money.
A test-mode signing secret is ordinarily treated as low-sensitivity: shared with development,
pasted into local
.envfiles, printed in transcripts. That assumption is what this shapebroke. It broke concretely on 2026-09-23 when this session printed the value into an agent
transcript. The owner is rotating it — but rotation replaces one value and does not fix a
shape, which is why this exists.
The rule
A deployment that has a live secret is the live deployment and accepts only live
signatures. Anything else accepts only test signatures. Exactly one secret is eligible,
decided before any signature is examined.
That makes a test-endpoint secret inert against production by construction, rather than by
remembering to keep it secret.
Three judgements worth disagreeing with
The
livemodeassertion is kept even though selection already picks one secret. The twoanswer different questions — which key signed this vs which mode Stripe sent it from — and
#1180 is the standing example of those disagreeing for 31 days while everything looked
configured. It runs before the Supabase client is constructed, so a mismatch cannot reach a
row even in principle.
Not fixed by deleting
STRIPE_WEBHOOK_SECRETfrom Supabase. That works today, and silentlyremoves test-webhook handling from any environment sharing those secrets with nothing recording
why. The code refuses it; the value's existence stays a deployment concern.
no-secret-providedis still rejected by name (#562). An endpoint configured without asecret sends the header with exactly that value, so treating it as usable would be a second
route to verifying unsigned traffic.
Where the coverage lives, and why
Nothing in CI executes an Edge Function:
tsconfigexcludessupabase/, Vitest excludessupabase/functions/**from discovery, andedge-functions.ymlis deliberately not requiredbecause it resolves over the network (#1153 — it cannot run here at all today, failing on
@types/node). So the decision is in a dependency-freeresolve.tsandtests/unit/importsit directly, the
create-orderpattern.deno checkpasses onresolve.ts.Verification
no-secret-providedliterallivemodeby truthiness instead oftypeofBaseline and restored 11/11.
type-checkclean, 921/921 unit, 1039/1039 script.Known consequence
Once deployed, the test-mode endpoint stops working against production — deliveries to
we_1U1geo…will 400. That is correct for a live store and is the point of the change, but itis a capability removed, so it is stated rather than discovered.
Merging does not close the hole. Edge functions deploy separately; a merged function is not
a deployed one.
🤖 Generated with Claude Code