@notifykit/sdk 1.0.5 → 1.2.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 +71 -18
- package/dist/client.d.ts +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/types.d.ts +22 -0
- package/dist/webhooks.d.ts +40 -0
- package/dist/webhooks.js +102 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,41 @@ await client.sendEmail({
|
|
|
74
74
|
});
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Per-Message Provider Routing (Paid Plans)
|
|
78
|
+
|
|
79
|
+
By default, NotifyKit picks the email provider based on your configured priority order and falls back through all of them on failure. To pin a specific provider per message, pass `provider`. To narrow the failover to one alternative, pass `fallback`.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Force this email through SendGrid; if it fails, the job fails.
|
|
83
|
+
await client.sendEmail({
|
|
84
|
+
to: "user@example.com",
|
|
85
|
+
subject: "Receipt",
|
|
86
|
+
body: "<h1>Thanks</h1>",
|
|
87
|
+
provider: "SENDGRID",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Force SendGrid first, then Resend if SendGrid fails. No other providers tried.
|
|
91
|
+
await client.sendEmail({
|
|
92
|
+
to: "user@example.com",
|
|
93
|
+
subject: "Receipt",
|
|
94
|
+
body: "<h1>Thanks</h1>",
|
|
95
|
+
provider: "SENDGRID",
|
|
96
|
+
fallback: "RESEND",
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Validation:**
|
|
101
|
+
|
|
102
|
+
| Case | Outcome |
|
|
103
|
+
| ------------------------------------------------------------------ | ----------------- |
|
|
104
|
+
| `fallback` set without `provider` | `400 Bad Request` |
|
|
105
|
+
| `provider` equals `fallback` | `400 Bad Request` |
|
|
106
|
+
| Requested `provider` or `fallback` not configured for your account | `400 Bad Request` |
|
|
107
|
+
|
|
108
|
+
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
|
+
|
|
110
|
+
To inspect which provider actually delivered (or which one was last attempted on failure), see [Tracking Jobs](#tracking-jobs) — `getJob(id)` returns a `deliveryLogs[]` array with a `usedProvider` field on each entry.
|
|
111
|
+
|
|
77
112
|
### Prevent Duplicate Sends
|
|
78
113
|
|
|
79
114
|
```typescript
|
|
@@ -114,7 +149,7 @@ await client.sendWebhook({
|
|
|
114
149
|
```typescript
|
|
115
150
|
await client.sendWebhook({
|
|
116
151
|
url: "https://yourapp.com/webhooks/order",
|
|
117
|
-
method: "POST",
|
|
152
|
+
method: "POST",
|
|
118
153
|
payload: { orderId: "12345" },
|
|
119
154
|
headers: {
|
|
120
155
|
"X-Webhook-Secret": process.env.WEBHOOK_SECRET!,
|
|
@@ -159,13 +194,30 @@ if (status.status === "completed") {
|
|
|
159
194
|
}
|
|
160
195
|
```
|
|
161
196
|
|
|
197
|
+
### Inspect Delivery Attempts
|
|
198
|
+
|
|
199
|
+
`getJob(id)` returns a `deliveryLogs[]` array — one entry per delivery attempt — with the provider that was used:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const status = await client.getJob(job.jobId);
|
|
203
|
+
|
|
204
|
+
for (const log of status.deliveryLogs) {
|
|
205
|
+
console.log(
|
|
206
|
+
`attempt ${log.attempt} via ${log.usedProvider ?? "unknown"}: ${log.status}`,
|
|
207
|
+
);
|
|
208
|
+
if (log.errorMessage) console.log(` error: ${log.errorMessage}`);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
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 an empty array.
|
|
213
|
+
|
|
162
214
|
### List Jobs with Filters
|
|
163
215
|
|
|
164
216
|
```typescript
|
|
165
217
|
const result = await client.listJobs({
|
|
166
218
|
page: 1,
|
|
167
219
|
limit: 20,
|
|
168
|
-
type: "email",
|
|
220
|
+
type: "email", // Filter by type: 'email' or 'webhook'
|
|
169
221
|
status: "failed", // Filter by status
|
|
170
222
|
});
|
|
171
223
|
|
|
@@ -210,11 +262,12 @@ try {
|
|
|
210
262
|
await client.sendEmail({ to: "bad-email", subject: "Test", body: "Hello" });
|
|
211
263
|
} catch (error) {
|
|
212
264
|
if (error instanceof NotifyKitError) {
|
|
213
|
-
console.error(error.getFullMessage());
|
|
265
|
+
console.error(error.getFullMessage());
|
|
214
266
|
|
|
215
267
|
if (error.isStatus(400)) console.error("Bad request:", error.message);
|
|
216
268
|
if (error.isStatus(401)) console.error("Invalid API key");
|
|
217
|
-
if (error.isStatus(403))
|
|
269
|
+
if (error.isStatus(403))
|
|
270
|
+
console.error("Quota or permission error:", error.message);
|
|
218
271
|
if (error.isStatus(409)) console.error("Duplicate idempotency key");
|
|
219
272
|
if (error.isStatus(429)) console.error("Rate limit exceeded");
|
|
220
273
|
}
|
|
@@ -225,15 +278,15 @@ try {
|
|
|
225
278
|
|
|
226
279
|
## API Reference
|
|
227
280
|
|
|
228
|
-
| Method | Description
|
|
229
|
-
| ---------------------- |
|
|
230
|
-
| `sendEmail(options)` | Send an email notification
|
|
231
|
-
| `sendWebhook(options)` | Send a webhook notification
|
|
232
|
-
| `getJob(jobId)` | Get job status and details
|
|
233
|
-
| `listJobs(options?)` | List jobs with optional filters
|
|
234
|
-
| `retryJob(jobId)` | Retry a failed job
|
|
235
|
-
| `ping()` | Test API connection
|
|
236
|
-
| `getApiInfo()` | Get API version info
|
|
281
|
+
| Method | Description | Returns |
|
|
282
|
+
| ---------------------- | ------------------------------- | ------------------------------- |
|
|
283
|
+
| `sendEmail(options)` | Send an email notification | `Promise<JobResponse>` |
|
|
284
|
+
| `sendWebhook(options)` | Send a webhook notification | `Promise<JobResponse>` |
|
|
285
|
+
| `getJob(jobId)` | Get job status and details | `Promise<JobStatus>` |
|
|
286
|
+
| `listJobs(options?)` | List jobs with optional filters | `Promise<{ data, pagination }>` |
|
|
287
|
+
| `retryJob(jobId)` | Retry a failed job | `Promise<RetryJobResponse>` |
|
|
288
|
+
| `ping()` | Test API connection | `Promise<string>` |
|
|
289
|
+
| `getApiInfo()` | Get API version info | `Promise<ApiInfo>` |
|
|
237
290
|
|
|
238
291
|
### TypeScript Types
|
|
239
292
|
|
|
@@ -251,11 +304,11 @@ import type {
|
|
|
251
304
|
|
|
252
305
|
## Plans
|
|
253
306
|
|
|
254
|
-
| Plan | Price
|
|
255
|
-
| ------- |
|
|
256
|
-
| Free | $0
|
|
257
|
-
| Indie | $9/mo
|
|
258
|
-
| Startup | $30/mo
|
|
307
|
+
| Plan | Price | Webhooks/month | Emails/month |
|
|
308
|
+
| ------- | ------ | -------------- | --------------------------------- |
|
|
309
|
+
| Free | $0 | 100 (shared) | 100 (shared with webhooks) |
|
|
310
|
+
| Indie | $9/mo | 4,000 | Unlimited (via your SendGrid key) |
|
|
311
|
+
| Startup | $30/mo | 15,000 | Unlimited (via your SendGrid key) |
|
|
259
312
|
|
|
260
313
|
---
|
|
261
314
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NotifyKitConfig, SendEmailOptions, SendWebhookOptions, JobResponse, JobStatus, ApiInfo, PaginationMeta, RetryJobResponse } from './types';
|
|
1
|
+
import { NotifyKitConfig, SendEmailOptions, SendWebhookOptions, JobResponse, JobStatus, JobSummary, ApiInfo, PaginationMeta, RetryJobResponse } from './types';
|
|
2
2
|
export declare class NotifyKitClient {
|
|
3
3
|
private client;
|
|
4
4
|
constructor(config: NotifyKitConfig);
|
|
@@ -19,7 +19,7 @@ export declare class NotifyKitClient {
|
|
|
19
19
|
type?: 'email' | 'webhook';
|
|
20
20
|
status?: 'pending' | 'processing' | 'completed' | 'failed';
|
|
21
21
|
}): Promise<{
|
|
22
|
-
data:
|
|
22
|
+
data: JobSummary[];
|
|
23
23
|
pagination: PaginationMeta;
|
|
24
24
|
}>;
|
|
25
25
|
/** Retry a failed job */
|
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
|
@@ -8,6 +8,7 @@ export interface PaginationMeta {
|
|
|
8
8
|
limit: number;
|
|
9
9
|
totalPages: number;
|
|
10
10
|
}
|
|
11
|
+
export type EmailProvider = "SENDGRID" | "RESEND" | "POSTMARK";
|
|
11
12
|
export interface SendEmailOptions {
|
|
12
13
|
to: string;
|
|
13
14
|
subject: string;
|
|
@@ -15,6 +16,8 @@ export interface SendEmailOptions {
|
|
|
15
16
|
from?: string;
|
|
16
17
|
priority?: 1 | 5 | 10;
|
|
17
18
|
idempotencyKey?: string;
|
|
19
|
+
provider?: EmailProvider;
|
|
20
|
+
fallback?: EmailProvider;
|
|
18
21
|
}
|
|
19
22
|
export interface SendWebhookOptions {
|
|
20
23
|
url: string;
|
|
@@ -30,6 +33,14 @@ export interface JobResponse {
|
|
|
30
33
|
type: string;
|
|
31
34
|
createdAt: string;
|
|
32
35
|
}
|
|
36
|
+
export interface DeliveryLog {
|
|
37
|
+
id: string;
|
|
38
|
+
attempt: number;
|
|
39
|
+
status: string;
|
|
40
|
+
usedProvider: EmailProvider | null;
|
|
41
|
+
errorMessage: string | null;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
33
44
|
export interface JobStatus {
|
|
34
45
|
id: string;
|
|
35
46
|
type: string;
|
|
@@ -42,6 +53,17 @@ export interface JobStatus {
|
|
|
42
53
|
createdAt: string;
|
|
43
54
|
startedAt?: string;
|
|
44
55
|
completedAt?: string;
|
|
56
|
+
deliveryLogs: DeliveryLog[];
|
|
57
|
+
}
|
|
58
|
+
export interface JobSummary {
|
|
59
|
+
id: string;
|
|
60
|
+
type: string;
|
|
61
|
+
status: "pending" | "processing" | "completed" | "failed";
|
|
62
|
+
priority: number;
|
|
63
|
+
attempts: number;
|
|
64
|
+
errorMessage?: string;
|
|
65
|
+
createdAt: string;
|
|
66
|
+
completedAt?: string;
|
|
45
67
|
}
|
|
46
68
|
export interface RetryJobResponse {
|
|
47
69
|
jobId: string;
|
|
@@ -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
|
+
}
|