@notifykit/sdk 1.1.0 → 1.3.0
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 +88 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/types.d.ts +0 -8
- package/dist/webhooks.d.ts +40 -0
- package/dist/webhooks.js +102 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ const webhookJob = await client.sendWebhook({
|
|
|
40
40
|
|
|
41
41
|
Emails are queued immediately (HTTP 202 Accepted) and delivered asynchronously. Use [getJob](#tracking-jobs) to confirm delivery.
|
|
42
42
|
|
|
43
|
-
> **Note:** On the **Free plan**, emails send from NotifyKit's shared SendGrid account (`noreply@notifykit.dev`). On **Indie/Startup plans**, connect your own SendGrid API key in the dashboard before sending emails.
|
|
43
|
+
> **Note:** On the **Free plan**, emails send from NotifyKit's shared SendGrid account (`noreply@notifykit.dev`). On **Indie/Startup plans**, connect your own SendGrid, Resend, or Postmark API key in the dashboard before sending emails.
|
|
44
44
|
|
|
45
45
|
### Basic Email
|
|
46
46
|
|
|
@@ -59,7 +59,7 @@ await client.sendEmail({
|
|
|
59
59
|
to: "user@example.com",
|
|
60
60
|
subject: "Welcome",
|
|
61
61
|
body: "<h1>Hello</h1>",
|
|
62
|
-
from: "hello@em.yourapp.com", // Must be a verified domain
|
|
62
|
+
from: "hello@em.yourapp.com", // Must be a verified sending domain
|
|
63
63
|
});
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -87,16 +87,18 @@ await client.sendEmail({
|
|
|
87
87
|
provider: "SENDGRID",
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
// Force
|
|
90
|
+
// Force Postmark first, then Resend if Postmark fails. No other providers tried.
|
|
91
91
|
await client.sendEmail({
|
|
92
92
|
to: "user@example.com",
|
|
93
93
|
subject: "Receipt",
|
|
94
94
|
body: "<h1>Thanks</h1>",
|
|
95
|
-
provider: "
|
|
95
|
+
provider: "POSTMARK",
|
|
96
96
|
fallback: "RESEND",
|
|
97
97
|
});
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
Valid provider values: `"SENDGRID"` | `"RESEND"` | `"POSTMARK"`
|
|
101
|
+
|
|
100
102
|
**Validation:**
|
|
101
103
|
|
|
102
104
|
| Case | Outcome |
|
|
@@ -104,6 +106,7 @@ await client.sendEmail({
|
|
|
104
106
|
| `fallback` set without `provider` | `400 Bad Request` |
|
|
105
107
|
| `provider` equals `fallback` | `400 Bad Request` |
|
|
106
108
|
| Requested `provider` or `fallback` not configured for your account | `400 Bad Request` |
|
|
109
|
+
| Either provider used on Free plan | `400 Bad Request` |
|
|
107
110
|
|
|
108
111
|
Forced routing is a contract: NotifyKit does **not** retry through providers you didn't authorize. The routing fields persist with the job, so manual or automatic retries replay the same attempt set.
|
|
109
112
|
|
|
@@ -130,7 +133,7 @@ await client.sendEmail({
|
|
|
130
133
|
|
|
131
134
|
## Sending Webhooks
|
|
132
135
|
|
|
133
|
-
Webhooks are queued immediately (HTTP 202 Accepted) and delivered asynchronously with automatic retries on failure.
|
|
136
|
+
Webhooks are queued immediately (HTTP 202 Accepted) and delivered asynchronously with automatic retries on failure. Payloads are capped at **10kb**.
|
|
134
137
|
|
|
135
138
|
### Basic Webhook
|
|
136
139
|
|
|
@@ -152,7 +155,6 @@ await client.sendWebhook({
|
|
|
152
155
|
method: "POST",
|
|
153
156
|
payload: { orderId: "12345" },
|
|
154
157
|
headers: {
|
|
155
|
-
"X-Webhook-Secret": process.env.WEBHOOK_SECRET!,
|
|
156
158
|
"X-Event-Type": "order.created",
|
|
157
159
|
},
|
|
158
160
|
idempotencyKey: "order-12345-webhook",
|
|
@@ -167,6 +169,50 @@ await client.sendWebhook({
|
|
|
167
169
|
|
|
168
170
|
---
|
|
169
171
|
|
|
172
|
+
## Webhook Signing
|
|
173
|
+
|
|
174
|
+
When a webhook signing secret is configured in your dashboard, NotifyKit signs every outgoing webhook delivery with HMAC-SHA256. Your receiving endpoint can verify this signature to confirm the request is genuine and hasn't been replayed.
|
|
175
|
+
|
|
176
|
+
**Headers sent with each delivery:**
|
|
177
|
+
|
|
178
|
+
| Header | Value |
|
|
179
|
+
| ----------------------- | ------------------------------ |
|
|
180
|
+
| `X-Webhook-Timestamp` | Unix timestamp (seconds) |
|
|
181
|
+
| `X-Webhook-Signature` | `t=<timestamp>,v1=<hex>` |
|
|
182
|
+
|
|
183
|
+
### Verifying Signatures
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { verifyWebhookSignature } from "@notifykit/sdk";
|
|
187
|
+
|
|
188
|
+
app.post("/webhooks/notifykit", (req, res) => {
|
|
189
|
+
const valid = verifyWebhookSignature({
|
|
190
|
+
payload: req.rawBody, // raw string — NOT parsed JSON
|
|
191
|
+
timestamp: req.headers["x-webhook-timestamp"],
|
|
192
|
+
signature: req.headers["x-webhook-signature"],
|
|
193
|
+
secret: process.env.NOTIFYKIT_WEBHOOK_SECRET!,
|
|
194
|
+
tolerance: 300, // optional, default 300s (5 min)
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!valid) return res.status(401).send("Invalid signature");
|
|
198
|
+
|
|
199
|
+
const event = req.body;
|
|
200
|
+
// handle event...
|
|
201
|
+
res.sendStatus(200);
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> **Important:** Always use the **raw body string** (`req.rawBody`), not the parsed JSON object. Re-serializing a parsed object can produce a different byte sequence and will cause verification to fail.
|
|
206
|
+
|
|
207
|
+
The `tolerance` option rejects requests older than N seconds, protecting against replay attacks. Set it to `0` to disable the time check entirely.
|
|
208
|
+
|
|
209
|
+
**`verifyWebhookSignature` returns `false` (never throws) when:**
|
|
210
|
+
- The signature header is missing or malformed
|
|
211
|
+
- The timestamp is outside the tolerance window
|
|
212
|
+
- The HMAC digest does not match
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
170
216
|
## Tracking Jobs
|
|
171
217
|
|
|
172
218
|
Every notification returns a job ID you can use to track delivery status.
|
|
@@ -209,7 +255,7 @@ for (const log of status.deliveryLogs) {
|
|
|
209
255
|
}
|
|
210
256
|
```
|
|
211
257
|
|
|
212
|
-
For successful sends, the last entry's `usedProvider` is the provider that delivered. For failures, it's the last provider attempted. Webhook jobs return
|
|
258
|
+
For successful sends, the last entry's `usedProvider` is the provider that delivered. For failures, it's the last provider attempted. Webhook jobs and Free plan jobs return `null` for `usedProvider`.
|
|
213
259
|
|
|
214
260
|
### List Jobs with Filters
|
|
215
261
|
|
|
@@ -217,8 +263,8 @@ For successful sends, the last entry's `usedProvider` is the provider that deliv
|
|
|
217
263
|
const result = await client.listJobs({
|
|
218
264
|
page: 1,
|
|
219
265
|
limit: 20,
|
|
220
|
-
type: "email",
|
|
221
|
-
status: "failed", //
|
|
266
|
+
type: "email", // 'email' | 'webhook'
|
|
267
|
+
status: "failed", // 'pending' | 'processing' | 'completed' | 'failed'
|
|
222
268
|
});
|
|
223
269
|
|
|
224
270
|
console.log(`Total: ${result.pagination.total} jobs`);
|
|
@@ -266,10 +312,12 @@ try {
|
|
|
266
312
|
|
|
267
313
|
if (error.isStatus(400)) console.error("Bad request:", error.message);
|
|
268
314
|
if (error.isStatus(401)) console.error("Invalid API key");
|
|
269
|
-
if (error.isStatus(403))
|
|
270
|
-
console.error("Quota or permission error:", error.message);
|
|
315
|
+
if (error.isStatus(403)) console.error("Quota or permission error:", error.message);
|
|
271
316
|
if (error.isStatus(409)) console.error("Duplicate idempotency key");
|
|
272
|
-
if (error.isStatus(429))
|
|
317
|
+
if (error.isStatus(429)) {
|
|
318
|
+
console.error("Rate limit exceeded");
|
|
319
|
+
if (error.retryAfter) console.log(`Retry after ${error.retryAfter}s`);
|
|
320
|
+
}
|
|
273
321
|
}
|
|
274
322
|
}
|
|
275
323
|
```
|
|
@@ -278,15 +326,23 @@ try {
|
|
|
278
326
|
|
|
279
327
|
## API Reference
|
|
280
328
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
329
|
+
### `NotifyKitClient` methods
|
|
330
|
+
|
|
331
|
+
| Method | Description | Returns |
|
|
332
|
+
| ------------------------------- | ------------------------------- | ------------------------------- |
|
|
333
|
+
| `sendEmail(options)` | Send an email notification | `Promise<JobResponse>` |
|
|
334
|
+
| `sendWebhook(options)` | Send a webhook notification | `Promise<JobResponse>` |
|
|
335
|
+
| `getJob(jobId)` | Get job status and delivery logs | `Promise<JobStatus>` |
|
|
336
|
+
| `listJobs(options?)` | List jobs with optional filters | `Promise<{ data, pagination }>` |
|
|
337
|
+
| `retryJob(jobId)` | Retry a failed job | `Promise<RetryJobResponse>` |
|
|
338
|
+
| `ping()` | Test API connection | `Promise<string>` |
|
|
339
|
+
| `getApiInfo()` | Get API version info | `Promise<ApiInfo>` |
|
|
340
|
+
|
|
341
|
+
### Standalone utilities
|
|
342
|
+
|
|
343
|
+
| Function | Description |
|
|
344
|
+
| ------------------------------- | -------------------------------------------------- |
|
|
345
|
+
| `verifyWebhookSignature(options)` | Verify HMAC-SHA256 signature on incoming webhooks |
|
|
290
346
|
|
|
291
347
|
### TypeScript Types
|
|
292
348
|
|
|
@@ -296,7 +352,13 @@ import type {
|
|
|
296
352
|
SendEmailOptions,
|
|
297
353
|
SendWebhookOptions,
|
|
298
354
|
JobResponse,
|
|
355
|
+
JobStatus,
|
|
356
|
+
JobSummary,
|
|
357
|
+
DeliveryLog,
|
|
358
|
+
RetryJobResponse,
|
|
299
359
|
ApiInfo,
|
|
360
|
+
EmailProvider,
|
|
361
|
+
VerifyWebhookSignatureOptions,
|
|
300
362
|
} from "@notifykit/sdk";
|
|
301
363
|
```
|
|
302
364
|
|
|
@@ -304,11 +366,11 @@ import type {
|
|
|
304
366
|
|
|
305
367
|
## Plans
|
|
306
368
|
|
|
307
|
-
| Plan | Price
|
|
308
|
-
| ------- |
|
|
309
|
-
| Free | $0
|
|
310
|
-
| Indie | $
|
|
311
|
-
| Startup | $
|
|
369
|
+
| Plan | Price | Webhooks/month | Emails/month | Rate limit |
|
|
370
|
+
| ------- | ------- | -------------- | -------------------------- | ------------ |
|
|
371
|
+
| Free | $0 | 100 (shared) | 100 (shared with webhooks) | 5 req/min |
|
|
372
|
+
| Indie | $5/mo | 4,000 | Unlimited (via your key) | 50 req/min |
|
|
373
|
+
| Startup | $15/mo | 15,000 | Unlimited (via your key) | 200 req/min |
|
|
312
374
|
|
|
313
375
|
---
|
|
314
376
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,8 +14,10 @@ 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.NotifyKitClient = void 0;
|
|
17
|
+
exports.verifyWebhookSignature = exports.NotifyKitClient = void 0;
|
|
18
18
|
var client_1 = require("./client");
|
|
19
19
|
Object.defineProperty(exports, "NotifyKitClient", { enumerable: true, get: function () { return client_1.NotifyKitClient; } });
|
|
20
20
|
__exportStar(require("./types"), exports);
|
|
21
21
|
__exportStar(require("./errors"), exports);
|
|
22
|
+
var webhooks_1 = require("./webhooks");
|
|
23
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhooks_1.verifyWebhookSignature; } });
|
package/dist/types.d.ts
CHANGED
|
@@ -16,15 +16,7 @@ export interface SendEmailOptions {
|
|
|
16
16
|
from?: string;
|
|
17
17
|
priority?: 1 | 5 | 10;
|
|
18
18
|
idempotencyKey?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Force this email through a specific configured provider (paid plans only).
|
|
21
|
-
* If unset, the customer's priority order with full failover applies.
|
|
22
|
-
*/
|
|
23
19
|
provider?: EmailProvider;
|
|
24
|
-
/**
|
|
25
|
-
* Fallback provider to try if `provider` fails. Ignored unless `provider`
|
|
26
|
-
* is set. Other configured providers are not tried.
|
|
27
|
-
*/
|
|
28
20
|
fallback?: EmailProvider;
|
|
29
21
|
}
|
|
30
22
|
export interface SendWebhookOptions {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface VerifyWebhookSignatureOptions {
|
|
2
|
+
/** Raw request body as a string — do NOT pass a parsed object. */
|
|
3
|
+
payload: string;
|
|
4
|
+
/** Value of the X-Webhook-Timestamp header. */
|
|
5
|
+
timestamp: string;
|
|
6
|
+
/** Value of the X-Webhook-Signature header (format: t=<ts>,v1=<hex>). */
|
|
7
|
+
signature: string;
|
|
8
|
+
/** Plaintext webhook signing secret from your NotifyKit dashboard. */
|
|
9
|
+
secret: string;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum age of the request in seconds before it is rejected as a replay.
|
|
12
|
+
* Defaults to 300 (5 minutes).
|
|
13
|
+
*/
|
|
14
|
+
tolerance?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Verify an incoming webhook signature from NotifyKit.
|
|
18
|
+
*
|
|
19
|
+
* NotifyKit signs outgoing webhook requests with HMAC-SHA256 when a signing
|
|
20
|
+
* secret is configured. Call this on your receiving endpoint to confirm the
|
|
21
|
+
* request is genuine and has not been replayed.
|
|
22
|
+
*
|
|
23
|
+
* @returns `true` if the signature is valid and the request is within the
|
|
24
|
+
* tolerance window, `false` otherwise.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* app.post('/webhook', (req, res) => {
|
|
29
|
+
* const valid = verifyWebhookSignature({
|
|
30
|
+
* payload: req.rawBody, // raw string, not parsed JSON
|
|
31
|
+
* timestamp: req.headers['x-webhook-timestamp'],
|
|
32
|
+
* signature: req.headers['x-webhook-signature'],
|
|
33
|
+
* secret: process.env.NOTIFYKIT_WEBHOOK_SECRET,
|
|
34
|
+
* });
|
|
35
|
+
* if (!valid) return res.status(401).send('Invalid signature');
|
|
36
|
+
* // ...
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): boolean;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
/**
|
|
39
|
+
* Verify an incoming webhook signature from NotifyKit.
|
|
40
|
+
*
|
|
41
|
+
* NotifyKit signs outgoing webhook requests with HMAC-SHA256 when a signing
|
|
42
|
+
* secret is configured. Call this on your receiving endpoint to confirm the
|
|
43
|
+
* request is genuine and has not been replayed.
|
|
44
|
+
*
|
|
45
|
+
* @returns `true` if the signature is valid and the request is within the
|
|
46
|
+
* tolerance window, `false` otherwise.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* app.post('/webhook', (req, res) => {
|
|
51
|
+
* const valid = verifyWebhookSignature({
|
|
52
|
+
* payload: req.rawBody, // raw string, not parsed JSON
|
|
53
|
+
* timestamp: req.headers['x-webhook-timestamp'],
|
|
54
|
+
* signature: req.headers['x-webhook-signature'],
|
|
55
|
+
* secret: process.env.NOTIFYKIT_WEBHOOK_SECRET,
|
|
56
|
+
* });
|
|
57
|
+
* if (!valid) return res.status(401).send('Invalid signature');
|
|
58
|
+
* // ...
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function verifyWebhookSignature(options) {
|
|
63
|
+
const { payload, timestamp, signature, secret, tolerance = 300 } = options;
|
|
64
|
+
let v1;
|
|
65
|
+
let headerTs;
|
|
66
|
+
for (const part of signature.split(",")) {
|
|
67
|
+
const eq = part.indexOf("=");
|
|
68
|
+
if (eq === -1)
|
|
69
|
+
continue;
|
|
70
|
+
const key = part.slice(0, eq).trim();
|
|
71
|
+
const val = part.slice(eq + 1).trim();
|
|
72
|
+
if (key === "v1")
|
|
73
|
+
v1 = val;
|
|
74
|
+
if (key === "t")
|
|
75
|
+
headerTs = val;
|
|
76
|
+
}
|
|
77
|
+
if (!v1 || !headerTs)
|
|
78
|
+
return false;
|
|
79
|
+
if (headerTs !== timestamp)
|
|
80
|
+
return false;
|
|
81
|
+
const ts = parseInt(timestamp, 10);
|
|
82
|
+
if (!Number.isFinite(ts))
|
|
83
|
+
return false;
|
|
84
|
+
const age = Math.floor(Date.now() / 1000) - ts;
|
|
85
|
+
if (age < 0 || age > tolerance)
|
|
86
|
+
return false;
|
|
87
|
+
const signed = `${timestamp}.${payload}`;
|
|
88
|
+
const expected = crypto
|
|
89
|
+
.createHmac("sha256", secret)
|
|
90
|
+
.update(signed)
|
|
91
|
+
.digest("hex");
|
|
92
|
+
try {
|
|
93
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
94
|
+
const actualBuf = Buffer.from(v1, "hex");
|
|
95
|
+
if (expectedBuf.length !== actualBuf.length)
|
|
96
|
+
return false;
|
|
97
|
+
return crypto.timingSafeEqual(expectedBuf, actualBuf);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|