@mailhooks/sdk 1.0.5 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/webhooks.d.ts +123 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +81 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,6 +211,122 @@ try {
|
|
|
211
211
|
- **404 Not Found**: Resource not found
|
|
212
212
|
- **429 Too Many Requests**: Rate limit exceeded
|
|
213
213
|
|
|
214
|
+
## Webhook Signature Verification
|
|
215
|
+
|
|
216
|
+
When you create a webhook in Mailhooks, you receive a secret that can be used to verify that incoming webhook requests are genuinely from Mailhooks. Each webhook request includes an `X-Webhook-Signature` header containing an HMAC-SHA256 signature of the request body.
|
|
217
|
+
|
|
218
|
+
### Verifying Signatures
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { verifyWebhookSignature, parseWebhookPayload } from '@mailhooks/sdk';
|
|
222
|
+
|
|
223
|
+
// Express.js example - IMPORTANT: Use raw body parser for signature verification
|
|
224
|
+
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
225
|
+
const signature = req.headers['x-webhook-signature'] as string;
|
|
226
|
+
const payload = req.body.toString();
|
|
227
|
+
|
|
228
|
+
// Verify the signature using your webhook secret
|
|
229
|
+
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
230
|
+
console.error('Invalid webhook signature');
|
|
231
|
+
return res.status(401).send('Invalid signature');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse the verified payload
|
|
235
|
+
const event = parseWebhookPayload(payload);
|
|
236
|
+
|
|
237
|
+
console.log(`Received email from ${event.from}: ${event.subject}`);
|
|
238
|
+
|
|
239
|
+
// Process the webhook...
|
|
240
|
+
res.status(200).send('OK');
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Webhook Payload
|
|
245
|
+
|
|
246
|
+
The webhook payload contains the following fields:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
interface WebhookPayload {
|
|
250
|
+
id: string; // Unique email ID
|
|
251
|
+
from: string; // Sender email address
|
|
252
|
+
to: string[]; // Array of recipient addresses
|
|
253
|
+
subject: string; // Email subject
|
|
254
|
+
body: string; // Plain text body
|
|
255
|
+
html?: string; // HTML body (if available)
|
|
256
|
+
attachments: Array<{ // Attachment metadata
|
|
257
|
+
filename: string;
|
|
258
|
+
contentType: string;
|
|
259
|
+
size: number;
|
|
260
|
+
}>;
|
|
261
|
+
receivedAt: string; // ISO 8601 timestamp
|
|
262
|
+
|
|
263
|
+
// Email authentication results
|
|
264
|
+
spfResult?: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none';
|
|
265
|
+
dkimResult?: 'pass' | 'fail' | 'none' | 'temperror' | 'permerror';
|
|
266
|
+
dmarcResult?: 'pass' | 'fail' | 'none' | 'temperror' | 'permerror';
|
|
267
|
+
authSummary?: 'pass' | 'fail' | 'partial';
|
|
268
|
+
|
|
269
|
+
// Spam detection
|
|
270
|
+
spamScore?: number; // 0.0 to 1.0
|
|
271
|
+
isSpam?: boolean;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Security Best Practices
|
|
276
|
+
|
|
277
|
+
1. **Always verify signatures** before processing webhook data
|
|
278
|
+
2. **Use the raw request body** for signature verification (not parsed JSON)
|
|
279
|
+
3. **Store secrets securely** in environment variables, not in code
|
|
280
|
+
4. **Use HTTPS** for your webhook endpoint
|
|
281
|
+
5. **Respond quickly** - return 200 OK before doing heavy processing
|
|
282
|
+
|
|
283
|
+
### Framework Examples
|
|
284
|
+
|
|
285
|
+
#### Next.js API Route
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { verifyWebhookSignature, parseWebhookPayload } from '@mailhooks/sdk';
|
|
289
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
290
|
+
|
|
291
|
+
export async function POST(request: NextRequest) {
|
|
292
|
+
const payload = await request.text();
|
|
293
|
+
const signature = request.headers.get('x-webhook-signature') || '';
|
|
294
|
+
|
|
295
|
+
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
296
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const event = parseWebhookPayload(payload);
|
|
300
|
+
// Process event...
|
|
301
|
+
|
|
302
|
+
return NextResponse.json({ received: true });
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Fastify
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { verifyWebhookSignature, parseWebhookPayload } from '@mailhooks/sdk';
|
|
310
|
+
|
|
311
|
+
fastify.post('/webhook', {
|
|
312
|
+
config: {
|
|
313
|
+
rawBody: true, // Enable raw body
|
|
314
|
+
},
|
|
315
|
+
}, async (request, reply) => {
|
|
316
|
+
const signature = request.headers['x-webhook-signature'] as string;
|
|
317
|
+
const payload = request.rawBody?.toString() || '';
|
|
318
|
+
|
|
319
|
+
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
320
|
+
return reply.status(401).send({ error: 'Invalid signature' });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const event = parseWebhookPayload(payload);
|
|
324
|
+
// Process event...
|
|
325
|
+
|
|
326
|
+
return { received: true };
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
214
330
|
## License
|
|
215
331
|
|
|
216
332
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Mailhooks } from './mailhooks';
|
|
2
2
|
export { EmailsResource } from './resources/emails';
|
|
3
3
|
export * from './types';
|
|
4
|
+
export { verifyWebhookSignature, parseWebhookPayload, constructSignature, type WebhookPayload, } from './webhooks';
|
|
4
5
|
import { Mailhooks } from './mailhooks';
|
|
5
6
|
export default Mailhooks;
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,eAAe,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,cAAc,SAAS,CAAC;AAGxB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,eAAe,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,12 +14,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.EmailsResource = exports.Mailhooks = void 0;
|
|
17
|
+
exports.constructSignature = exports.parseWebhookPayload = exports.verifyWebhookSignature = exports.EmailsResource = exports.Mailhooks = void 0;
|
|
18
18
|
var mailhooks_1 = require("./mailhooks");
|
|
19
19
|
Object.defineProperty(exports, "Mailhooks", { enumerable: true, get: function () { return mailhooks_1.Mailhooks; } });
|
|
20
20
|
var emails_1 = require("./resources/emails");
|
|
21
21
|
Object.defineProperty(exports, "EmailsResource", { enumerable: true, get: function () { return emails_1.EmailsResource; } });
|
|
22
22
|
__exportStar(require("./types"), exports);
|
|
23
|
+
// Webhook verification utilities
|
|
24
|
+
var webhooks_1 = require("./webhooks");
|
|
25
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhooks_1.verifyWebhookSignature; } });
|
|
26
|
+
Object.defineProperty(exports, "parseWebhookPayload", { enumerable: true, get: function () { return webhooks_1.parseWebhookPayload; } });
|
|
27
|
+
Object.defineProperty(exports, "constructSignature", { enumerable: true, get: function () { return webhooks_1.constructSignature; } });
|
|
23
28
|
// Default export for convenience
|
|
24
29
|
const mailhooks_2 = require("./mailhooks");
|
|
25
30
|
exports.default = mailhooks_2.Mailhooks;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook payload sent by Mailhooks when an email is received.
|
|
3
|
+
*/
|
|
4
|
+
export interface WebhookPayload {
|
|
5
|
+
/** Unique email ID */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Sender email address */
|
|
8
|
+
from: string;
|
|
9
|
+
/** Array of recipient email addresses */
|
|
10
|
+
to: string[];
|
|
11
|
+
/** Email subject line */
|
|
12
|
+
subject: string;
|
|
13
|
+
/** Plain text body of the email */
|
|
14
|
+
body: string;
|
|
15
|
+
/** HTML body of the email (if available) */
|
|
16
|
+
html?: string;
|
|
17
|
+
/** Array of attachment metadata */
|
|
18
|
+
attachments: Array<{
|
|
19
|
+
filename: string;
|
|
20
|
+
contentType: string;
|
|
21
|
+
size: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** ISO 8601 timestamp when the email was received */
|
|
24
|
+
receivedAt: string;
|
|
25
|
+
/** SPF authentication result */
|
|
26
|
+
spfResult?: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none';
|
|
27
|
+
/** DKIM authentication result */
|
|
28
|
+
dkimResult?: 'pass' | 'fail' | 'none' | 'temperror' | 'permerror';
|
|
29
|
+
/** DMARC authentication result */
|
|
30
|
+
dmarcResult?: 'pass' | 'fail' | 'none' | 'temperror' | 'permerror';
|
|
31
|
+
/** Overall authentication summary */
|
|
32
|
+
authSummary?: 'pass' | 'fail' | 'partial';
|
|
33
|
+
/** Email headers as key-value pairs */
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
/** Authentication diagnostic details */
|
|
36
|
+
authDiagnostics?: {
|
|
37
|
+
spf: {
|
|
38
|
+
clientIp: string;
|
|
39
|
+
domain: string;
|
|
40
|
+
record?: string;
|
|
41
|
+
helo?: string;
|
|
42
|
+
} | null;
|
|
43
|
+
dkim: Array<{
|
|
44
|
+
domain: string;
|
|
45
|
+
selector?: string;
|
|
46
|
+
algorithm?: string;
|
|
47
|
+
aligned?: boolean;
|
|
48
|
+
result: string;
|
|
49
|
+
}>;
|
|
50
|
+
dmarc: {
|
|
51
|
+
domain: string;
|
|
52
|
+
policy: string;
|
|
53
|
+
record?: string;
|
|
54
|
+
alignment: {
|
|
55
|
+
spf: {
|
|
56
|
+
result: string | false;
|
|
57
|
+
strict: boolean;
|
|
58
|
+
};
|
|
59
|
+
dkim: {
|
|
60
|
+
result: string | false;
|
|
61
|
+
strict: boolean;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
} | null;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verifies a webhook signature using HMAC-SHA256.
|
|
69
|
+
*
|
|
70
|
+
* Each webhook request from Mailhooks includes an `X-Webhook-Signature` header
|
|
71
|
+
* containing a hex-encoded HMAC-SHA256 signature of the request body.
|
|
72
|
+
*
|
|
73
|
+
* @param payload - The raw request body as a string or Buffer
|
|
74
|
+
* @param signature - The signature from the `X-Webhook-Signature` header
|
|
75
|
+
* @param secret - Your webhook secret (starts with `whsec_`)
|
|
76
|
+
* @returns `true` if the signature is valid, `false` otherwise
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* import { verifyWebhookSignature } from '@mailhooks/sdk';
|
|
81
|
+
*
|
|
82
|
+
* // Express.js with raw body parser
|
|
83
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
84
|
+
* const signature = req.headers['x-webhook-signature'] as string;
|
|
85
|
+
* const payload = req.body.toString();
|
|
86
|
+
*
|
|
87
|
+
* if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
88
|
+
* return res.status(401).send('Invalid signature');
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* const event = JSON.parse(payload);
|
|
92
|
+
* // Process the webhook...
|
|
93
|
+
* res.status(200).send('OK');
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function verifyWebhookSignature(payload: string | Buffer, signature: string, secret: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Parses a webhook payload from a JSON string.
|
|
100
|
+
*
|
|
101
|
+
* @param body - The raw request body as a string
|
|
102
|
+
* @returns The parsed webhook payload
|
|
103
|
+
* @throws {SyntaxError} If the body is not valid JSON
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* import { parseWebhookPayload } from '@mailhooks/sdk';
|
|
108
|
+
*
|
|
109
|
+
* const payload = parseWebhookPayload(req.body.toString());
|
|
110
|
+
* console.log(`Received email from ${payload.from}: ${payload.subject}`);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare function parseWebhookPayload(body: string): WebhookPayload;
|
|
114
|
+
/**
|
|
115
|
+
* Constructs the expected signature for a webhook payload.
|
|
116
|
+
* Useful for debugging or manual verification.
|
|
117
|
+
*
|
|
118
|
+
* @param payload - The raw request body as a string or Buffer
|
|
119
|
+
* @param secret - Your webhook secret
|
|
120
|
+
* @returns The expected HMAC-SHA256 signature as a hex string
|
|
121
|
+
*/
|
|
122
|
+
export declare function constructSignature(payload: string | Buffer, secret: string): string;
|
|
123
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,WAAW,EAAE,KAAK,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IACH,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;IAC9D,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAClE,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IACnE,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC1C,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,wCAAwC;IACxC,eAAe,CAAC,EAAE;QAChB,GAAG,EAAE;YACH,QAAQ,EAAE,MAAM,CAAC;YACjB,MAAM,EAAE,MAAM,CAAC;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,IAAI,CAAC,EAAE,MAAM,CAAC;SACf,GAAG,IAAI,CAAC;QACT,IAAI,EAAE,KAAK,CAAC;YACV,MAAM,EAAE,MAAM,CAAC;YACf,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,OAAO,CAAC,EAAE,OAAO,CAAC;YAClB,MAAM,EAAE,MAAM,CAAC;SAChB,CAAC,CAAC;QACH,KAAK,EAAE;YACL,MAAM,EAAE,MAAM,CAAC;YACf,MAAM,EAAE,MAAM,CAAC;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,SAAS,EAAE;gBACT,GAAG,EAAE;oBAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;oBAAC,MAAM,EAAE,OAAO,CAAA;iBAAE,CAAC;gBACjD,IAAI,EAAE;oBAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;oBAAC,MAAM,EAAE,OAAO,CAAA;iBAAE,CAAC;aACnD,CAAC;SACH,GAAG,IAAI,CAAC;KACV,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAqBT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,MAAM,CAER"}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
4
|
+
exports.parseWebhookPayload = parseWebhookPayload;
|
|
5
|
+
exports.constructSignature = constructSignature;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
/**
|
|
8
|
+
* Verifies a webhook signature using HMAC-SHA256.
|
|
9
|
+
*
|
|
10
|
+
* Each webhook request from Mailhooks includes an `X-Webhook-Signature` header
|
|
11
|
+
* containing a hex-encoded HMAC-SHA256 signature of the request body.
|
|
12
|
+
*
|
|
13
|
+
* @param payload - The raw request body as a string or Buffer
|
|
14
|
+
* @param signature - The signature from the `X-Webhook-Signature` header
|
|
15
|
+
* @param secret - Your webhook secret (starts with `whsec_`)
|
|
16
|
+
* @returns `true` if the signature is valid, `false` otherwise
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { verifyWebhookSignature } from '@mailhooks/sdk';
|
|
21
|
+
*
|
|
22
|
+
* // Express.js with raw body parser
|
|
23
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
24
|
+
* const signature = req.headers['x-webhook-signature'] as string;
|
|
25
|
+
* const payload = req.body.toString();
|
|
26
|
+
*
|
|
27
|
+
* if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
28
|
+
* return res.status(401).send('Invalid signature');
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* const event = JSON.parse(payload);
|
|
32
|
+
* // Process the webhook...
|
|
33
|
+
* res.status(200).send('OK');
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
38
|
+
if (!payload || !signature || !secret) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Normalize and validate signature format (should be 64 hex chars for SHA256)
|
|
42
|
+
const normalizedSignature = signature.trim().toLowerCase();
|
|
43
|
+
if (!/^[a-f0-9]{64}$/.test(normalizedSignature)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const expectedSignature = (0, crypto_1.createHmac)('sha256', secret)
|
|
47
|
+
.update(payload)
|
|
48
|
+
.digest('hex');
|
|
49
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
50
|
+
// Both buffers are guaranteed to be 32 bytes (64 hex chars)
|
|
51
|
+
return (0, crypto_1.timingSafeEqual)(Buffer.from(normalizedSignature, 'hex'), Buffer.from(expectedSignature, 'hex'));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parses a webhook payload from a JSON string.
|
|
55
|
+
*
|
|
56
|
+
* @param body - The raw request body as a string
|
|
57
|
+
* @returns The parsed webhook payload
|
|
58
|
+
* @throws {SyntaxError} If the body is not valid JSON
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { parseWebhookPayload } from '@mailhooks/sdk';
|
|
63
|
+
*
|
|
64
|
+
* const payload = parseWebhookPayload(req.body.toString());
|
|
65
|
+
* console.log(`Received email from ${payload.from}: ${payload.subject}`);
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
function parseWebhookPayload(body) {
|
|
69
|
+
return JSON.parse(body);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Constructs the expected signature for a webhook payload.
|
|
73
|
+
* Useful for debugging or manual verification.
|
|
74
|
+
*
|
|
75
|
+
* @param payload - The raw request body as a string or Buffer
|
|
76
|
+
* @param secret - Your webhook secret
|
|
77
|
+
* @returns The expected HMAC-SHA256 signature as a hex string
|
|
78
|
+
*/
|
|
79
|
+
function constructSignature(payload, secret) {
|
|
80
|
+
return (0, crypto_1.createHmac)('sha256', secret).update(payload).digest('hex');
|
|
81
|
+
}
|