Skip to content

feat: Raise a distinct error for an unreadable webhook payload - #19

Merged
razor-x merged 4 commits into
mainfrom
claude/webhook-payload-validation-o2iid9
Aug 29, 2026
Merged

feat: Raise a distinct error for an unreadable webhook payload#19
razor-x merged 4 commits into
mainfrom
claude/webhook-payload-validation-o2iid9

Conversation

@razor-x

@razor-x razor-x commented Aug 28, 2026

Copy link
Copy Markdown
Member

What

verify() returned whatever svix handed back, cast blindly as SeamEvent. Svix runs JSON.parse after the signature matches (standardwebhooks returns JSON.parse(payload.toString()) only once timingSafeEqual passes), so two cases escaped the SDK's error surface:

  • A correctly-signed payload that is not valid JSON threw a raw SyntaxError from inside svix.
  • A correctly-signed payload that parses to a non-event (null, [1], 42, "event", {}) was returned and cast to SeamEvent, so the caller got a TypeError or silent undefined further downstream.

Both conflate with forgery, and the README's catch { return res.status(400) } maps them to a signature failure — so Svix retries a permanently-unreadable payload across its full backoff schedule while operators investigate a phantom forgery.

This adds SeamInvalidWebhookPayloadError, raised only after verification succeeds:

  • Unparseable JSON is converted from svix's SyntaxError, retaining the original as cause.
  • A verified payload without a string event_id and event_type throws the same error.
  • WebhookVerificationError is rethrown untouched — the webhook handler is svix, so a failed signature keeps raising svix's own error (still re-exported as SeamWebhookVerificationError).
  • Ships with an isSeamInvalidWebhookPayloadError type guard, exported from the index, per the convention in the other JS SDKs.

An unknown/newer event_type still parses, so forward compatibility is unaffected.

This brings the JS webhook SDK in line with the Seam SDK audit's webhook finding, already fixed in PHP (InvalidWebhookPayloadError) and in progress for Python (seamapi/python#644).

Testing

The package previously had one test (a constructor smoke test). This adds 15 more, minting real signatures with svix's own Webhook.sign: valid event, mixed-case headers, tampered payload, wrong secret, expired timestamp, each missing svix header, signed-but-unparseable JSON, signed non-events (null, [1], 42, "event", {}), and an unknown event type. Verification failures are asserted not to be payload errors, so the two classes cannot drift back together.

16 tests pass; typecheck, lint, and prettier are clean; 100% statement/function/line coverage on both touched files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y1RzepycXEYA3LStfjt8cY


Generated by Claude Code

Comment thread src/lib/seam-webhook.ts Outdated
Comment thread README.md Outdated
@razor-x
razor-x merged commit 6cccdcc into main Aug 29, 2026
11 checks passed
@razor-x
razor-x deleted the claude/webhook-payload-validation-o2iid9 branch August 29, 2026 03:32
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.

2 participants