Skip to content

feat: add the webhooks namespace and signature verification - #15

Merged
pannoury merged 1 commit into
mainfrom
feat/webhooks-namespace
Aug 8, 2026
Merged

feat: add the webhooks namespace and signature verification#15
pannoury merged 1 commit into
mainfrom
feat/webhooks-namespace

Conversation

@pannoury

@pannoury pannoury commented Aug 8, 2026

Copy link
Copy Markdown
Member

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 webhooks namespace

All ten endpoints under /v1/projects/:projectId/webhooks, in JavaScript, Python (sync and async), and PHP:

Method Covers
list / get endpoints, with delivery counts on get
create returns the signing secret — the only call that ever does
update / delete
resume clears an auto-pause and re-queues the held backlog
rotateSecret
test synthetic event, reports what the endpoint answered
listDeliveries cursor-paginated, filterable by status and event type
replayDelivery

Argument validation happens client-side before any request, and projectId can 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.ts so 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:

  • verify returns a plain boolean and never throws, for callers that only need a yes/no.
  • constructEvent raises WebhookSignatureError and 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

  • Header names. Built against x-cosmoner-signature, which depends on datablock-dev/cosmoner#352 renaming them from x-datablock-*. That PR should merge first; the names are otherwise frozen the moment this SDK documents them.
  • Node crypto. JS verification uses node:crypto, which meant adding @types/node and a types entry to the tsconfig. This keeps verify synchronous. 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.
  • Clearing a description. update needs 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 skip description; positional use would clear it. The tests cover both paths.
  • PHP empty bodies. json_encode([]) emits [], not {}, which the API rejects where it expects an object. test and update send no body at all rather than an empty array.

Tests

320 tests across the three SDKs, all green, with static analysis clean in each:

  • JavaScript — 104 tests, tsc --noEmit and oxlint clean
  • Python — 112 tests, mypy and ruff clean
  • PHP — 103 tests, phpstan and phpcs clean

Verification 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 builtin list in class scope, so events: list[str] was resolving to the method rather than the type. Input sequences are now typed Sequence[str].

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.
@pannoury
pannoury merged commit 97783a1 into main Aug 8, 2026
8 checks passed
@pannoury
pannoury deleted the feat/webhooks-namespace branch August 8, 2026 16:20
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.

1 participant