fix: Stripe webhook raw body preservation and async signature verification #75

Closed
pook wants to merge 3 commits from feat/stripe-webhook-raw-body into main
Owner

Summary

  • Fix Bun runtime crash: Switch constructEvent() to constructEventAsync() in the Stripe service — Bun's SubtleCryptoProvider only supports async HMAC, so the synchronous variant throws at runtime
  • Add raw body guard: Add empty-body check and documentation comments on the webhook route to prevent silent failures and future regressions from body-parsing middleware
  • Add signature verification tests: 5 unit tests covering valid signature, wrong secret, tampered payload, missing header, and whitespace-sensitivity (proving why raw body preservation matters)

Changes

File What
packages/api/src/services/stripe.ts constructWebhookEvent → async, uses constructEventAsync
packages/api/src/routes/billing.ts await constructWebhookEvent, empty body guard, docs
packages/api/tests/unit/webhook-signature.test.ts New: 5 signature verification tests

Test plan

  • bun test packages/api/tests/unit/webhook-signature.test.ts — 5/5 pass
  • tsc --noEmit --rootDir . — 0 type errors
  • Manual: send test webhook via Stripe CLI to staging

🤖 Generated with Claude Code

## Summary - **Fix Bun runtime crash**: Switch `constructEvent()` to `constructEventAsync()` in the Stripe service — Bun's SubtleCryptoProvider only supports async HMAC, so the synchronous variant throws at runtime - **Add raw body guard**: Add empty-body check and documentation comments on the webhook route to prevent silent failures and future regressions from body-parsing middleware - **Add signature verification tests**: 5 unit tests covering valid signature, wrong secret, tampered payload, missing header, and whitespace-sensitivity (proving why raw body preservation matters) ## Changes | File | What | |------|------| | `packages/api/src/services/stripe.ts` | `constructWebhookEvent` → async, uses `constructEventAsync` | | `packages/api/src/routes/billing.ts` | `await constructWebhookEvent`, empty body guard, docs | | `packages/api/tests/unit/webhook-signature.test.ts` | New: 5 signature verification tests | ## Test plan - [x] `bun test packages/api/tests/unit/webhook-signature.test.ts` — 5/5 pass - [x] `tsc --noEmit --rootDir .` — 0 type errors - [ ] Manual: send test webhook via Stripe CLI to staging 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Closing as duplicate of #85. This PR switches to constructEventAsync() but does not fix the core raw body issue (still uses c.req.text()). PR #85 correctly uses Buffer.from(await c.req.arrayBuffer()) to preserve raw bytes. See #87 for the canonical issue.

Closing as duplicate of #85. This PR switches to `constructEventAsync()` but does not fix the core raw body issue (still uses `c.req.text()`). PR #85 correctly uses `Buffer.from(await c.req.arrayBuffer())` to preserve raw bytes. See #87 for the canonical issue.
pook closed this pull request 2026-04-08 17:57:42 -04:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pook/compliancebot!75
No description provided.