feat: add the webhooks namespace and signature verification - #15
Merged
Conversation
Second step of the SDK expansion: cover the webhooks API, which was already complete server-side and had no client. Each SDK gains a `webhooks` namespace over the ten endpoints under /v1/projects/:projectId/webhooks — list, get, create, update, delete, resume, rotate-secret, test, list deliveries, and replay a delivery. The other half is receiving. Every SDK can now verify the HMAC signature the platform sends, against the same scheme the platform signs with: hmac-sha256 over "<timestamp>.<raw body>", a 300s replay window, and a constant-time digest comparison. Two entry points: a boolean `verify` for callers that only need a yes/no, and a `constructEvent` that raises WebhookSignatureError and returns the parsed envelope, so an unverified payload cannot be read by accident. Verification deliberately takes the raw body rather than a parsed object, since key order and whitespace are part of what was signed.
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.
Step 2 of the SDK expansion. The webhooks API was already complete server-side and had no client at all, so unlike the deploy work further down the roadmap this needed no platform changes to become usable.
Sending: the
webhooksnamespaceAll ten endpoints under
/v1/projects/:projectId/webhooks, in JavaScript, Python (sync and async), and PHP:list/getgetcreateupdate/deleteresumerotateSecrettestlistDeliveriesreplayDeliveryArgument validation happens client-side before any request, and
projectIdcan be passed per call to work across projects with one client, matching the existing email namespace.Receiving: signature verification
Ported from
packages/webhooks/src/signature.tsso the SDKs verify exactly what the platform signs — hmac-sha256 over`<timestamp>.<raw body>`, a 300s replay window, constant-time digest comparison.Two entry points per language:
verifyreturns a plain boolean and never throws, for callers that only need a yes/no.constructEventraisesWebhookSignatureErrorand returns the parsed{id, type, createdAt, data}envelope. There is deliberately no way to get the payload out of it without the signature holding first, so an unverified body cannot be read by accident.Both take the raw request body rather than a parsed object — key order and whitespace are part of what was signed, so a re-serialized dict will not verify. This is called out in the docstrings because it is the most likely way to get verification subtly wrong.
Verification is a pure function with no client, credentials, or network involved, so a receiver can call it without constructing a Cosmoner instance.
Notes on decisions
x-cosmoner-signature, which depends on datablock-dev/cosmoner#352 renaming them fromx-datablock-*. That PR should merge first; the names are otherwise frozen the moment this SDK documents them.node:crypto, which meant adding@types/nodeand atypesentry to the tsconfig. This keepsverifysynchronous. Web Crypto would work in edge runtimes but is async-only, which would have forced a promise-returning verify — worth revisiting if edge receivers become a real use case.updateneeds to distinguish "leave alone" from "set to null", so Python and PHP use a sentinel default. In PHP this means callers must use named arguments to skipdescription; positional use would clear it. The tests cover both paths.json_encode([])emits[], not{}, which the API rejects where it expects an object.testandupdatesend no body at all rather than an empty array.Tests
320 tests across the three SDKs, all green, with static analysis clean in each:
tsc --noEmitandoxlintcleanmypyandruffcleanphpstanandphpcscleanVerification coverage is deliberately adversarial in all three: tampered bodies, wrong secrets, stale and future timestamps, the tolerance boundary, malformed headers, and a short digest that would otherwise make a constant-time comparison throw on a length mismatch.
mypy caught a genuine bug during the Python port — the
list()method shadows the builtinlistin class scope, soevents: list[str]was resolving to the method rather than the type. Input sequences are now typedSequence[str].