Skip to content

feat(webhooks): add verifyAndDecodeWebhook for compressed payloads#1735

Open
nijeesh-stream wants to merge 1 commit intomasterfrom
nijeeshjoshy/cha-3071-compress-webhook-payloads
Open

feat(webhooks): add verifyAndDecodeWebhook for compressed payloads#1735
nijeesh-stream wants to merge 1 commit intomasterfrom
nijeeshjoshy/cha-3071-compress-webhook-payloads

Conversation

@nijeesh-stream
Copy link
Copy Markdown
Contributor

Summary

Stream chat backend can now compress outbound webhook payloads with gzip and (for SQS / SNS firehose) base64-wrap them so they stay valid UTF-8 over the queue. This PR adds two new methods on StreamChat so customers can decompress + verify in one call.

Linear: CHA-3071

New public surface (all in src/client.ts / re-exported from the package barrel)

  • decompressWebhookBody(rawBody, contentEncoding?, payloadEncoding?): Buffer — reverses the encoding wrappers Stream applies to outbound webhook / SQS / SNS payloads.
  • verifyAndDecodeWebhook(rawBody, xSignature, contentEncoding?, payloadEncoding?): Buffer — decompresses (when needed) and verifies the HMAC, returning the uncompressed JSON Buffer. Throws a new WebhookSignatureError on mismatch.
  • WebhookSignatureError — typed error so consumers can instanceof-check.

The HMAC is always computed over the innermost (uncompressed, base64-decoded) JSON, so the verification rule is invariant across HTTP webhooks and SQS / SNS.

SQS / SNS support

Pass payloadEncoding: 'base64' (alias 'b64' also accepted) when receiving Stream webhooks via SQS or SNS. The helper base64-decodes, then gunzips, then HMAC-verifies in that order.

Backward compatibility

  • The legacy verifyWebhook and the underlying CheckSignature helper in src/signing.ts are intentionally untouched.
  • null / undefined / '' for either encoding makes decompressWebhookBody and verifyAndDecodeWebhook behave identically to the existing plain-JSON HTTP webhook flow.

Encoding handling

  • Encoding names are matched case-insensitively ('GZIP', 'BASE64', 'b64' all work).
  • Unsupported Content-Encoding (br, brotli, zstd, deflate, compress, lz4) and unsupported payload_encoding (hex, url, binary) throw a clear Error.
  • Malformed base64 (extra characters, missing padding) throws WebhookSignatureError before the body reaches the HMAC step, since silently accepting it would corrupt the signed bytes.
  • Bad gzip bytes throw Error('failed to decompress webhook body: ...').

Build / browser

zlib was added to package.json browser: { ... } so esbuild shims it to an empty module on browser bundles, mirroring the existing crypto: false shim. The new helpers are server-only — they live in src/signing.ts next to the existing CheckSignature.

Files changed

  • src/signing.ts — new helpers, new WebhookSignatureError class.
  • src/client.ts — thin wrappers on StreamChat that delegate to the helpers.
  • package.jsonbrowser.zlib = false shim.
  • docs/webhooks.md (new) — end-to-end Express + SQS / SNS guide.
  • README.md — link to the new doc.
  • test/unit/webhook-compression.test.ts (new) — 31 cases covering passthrough, gzip, base64, base64+gzip, case-insensitive encoding names, every requested unsupported encoding, malformed gzip / base64, signature happy paths, and signature-mismatch rejection (including the "signature was computed over the wrapped bytes" attack).

Test plan

  • yarn lint (prettier + eslint, --max-warnings 0)
  • yarn build (tsc + esbuild for browser-cjs / node-cjs / browser-esm)
  • yarn types (tsc --noEmit)
  • yarn test-unit run — 2831 passed / 1 skipped
  • yarn test-unit run test/unit/webhook-compression.test.ts — 31 / 31 pass
  • Manual smoke test against a Stream app with webhook_compression_algorithm = "gzip" (left for reviewer / staging)

Made with Cursor

…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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Size Change: +4.35 kB (+1.14%)

Total Size: 385 kB

📦 View Changed
Filename Size Change
dist/cjs/index.browser.js 128 kB +1.44 kB (+1.14%)
dist/cjs/index.node.js 129 kB +1.47 kB (+1.15%)
dist/esm/index.mjs 127 kB +1.44 kB (+1.15%)

compressed-size-action

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