MT-22022: Add webhook signature verification helper#130
Conversation
📝 WalkthroughWalkthroughThis PR adds webhook signature verification to the Mailtrap SDK. A new ChangesWebhook Signature Verification Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
b8a48e6 to
2bad438
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/webhooks/verify-signature.ts`:
- Around line 50-52: The payload-type check in verifySignature (the function
handling parameters payload, signature, secret) currently allows empty
strings/Buffers; update the guard so that it also rejects empty payloads by
returning false when payload is an empty string (payload.length === 0) or an
empty Buffer (payload.length === 0) in addition to the existing
typeof/Buffer.isBuffer checks; keep the existing type check logic but add the
emptiness condition so empty inputs explicitly return false.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9d01f3a3-563c-4243-8f35-779cff4476d0
📒 Files selected for processing (5)
README.mdexamples/webhooks/verify-signature.tssrc/__tests__/lib/webhooks/verify-signature.test.tssrc/index.tssrc/lib/webhooks/verify-signature.ts
Add `verifyWebhookSignature(payload, signature, signingSecret)` exported
from the package root for verifying Mailtrap webhook signatures using
HMAC-SHA256 over the raw request body with constant-time hex comparison
via `crypto.timingSafeEqual`.
Returns false (no throw) for missing/empty/malformed/wrong-length
signatures so a single guard at the request handler covers every bad-input
case. Accepts `string | Buffer` payloads; signed input must be the raw
body bytes — the README and example warn against `express.json()` and show
the `express.raw({type: 'application/json'})` pattern.
Includes the shared cross-SDK test fixture (payload + secret + expected
signature) that all six Mailtrap SDKs use to stay byte-for-byte
compatible, plus a runnable Express receiver example and README
subsection.
See https://railsware.atlassian.net/browse/MT-22022
2bad438 to
eaab34a
Compare
Co-authored-by: Marcin Klocek <marcin.klocek@railsware.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
examples/webhooks/verify-signature.ts (1)
13-16: ⚡ Quick winKeep payload as Buffer and normalize
mailtrap-signatureheader type.Passing raw
Bufferavoids encoding assumptions, and explicit header normalization handlesstring[]correctly.Suggested patch
- const payload = Buffer.concat(chunks).toString("utf-8"); - const signature = (req.headers["mailtrap-signature"] as string) ?? ""; + const payload = Buffer.concat(chunks); + const rawSignature = req.headers["mailtrap-signature"]; + const signature = Array.isArray(rawSignature) + ? rawSignature[0] ?? "" + : rawSignature ?? "";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/webhooks/verify-signature.ts` around lines 13 - 16, Keep the payload as a Buffer and normalize the "mailtrap-signature" header before calling verifyWebhookSignature: replace payload = Buffer.concat(chunks).toString("utf-8") with the raw Buffer (const payload = Buffer.concat(chunks)) and normalize the header value from req.headers["mailtrap-signature"] (handle string | string[] by using Array.isArray to pick the first element or join them) into a single string (falling back to ""), then pass that normalized signature and the Buffer payload into verifyWebhookSignature(payload, signature, SIGNING_SECRET).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/webhooks/verify-signature.ts`:
- Around line 10-24: Add a streaming size limit to the webhook body collection:
in the req.on("data") handler for the chunks array, track accumulated bytes and
if it exceeds a configurable max (e.g., MAX_PAYLOAD_BYTES) immediately stop
reading, respond with 413 and end the request, and ensure you clean up listeners
/ destroy the socket to avoid continuing to buffer; only call Buffer.concat(...)
and verifyWebhookSignature(payload, signature, SIGNING_SECRET) when under the
limit. Also stop casting the header to string — normalize
req.headers["mailtrap-signature"] (which may be string | string[] | undefined)
into a single string (e.g., take the first element or join with ",") before
passing it to verifyWebhookSignature so you handle all header shapes safely.
---
Nitpick comments:
In `@examples/webhooks/verify-signature.ts`:
- Around line 13-16: Keep the payload as a Buffer and normalize the
"mailtrap-signature" header before calling verifyWebhookSignature: replace
payload = Buffer.concat(chunks).toString("utf-8") with the raw Buffer (const
payload = Buffer.concat(chunks)) and normalize the header value from
req.headers["mailtrap-signature"] (handle string | string[] by using
Array.isArray to pick the first element or join them) into a single string
(falling back to ""), then pass that normalized signature and the Buffer payload
into verifyWebhookSignature(payload, signature, SIGNING_SECRET).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5ae47283-e3f8-43c0-a6e5-7f20368975fe
📒 Files selected for processing (5)
README.mdexamples/webhooks/verify-signature.tssrc/__tests__/lib/webhooks/verify-signature.test.tssrc/index.tssrc/lib/webhooks/verify-signature.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/index.ts
- src/lib/webhooks/verify-signature.ts
| const chunks: Buffer[] = []; | ||
| req.on("data", (chunk: Buffer) => chunks.push(chunk)); | ||
| req.on("end", () => { | ||
| const payload = Buffer.concat(chunks).toString("utf-8"); | ||
| const signature = (req.headers["mailtrap-signature"] as string) ?? ""; | ||
|
|
||
| if (!verifyWebhookSignature(payload, signature, SIGNING_SECRET)) { | ||
| res.writeHead(401, { "Content-Type": "text/plain" }); | ||
| res.end("Invalid signature"); | ||
| return; | ||
| } | ||
|
|
||
| res.writeHead(200, { "Content-Type": "text/plain" }); | ||
| res.end(); | ||
| }); |
There was a problem hiding this comment.
Add a request body size limit to prevent memory DoS.
The example buffers the entire webhook body without bounds; a large request can exhaust memory. Enforce a max byte limit while streaming (e.g., return 413 and stop reading) before Buffer.concat(...) and signature verification.
- Also normalize
req.headers["mailtrap-signature"]to handlestring[] | undefinedinstead of casting tostring.
Suggested patch
+const MAX_BODY_BYTES = 1024 * 1024; // 1MB example limit
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
// Use the raw request body — parsing and re-serializing the JSON may
// reorder keys or alter whitespace and invalidate the signature.
const chunks: Buffer[] = [];
+ let total = 0;
req.on("data", (chunk: Buffer) => chunks.push(chunk));
+ req.on("data", (chunk: Buffer) => {
+ total += chunk.length;
+ if (total > MAX_BODY_BYTES) {
+ res.writeHead(413, { "Content-Type": "text/plain" });
+ res.end("Payload too large");
+ req.destroy();
+ }
+ });
req.on("end", () => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/webhooks/verify-signature.ts` around lines 10 - 24, Add a streaming
size limit to the webhook body collection: in the req.on("data") handler for the
chunks array, track accumulated bytes and if it exceeds a configurable max
(e.g., MAX_PAYLOAD_BYTES) immediately stop reading, respond with 413 and end the
request, and ensure you clean up listeners / destroy the socket to avoid
continuing to buffer; only call Buffer.concat(...) and
verifyWebhookSignature(payload, signature, SIGNING_SECRET) when under the limit.
Also stop casting the header to string — normalize
req.headers["mailtrap-signature"] (which may be string | string[] | undefined)
into a single string (e.g., take the first element or join with ",") before
passing it to verifyWebhookSignature so you handle all header shapes safely.
Motivation
Expose a helper so Node.js users don't have to re-implement Mailtrap's HMAC-SHA256 webhook signature check on every receiver.
Changes
verifyWebhookSignature(payload, signature, signingSecret)exported from the package root. HMAC-SHA256 over the raw body, constant-time compare viacrypto.timingSafeEqual. Acceptspayloadasstring | Buffer. Returnsfalse(never throws) on empty / wrong-length / non-hex / wrong-type inputs.src/__tests__/lib/webhooks/verify-signature.test.tspins the cross-SDK fixture (payload + signing_secret + expected digest) shared verbatim across all six official Mailtrap SDKs to guarantee byte-for-byte parity.examples/webhooks/verify-signature.ts— runnable usage snippet.How to test
CI runs jest, eslint, and tsc. Manually:
The script should exit 0 with no output.
Companion PRs
Coordinated rollout across all six official SDKs (same algorithm, same shared fixture):
Summary by CodeRabbit
New Features
Documentation
Examples
Tests