Skip to content

Commit 386e6e0

Browse files
authored
feat: add verify webhooks (#636)
1 parent 9a5dec1 commit 386e6e0

File tree

5 files changed

+116
-0
lines changed

5 files changed

+116
-0
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
"url": "https://github.com/resend/resend-node/issues"
4747
},
4848
"homepage": "https://github.com/resend/resend-node#readme",
49+
"dependencies": {
50+
"svix": "1.76.1"
51+
},
4952
"peerDependencies": {
5053
"@react-email/render": "*"
5154
},

pnpm-lock.yaml

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/resend.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Emails } from './emails/emails';
1313
import type { ErrorResponse } from './interfaces';
1414
import { Templates } from './templates/templates';
1515
import { Topics } from './topics/topics';
16+
import { Webhooks } from './webhooks/webhooks';
1617

1718
const defaultBaseUrl = 'https://api.resend.com';
1819
const defaultUserAgent = `resend-node:${version}`;
@@ -38,6 +39,7 @@ export class Resend {
3839
readonly emails = new Emails(this);
3940
readonly templates = new Templates(this);
4041
readonly topics = new Topics(this);
42+
readonly webhooks = new Webhooks();
4143

4244
constructor(readonly key?: string) {
4345
if (!key) {

src/webhooks/webhooks.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Webhook } from 'svix';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { Webhooks } from './webhooks';
4+
5+
const mocks = vi.hoisted(() => {
6+
const verify = vi.fn();
7+
const webhookConstructor = vi.fn(() => ({
8+
verify,
9+
}));
10+
11+
return {
12+
verify,
13+
webhookConstructor,
14+
};
15+
});
16+
17+
vi.mock('svix', () => ({
18+
Webhook: mocks.webhookConstructor,
19+
}));
20+
21+
describe('Webhooks', () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
mocks.verify.mockReset();
25+
});
26+
27+
it('verifies payload using svix headers', () => {
28+
const options = {
29+
payload: '{"type":"email.sent"}',
30+
headers: {
31+
id: 'msg_123',
32+
timestamp: '1713984875',
33+
signature: 'v1,some-signature',
34+
},
35+
webhookSecret: 'whsec_123',
36+
};
37+
38+
const expectedResult = { id: 'msg_123', status: 'verified' };
39+
mocks.verify.mockReturnValue(expectedResult);
40+
41+
const result = new Webhooks().verify(options);
42+
43+
expect(Webhook).toHaveBeenCalledWith(options.webhookSecret);
44+
expect(mocks.verify).toHaveBeenCalledWith(options.payload, {
45+
'svix-id': options.headers.id,
46+
'svix-timestamp': options.headers.timestamp,
47+
'svix-signature': options.headers.signature,
48+
});
49+
expect(result).toBe(expectedResult);
50+
});
51+
});

src/webhooks/webhooks.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Webhook } from 'svix';
2+
3+
interface Headers {
4+
id: string;
5+
timestamp: string;
6+
signature: string;
7+
}
8+
9+
interface VerifyWebhookOptions {
10+
payload: string;
11+
headers: Headers;
12+
webhookSecret: string;
13+
}
14+
15+
export class Webhooks {
16+
verify(payload: VerifyWebhookOptions) {
17+
const webhook = new Webhook(payload.webhookSecret);
18+
return webhook.verify(payload.payload, {
19+
'svix-id': payload.headers.id,
20+
'svix-timestamp': payload.headers.timestamp,
21+
'svix-signature': payload.headers.signature,
22+
});
23+
}
24+
}

0 commit comments

Comments
 (0)