feat(webhooks): verifyAndParse* API for compressed payloads (CHA-3071)#1735
feat(webhooks): verifyAndParse* API for compressed payloads (CHA-3071)#1735nijeesh-stream wants to merge 6 commits into
Conversation
…HA-3071) Adds two new helpers on `StreamChat` so customers can decode + verify gzip-compressed and base64-wrapped webhook payloads in one call: - `decompressWebhookBody(rawBody, contentEncoding?, payloadEncoding?)` - `verifyAndDecodeWebhook(rawBody, xSignature, contentEncoding?, payloadEncoding?)` Both helpers are no-ops when the encoding arguments are null / undefined / empty, so existing HTTP webhook integrations behave identically. The HMAC signature is always checked against the innermost (uncompressed, base64-decoded) JSON, so the same call works for HTTP webhooks and for SQS / SNS firehose envelopes (pass `payloadEncoding: 'base64'`). A new `WebhookSignatureError` class is exported from the package barrel for typed error handling. The legacy `verifyWebhook` and the underlying `CheckSignature` helper are intentionally untouched for backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Size Change: +3.99 kB (+1.05%) Total Size: 385 kB 📦 View Changed
|
Replaces verifyAndDecodeWebhook / decompressWebhookBody with the cross-SDK contract documented at https://getstream.io/chat/docs/node/webhooks_overview/. Module-level helpers in src/signing: Primitives: ungzipPayload - gzip magic-byte detection + inflate decodeSqsPayload - base64 then ungzip-if-magic decodeSnsPayload - alias for decodeSqsPayload verifySignature - constant-time HMAC-SHA256 comparison (parameter order matches the cross-SDK spec) parseEvent - JSON -> typed Event Composite (return Event): verifyAndParseWebhook verifyAndParseSqs verifyAndParseSns The composite functions auto-detect compression from body bytes, so the same handler stays correct whether or not Stream is currently compressing payloads, and behind middleware that auto-decompresses. Client instance methods (StreamChat.verifyAndParse*) mirror the three composite helpers and pull the API secret from the client. Backward compatibility: * StreamChat.verifyWebhook(body, signature) -> bool kept unchanged (now delegates to verifySignature internally). * CheckSignature(body, secret, signature) preserved as a deprecated alias for verifySignature(body, signature, secret). WebhookSignatureError stays the sentinel error for HMAC mismatch and malformed gzip / base64 envelopes. Co-authored-by: Cursor <cursoragent@cursor.com>
RFC 1952 defines the gzip magic number as the two-byte sequence 1F 8B; the third byte (CM) is informational and not part of the identifier. Trim the magic check from three bytes to two to match the spec and stay consistent with the reference implementations in the public docs. Co-authored-by: Cursor <cursoragent@cursor.com>
…-3071) The previous webhooks doc described a verifyAndDecodeWebhook / decompressWebhookBody surface with contentEncoding / payloadEncoding arguments that was never implemented. The shipped API exposes verifyAndParseWebhook / verifyAndParseSqs / verifyAndParseSns (and the matching module-level functions) with magic-byte gzip detection and no encoding arguments. Rewrite the guide and reference table to match what actually exists, including SQS / SNS examples, the lower-level ungzipPayload / decodeSqsPayload / verifySignature helpers, and a note that the helpers detect gzip from the body bytes rather than the Content-Encoding header. Co-authored-by: Cursor <cursoragent@cursor.com>
mogita
left a comment
There was a problem hiding this comment.
Cross-SDK review pass for CHA-3071. Two inline comments — see below.
decodeSnsPayload now JSON-parses the SNS HTTP notification envelope
({"Type":"Notification","Message":"..."}) and extracts the inner
Message field before running the SQS pipeline. Falls through to the
pre-extracted Message string when the input is not a JSON envelope so
existing call sites keep working.
Test fixture adds a realistic SNS HTTP notification body and exercises
both the new envelope path and the existing pre-extracted Message path.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Cross-SDK coordination: unifying webhook exception types After the review pass across all 6 SDKs in this rollout and team discussion, we're consolidating the new webhook exception strategy to a single unified exception class rather than the split (signature vs parse) being introduced in this PR. The Webhook Handling Spec on Notion (CHA-2961) has been revised to reflect this — §5.2 / §5.3 / §7 now specify a single class. Why unified: From a customer's perspective, all failure modes — signature mismatch, gzip decompression failure, base64 decode failure, SNS envelope failure, JSON parse failure, missing schema field — terminate at the same Class name family: Per-SDK naming across the rollout:
Asks for this PR:
This same comment is being posted on all 6 SDK PRs (JS / Go / Ruby / PHP / Java / .NET) for coordination. Happy to discuss naming or scope tradeoffs. |
…n fixtures (CHA-3071) Per Tommaso's suggestion, align the gzip helper with the GNU `gunzip` command name. The function was added in this PR and not yet released, so this is a straight rename with no back-compat alias. Adds Tommaso's reference fixtures to the test suite as named cases so future SDKs can sanity-check against the same payloads: aGVsbG93b3JsZA== -> helloworld (base64) H4sIAGrYAWoAA8tIzcnJL88vykkBAK0g6/kKAAAA -> helloworld (base64+gzip) Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Adds first-class support for gzip-compressed webhook payloads (HTTP webhooks, SQS, SNS) and exposes a stable
verifyAndParse*API that mirrors the cross-SDK contract published in Webhooks Overview.New public API (
src/signing.ts)Primitives:
gunzipPayload(body)— gzip-magic-byte detection, no-op when not compresseddecodeSqsPayload(body)— strict base64 decode then gunzip-if-magicdecodeSnsPayload(notificationBody)— JSON-parse the SNS HTTP notification envelope, extract the innerMessage, then run the SQS pipeline. Falls through to a pre-extractedMessagestring when the input is not a JSON envelopeverifySignature(body, signature, secret)— HMAC-SHA256 over the uncompressed body, with a constant-time comparison (matters for the HTTP webhook path where thex-signatureheader is exposed publicly; SQS / SNS deliveries arrive over AWS-internal transports where timing-attack resistance is not strictly required)parseEvent(payload)— JSON → typedEvent<…>Composites (return a typed
Event<…>):verifyAndParseWebhook(body, signature, secret)verifyAndParseSqs(body, signature, secret)verifyAndParseSns(body, signature, secret)StreamChat#verifyAndParseWebhook/verifyAndParseSqs/verifyAndParseSnsuse the configured client secret automatically.Backwards compatibility
StreamChat#verifyWebhookis preserved and now delegates toverifySignature(same constant-time HMAC-SHA256).CheckSignatureis deprecated and aliasesverifySignature.WebhookSignatureErroris thrown by composites on signature mismatch.Tests
test/unit/webhook-compression.test.tscovers plain / gzip / base64 / base64+gzip payloads, signature mismatches, malformed bytes, and JSON parsing intoEvent<…>. Linked Linear ticket: CHA-3071.Golden test fixtures (Tommaso)
Added shared reference fixtures to the test suite so future SDKs can sanity-check decoders against the same payloads:
Test plan
yarn vitest run test/unit/webhook-compression.test.ts— 35 passedyarn lint— clean on touched filesyarn prettier:check— clean