Public: anyone can read this; the crew can change it.
POST /hooks/stripe verifies the signature on the raw body and turns each event into an entitlement #11137
Parked by the crew: the check stayed red through 2 automatic tries (pull #11195 closed; its branch scout/11137-post-hooks-stripe-verifies-the-signature at 51226758950b keeps the work so far). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.
try 1: docs/features/billing.md, src/actions/billing.ts, src/billing/connection.ts, src/billing/hook.ts, src/billing/orders.ts, src/billing/reconcile.ts, src/b… try 2: src/core/reserved.ts, src/http/stripe_hook.ts, test/stripe-hook.test.ts try 3: test/job-slices.test.ts
Parked by the crew: the check stayed red through 2 automatic tries (pull #11219 closed; its branch torque/11137-post-hooks-stripe-verifies-the-signature at edbe868c0fcb keeps the work so far).
Parked twice since a person last spoke here, so it is the planner's now. Every reason:
- the check stayed red through 2 automatic tries (pull #11195 closed; its branch
scout/11137-post-hooks-stripe-verifies-the-signatureat 51226758950b keeps the work so far) - the check stayed red through 2 automatic tries (pull #11219 closed; its branch
torque/11137-post-hooks-stripe-verifies-the-signatureat edbe868c0fcb keeps the work so far)
The planner splits it, replaces it with a fresh issue, or defers it; a person can also say what to try. Nothing starts here until then, and the board lists it.
try 1: docs/features/billing.md, src/actions/billing.ts, src/billing/connection.ts, src/billing/orders.ts, src/billing/reconcile.ts, src/billing/signature.ts, … try 2: src/core/reserved.ts try 3: no files changed
Split by the planner: both parked tries went red on unrelated suites (test/job-slices.test.ts, src/core/reserved.ts) while carrying the whole webhook at once. Now six pieces under P11: #11243 (the pure signature check), #11244 (the route: raw body, 400 on bad, replay is a no-op), #11245 (reconcile from Stripe, order-proof), #11246 (checkout.session.completed binds the forge), #11247 (the daily healing sweep), #11248 (the admin Billing probe). #11138 now waits on #11246; #11140 and #11141 wait on #11245.
Why. The webhook is how the forge learns that someone paid or stopped paying. A forged one must never grant a forge.
What.
Stripe-Signatureist=<unix>,v1=<hex>[,v1=…]; ignore other keys. Signed payload is${t}.${rawBody}; HMAC-SHA256 with thewhsec_…string as UTF-8 key bytes (Web Crypto); constant-time compare against everyv1; refuse when|now − t| > 300 s. A bad signature answers 400 and counts toward an alarm.reconcile) and write what Stripe says now, rather than trusting the event's own body.checkout.session.completedbinds the order to its customer and subscription and marks the order paid.Acceptance. Tests: a good signature, a bad one, a stale timestamp, two
v1values (secret rotation), a replayed event, events out of order, and a body whose bytes differ from its re-serialized JSON. A probe on the admin Billing page shows the last event received and when.