fix(webhooks): export standalone signature verifier and stop it throwing on malformed input - #829
Conversation
verifyWebhookSignature threw TypeError for a non-string secret or payload because the hex guard ran before createHmac()/update() received them, so a webhook route reading an unset env var or an unparsed body got a 500 where the documented contract promises false. Guard argument types before hashing, align the parameter order with the issue and with the isomorphic verifier, and add WebhookVerificationError plus assertWebhookSignature for consumers who prefer exceptions.
Exports the synchronous verifier as verifyWebhookSignatureSync alongside WebhookVerificationError and assertWebhookSignature. The existing async, isomorphic verifyWebhookSignature export from ./webhookMiddleware.js is left in place so no current consumer changes behaviour.
instanceof Uint8Array is realm-bound, so a Buffer created in another realm - a worker thread, a vm context, or a test runner that isolates modules - failed the check and verification returned false for a valid signature. ArrayBuffer.isView tests the internal slot and is realm-safe.
|
Pushed Tests: Typecheck: One fix from that run. The first CI run failed a case that passes locally: I had guarded binary input with |
Closes #617.
Makes the standalone webhook signature verifier reachable from the package
root, adds
WebhookVerificationError, and fixes a crash in the existingverifier.
The bug
src/webhooks/verify.tsdocuments that a malformed signature returnsfalse,and #617 requires it to never throw. It currently throws
TypeErrorfor fourinput classes, because the hex/length guard runs before the arguments ever
reach
createHmac()/.update():This bites exactly where the helper is meant to be used. A webhook route
reading
process.env.WEBHOOK_SECRET(undefined when the var is unset) orreq.body(undefined before a body parser runs) gets an unhandledTypeError— a 500 — where the contract promises
falseand a clean 401.Name collision, and how this PR avoids a breaking change
src/index.tsalready exports averifyWebhookSignature, but a differentone — the isomorphic verifier from
./webhookMiddleware.js:It is
async, accepts an object payload, and runs on WebCrypto with a Nodefallback. The helper in
webhooks/verify.tsis synchronous, string-only andNode-only. Re-exporting the latter under the same name would either be a
duplicate-export error or silently replace the former, which would break two
groups of existing callers: anyone passing a
WebhookPayloadobject (thesync path would throw inside
createHmac().update()), and anyone running in abrowser or edge runtime (the sync path imports Node
crypto).So this PR leaves the existing
verifyWebhookSignatureexport untouched andadds the synchronous helper under a distinct name:
Happy to rename or to consolidate the two verifiers instead if you'd prefer
one entry point — that is an API decision I did not want to make unilaterally
inside a bug fix.
Changes
src/webhooks/verify.tsthrows; accepts
string | Uint8Arrayfor payload and secret(payload, signature, secret), matching boththe issue and the existing isomorphic verifier (this module had no
importers, so the reorder is not observable to any caller)
WebhookVerificationError, withObject.setPrototypeOfsoinstanceofsurvives an ES5 targetassertWebhookSignature, the throwing wrappersrc/index.ts— re-export the three symbols abovetest/webhooks.verify.test.ts— vitest coverageValidation
payload, mismatched length, non-hex, odd-length, empty, uppercase hex, and
Buffer/Uint8Arrayinputs.false.survived: no behavioural divergence. The change only converts crashes
into
false; it does not alter any accept/reject decision.