Skip to content

Commit f67f37f

Browse files
bukinoshitalucasfcosta
authored andcommitted
feat: add verify webhooks (#636)
1 parent b4467c9 commit f67f37f

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
"url": "https://github.com/resendlabs/resend-node/issues"
4444
},
4545
"homepage": "https://github.com/resendlabs/resend-node#readme",
46+
"dependencies": {
47+
"svix": "1.76.1"
48+
},
4649
"peerDependencies": {
4750
"@react-email/render": "*"
4851
},

pnpm-lock.yaml

Lines changed: 54 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
@@ -10,6 +10,7 @@ import { Contacts } from './contacts/contacts';
1010
import { Domains } from './domains/domains';
1111
import { Emails } from './emails/emails';
1212
import type { ErrorResponse } from './interfaces';
13+
import { Webhooks } from './webhooks/webhooks';
1314

1415
const defaultBaseUrl = 'https://api.resend.com';
1516
const defaultUserAgent = `resend-node:${version}`;
@@ -32,6 +33,7 @@ export class Resend {
3233
readonly contacts = new Contacts(this);
3334
readonly domains = new Domains(this);
3435
readonly emails = new Emails(this);
36+
readonly webhooks = new Webhooks();
3537

3638
constructor(readonly key?: string) {
3739
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)