feat: add raw_json to the verified event and reject unreadable payloads - #20
Closed
razor-x wants to merge 4 commits into
Closed
feat: add raw_json to the verified event and reject unreadable payloads#20razor-x wants to merge 4 commits into
razor-x wants to merge 4 commits into
Conversation
verify() blind-cast whatever svix returned to SeamEvent, so a correctly signed body that is not an event reached the caller mistyped: an object with no event_type, a JSON array, or a bare null all came back as a SeamEvent, and a body that is not JSON leaked svix's internal SyntaxError. These now raise SeamInvalidWebhookPayloadError, kept distinct from SeamWebhookVerificationError because the two call for opposite responses. A verification failure means the sender may not be Seam, so the right answer is an error status that makes it retry. An invalid payload means the sender is Seam and the body will never become readable, so retrying only repeats the failure. This matches what the Python and PHP SDKs already do. The package had one test, for the constructor, and verify() was never exercised at all. It now has 12 covering the real signing path, at 100% coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
The added comments ran to roughly double the density of the code around them and mostly restated what the line below already said. Kept the ones carrying information the code cannot: why svix/util has to be required, why both key shapes are accepted after symbolize_names, and why array_map needed replacing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
The generated SeamEvent types cover only the fields they were generated for, so
a field Seam adds to an existing event between SDK releases is unreachable, and
an event type this version predates is mistyped as one it knows. verify() now
returns the event plus the payload it was parsed from:
JSON.parse(event.raw_json())['a_field_this_version_predates']
This is the true original string, not a re-serialization, since verify()
receives it. It is attached non-enumerably so JSON.stringify(event) does not
embed a second, escaped copy of the payload inside the payload, which is also
covered by a test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
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.
The two rules
1. Runtime does not explode on values the SDK does not recognize. This already held, because the package performs no runtime conversion —
verifyreturns svix's parsed object. An event type this SDK predates comes back intact, with the caveat that it is typed as one of the 108 members it does know. That type-level gap is not addressed here; widening the union is inherently breaking and needs its own decision.2. The webhook event carries the payload it was parsed from.
The generated
SeamEventtypes cover only the fields they were generated for, so a field Seam adds to an existing event is otherwise unreachable, and an event type this version predates is mistyped as one it knows.raw_json()is the escape hatch for both.This is the true original string, not a re-serialization, since
verifyreceives it.It is attached with
Object.definePropertyandenumerable: false. A spread would make it enumerable, andJSON.stringify(event)would then embed a second, escaped copy of the payload inside the payload — doubling its size and looking broken to anyone logging or persisting events that way. A test covers this.Signed but unreadable payloads
verifyblind-cast whatever svix returned:So a correctly signed body that is not an event reached the caller mistyped — an object with no
event_type, a JSON array, and a barenullall came back as aSeamEvent, and a body that is not JSON leaked svix's internalSyntaxError.These now raise
SeamInvalidWebhookPayloadError, kept distinct fromSeamWebhookVerificationErrorbecause the two call for opposite responses. A verification failure means the sender may not be Seam, so the right answer is an error status that makes it retry. An invalid payload means the sender is Seam and the body will never become readable, so retrying only repeats the failure. This matches the Python and PHP SDKs.An unrecognized
event_typeis explicitly not in this category — it passes through, as rule 1 requires.Tests
The package had one test, for the constructor;
verifywas never exercised at all. It now has 15 covering the real HMAC signing path — known and unrecognized events, bad signature, stale timestamp, the unreadable-payload matrix, andraw_jsonround-tripping.100% coverage.
tscand lint clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA