@nimbusnexus/webhooks-sdk 0.3.0 → 0.5.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 +20 -2
- package/dist/index.cjs +50 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -27
- package/dist/index.d.ts +44 -27
- package/dist/index.js +47 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,12 +35,24 @@ try {
|
|
|
35
35
|
{ orderId: "ord_123", total: 4200 },
|
|
36
36
|
{ idempotencyKey: "order-123" }, // makes the publish safe to retry
|
|
37
37
|
);
|
|
38
|
-
console.log(event.eventUid, event.deliveriesCreated);
|
|
38
|
+
console.log(event.eventUid, event.projectId, event.deliveriesCreated);
|
|
39
39
|
} catch (e) {
|
|
40
40
|
if (e instanceof WebhookdApiError) console.error(e.statusCode, e.code, e.message);
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
**Projects.** A project is addressed by its **id** (`prj_…`), not a slug. Omit `projectId` — as
|
|
45
|
+
above — to publish into your workspace's **default** project; the server resolves it. Pass one only to
|
|
46
|
+
target a specific project:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await wh.publish("order.created", { orderId: "ord_123" }, { projectId: "prj_3f9a…" });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The id is opaque and per-workspace, so there is no client-side sentinel for "the default project":
|
|
53
|
+
leaving `projectId` unset omits the field entirely. The response (`event.projectId`) always carries
|
|
54
|
+
the id the event actually landed in.
|
|
55
|
+
|
|
44
56
|
Transient failures (network errors, `429`, `5xx`) are retried with backoff (a `429` honours
|
|
45
57
|
`Retry-After`); other `4xx` throw `WebhookdApiError` carrying the `{error:{code,message}}` envelope.
|
|
46
58
|
|
|
@@ -84,6 +96,10 @@ webhookd returns the original event without re-fanning-out. A record that keeps
|
|
|
84
96
|
with capped exponential backoff up to `maxAttempts` (default 10), then parked **dead** (never retried
|
|
85
97
|
again, retrievable via `store.listDead()`) and passed to the optional `onDead` callback.
|
|
86
98
|
|
|
99
|
+
`enqueue` takes the same optional `projectId` as `publish` (omit it for the default project). It is
|
|
100
|
+
persisted on the buffered record as a nullable `project_id` — `null` means "the workspace's default
|
|
101
|
+
project", and `drain` then omits the field from the publish body.
|
|
102
|
+
|
|
87
103
|
**Built-in stores** — pass one as `store` in `ClientOptions`:
|
|
88
104
|
|
|
89
105
|
| Store | Durable? | Extra needed |
|
|
@@ -111,13 +127,15 @@ const wh = new WebhookdClient({ baseUrl: "https://webhooks.example.com", apiKey:
|
|
|
111
127
|
|
|
112
128
|
// --- Endpoints ---------------------------------------------------------------
|
|
113
129
|
// Create a receiver — its signing secret is in the response exactly once, so persist it now.
|
|
130
|
+
// Omit `projectId` (here and on listEndpoints) for the workspace's default project.
|
|
114
131
|
const ep = await wh.createEndpoint("https://your-app.example/webhooks", {
|
|
115
132
|
subscriptions: [{ match_kind: "prefix", pattern: "order." }],
|
|
116
133
|
description: "orders service",
|
|
117
134
|
});
|
|
118
135
|
const { id: endpointId, secret: signingSecret } = ep;
|
|
119
136
|
|
|
120
|
-
await wh.listEndpoints(
|
|
137
|
+
await wh.listEndpoints(); // default project — { items, next_offset }
|
|
138
|
+
await wh.listEndpoints({ projectId: "prj_3f9a…" }); // a specific project, by id
|
|
121
139
|
await wh.getEndpoint(endpointId);
|
|
122
140
|
|
|
123
141
|
// PATCH — send only the keys you want to change (omitted = unchanged, null = cleared):
|
package/dist/index.cjs
CHANGED
|
@@ -38,9 +38,9 @@ __export(src_exports, {
|
|
|
38
38
|
RedisStore: () => RedisStore,
|
|
39
39
|
SqliteStore: () => SqliteStore,
|
|
40
40
|
VERSION: () => VERSION,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
WebhooksApiError: () => WebhooksApiError,
|
|
42
|
+
WebhooksClient: () => WebhooksClient,
|
|
43
|
+
WebhooksError: () => WebhooksError,
|
|
44
44
|
isDead: () => isDead,
|
|
45
45
|
sign: () => sign,
|
|
46
46
|
verify: () => verify
|
|
@@ -83,18 +83,18 @@ function verify(secret, rawBody, signature, opts = {}) {
|
|
|
83
83
|
var import_node_crypto3 = require("crypto");
|
|
84
84
|
|
|
85
85
|
// src/errors.ts
|
|
86
|
-
var
|
|
86
|
+
var WebhooksError = class extends Error {
|
|
87
87
|
constructor(message) {
|
|
88
88
|
super(message);
|
|
89
|
-
this.name = "
|
|
89
|
+
this.name = "WebhooksError";
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
|
-
var
|
|
92
|
+
var WebhooksApiError = class extends WebhooksError {
|
|
93
93
|
statusCode;
|
|
94
94
|
code;
|
|
95
95
|
constructor(statusCode, code, message) {
|
|
96
96
|
super(`[${statusCode} ${code}] ${message}`);
|
|
97
|
-
this.name = "
|
|
97
|
+
this.name = "WebhooksApiError";
|
|
98
98
|
this.statusCode = statusCode;
|
|
99
99
|
this.code = code;
|
|
100
100
|
}
|
|
@@ -201,8 +201,7 @@ function rowToRecord(row) {
|
|
|
201
201
|
id: row.id,
|
|
202
202
|
eventType: row.event_type,
|
|
203
203
|
payload: row.payload,
|
|
204
|
-
|
|
205
|
-
application: row.application,
|
|
204
|
+
projectId: row.project_id,
|
|
206
205
|
source: row.source,
|
|
207
206
|
createdAt: Number(row.created_at),
|
|
208
207
|
attempts: row.attempts,
|
|
@@ -239,8 +238,7 @@ var PostgresStore = class {
|
|
|
239
238
|
id TEXT PRIMARY KEY,
|
|
240
239
|
event_type TEXT NOT NULL,
|
|
241
240
|
payload JSONB NOT NULL,
|
|
242
|
-
|
|
243
|
-
application TEXT NOT NULL,
|
|
241
|
+
project_id TEXT,
|
|
244
242
|
source TEXT,
|
|
245
243
|
sent BOOLEAN NOT NULL DEFAULT FALSE,
|
|
246
244
|
attempts INTEGER NOT NULL,
|
|
@@ -257,13 +255,12 @@ var PostgresStore = class {
|
|
|
257
255
|
const pool = await this.ensure();
|
|
258
256
|
await pool.query(
|
|
259
257
|
`INSERT INTO ${this.table}
|
|
260
|
-
(id, event_type, payload,
|
|
261
|
-
VALUES ($1, $2, $3, $4, $5, $6,
|
|
258
|
+
(id, event_type, payload, project_id, source, sent, attempts, last_error, next_attempt_at, created_at)
|
|
259
|
+
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7, $8, $9)
|
|
262
260
|
ON CONFLICT (id) DO UPDATE SET
|
|
263
261
|
event_type = EXCLUDED.event_type,
|
|
264
262
|
payload = EXCLUDED.payload,
|
|
265
|
-
|
|
266
|
-
application = EXCLUDED.application,
|
|
263
|
+
project_id = EXCLUDED.project_id,
|
|
267
264
|
source = EXCLUDED.source,
|
|
268
265
|
sent = EXCLUDED.sent,
|
|
269
266
|
attempts = EXCLUDED.attempts,
|
|
@@ -274,8 +271,7 @@ var PostgresStore = class {
|
|
|
274
271
|
record.id,
|
|
275
272
|
record.eventType,
|
|
276
273
|
JSON.stringify(record.payload),
|
|
277
|
-
record.
|
|
278
|
-
record.application,
|
|
274
|
+
record.projectId,
|
|
279
275
|
record.source,
|
|
280
276
|
record.attempts,
|
|
281
277
|
record.lastError,
|
|
@@ -432,8 +428,7 @@ function rowToRecord2(row) {
|
|
|
432
428
|
id: row.id,
|
|
433
429
|
eventType: row.event_type,
|
|
434
430
|
payload: JSON.parse(row.payload),
|
|
435
|
-
|
|
436
|
-
application: row.application,
|
|
431
|
+
projectId: row.project_id,
|
|
437
432
|
source: row.source,
|
|
438
433
|
createdAt: row.created_at,
|
|
439
434
|
attempts: row.attempts,
|
|
@@ -460,12 +455,14 @@ var SqliteStore = class {
|
|
|
460
455
|
}
|
|
461
456
|
this.db = new DatabaseSync(path);
|
|
462
457
|
this.db.exec(
|
|
458
|
+
// `project_id` is NULLABLE: NULL = the workspace's default project (the publish body simply
|
|
459
|
+
// omits the field). Renamed from the legacy `project` slug column — this store has no
|
|
460
|
+
// schema-versioning mechanism, so a table created by an older SDK build is not upgraded.
|
|
463
461
|
`CREATE TABLE IF NOT EXISTS webhookd_outbox (
|
|
464
462
|
id TEXT PRIMARY KEY,
|
|
465
463
|
event_type TEXT NOT NULL,
|
|
466
464
|
payload TEXT NOT NULL,
|
|
467
|
-
|
|
468
|
-
application TEXT NOT NULL,
|
|
465
|
+
project_id TEXT,
|
|
469
466
|
source TEXT,
|
|
470
467
|
created_at INTEGER NOT NULL,
|
|
471
468
|
attempts INTEGER NOT NULL,
|
|
@@ -477,13 +474,12 @@ var SqliteStore = class {
|
|
|
477
474
|
save(record) {
|
|
478
475
|
this.db.prepare(
|
|
479
476
|
`INSERT INTO webhookd_outbox
|
|
480
|
-
(id, event_type, payload,
|
|
481
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
477
|
+
(id, event_type, payload, project_id, source, created_at, attempts, last_error, next_attempt_at)
|
|
478
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
482
479
|
ON CONFLICT(id) DO UPDATE SET
|
|
483
480
|
event_type = excluded.event_type,
|
|
484
481
|
payload = excluded.payload,
|
|
485
|
-
|
|
486
|
-
application = excluded.application,
|
|
482
|
+
project_id = excluded.project_id,
|
|
487
483
|
source = excluded.source,
|
|
488
484
|
created_at = excluded.created_at,
|
|
489
485
|
attempts = excluded.attempts,
|
|
@@ -493,8 +489,7 @@ var SqliteStore = class {
|
|
|
493
489
|
record.id,
|
|
494
490
|
record.eventType,
|
|
495
491
|
JSON.stringify(record.payload),
|
|
496
|
-
record.
|
|
497
|
-
record.application,
|
|
492
|
+
record.projectId,
|
|
498
493
|
record.source,
|
|
499
494
|
record.createdAt,
|
|
500
495
|
record.attempts,
|
|
@@ -539,7 +534,7 @@ var SqliteStore = class {
|
|
|
539
534
|
|
|
540
535
|
// src/client.ts
|
|
541
536
|
var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
542
|
-
var
|
|
537
|
+
var WebhooksClient = class {
|
|
543
538
|
baseUrl;
|
|
544
539
|
apiKey;
|
|
545
540
|
timeoutMs;
|
|
@@ -567,10 +562,10 @@ var WebhookdClient = class {
|
|
|
567
562
|
async publish(eventType, payload, opts = {}) {
|
|
568
563
|
const body = {
|
|
569
564
|
event_type: eventType,
|
|
570
|
-
payload
|
|
571
|
-
environment: opts.environment ?? "prod",
|
|
572
|
-
application: opts.application ?? "default"
|
|
565
|
+
payload
|
|
573
566
|
};
|
|
567
|
+
const projectId = normalizeProjectId(opts.projectId);
|
|
568
|
+
if (projectId !== null) body.project_id = projectId;
|
|
574
569
|
if (opts.source !== void 0) body.source = opts.source;
|
|
575
570
|
const headers = {};
|
|
576
571
|
if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
@@ -580,8 +575,7 @@ var WebhookdClient = class {
|
|
|
580
575
|
id: String(data.id),
|
|
581
576
|
eventUid: String(data.event_uid),
|
|
582
577
|
eventType: String(data.event_type),
|
|
583
|
-
|
|
584
|
-
environment: String(data.environment ?? "prod"),
|
|
578
|
+
projectId: String(data.project_id),
|
|
585
579
|
deliveriesCreated: Number(data.deliveries_created ?? 0),
|
|
586
580
|
source: data.source ?? null
|
|
587
581
|
};
|
|
@@ -599,8 +593,8 @@ var WebhookdClient = class {
|
|
|
599
593
|
id: opts.idempotencyKey ?? (0, import_node_crypto3.randomUUID)(),
|
|
600
594
|
eventType,
|
|
601
595
|
payload,
|
|
602
|
-
|
|
603
|
-
|
|
596
|
+
// null = the workspace's default project (the field is omitted from the publish body on drain).
|
|
597
|
+
projectId: normalizeProjectId(opts.projectId),
|
|
604
598
|
source: opts.source ?? null,
|
|
605
599
|
createdAt: now,
|
|
606
600
|
attempts: 0,
|
|
@@ -627,10 +621,9 @@ var WebhookdClient = class {
|
|
|
627
621
|
for (const record of rows) {
|
|
628
622
|
const body = {
|
|
629
623
|
event_type: record.eventType,
|
|
630
|
-
payload: record.payload
|
|
631
|
-
environment: record.environment,
|
|
632
|
-
application: record.application
|
|
624
|
+
payload: record.payload
|
|
633
625
|
};
|
|
626
|
+
if (record.projectId !== null) body.project_id = record.projectId;
|
|
634
627
|
if (record.source !== null) body.source = record.source;
|
|
635
628
|
try {
|
|
636
629
|
await this.request("POST", "/v1/events", {
|
|
@@ -690,18 +683,16 @@ var WebhookdClient = class {
|
|
|
690
683
|
}
|
|
691
684
|
requireStore() {
|
|
692
685
|
if (!this.store) {
|
|
693
|
-
throw new
|
|
686
|
+
throw new WebhooksError("no outbox store configured \u2014 pass `store` in ClientOptions");
|
|
694
687
|
}
|
|
695
688
|
return this.store;
|
|
696
689
|
}
|
|
697
690
|
// ── Endpoints ──────────────────────────────────────────────────────────────
|
|
698
691
|
/** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
|
|
699
692
|
async createEndpoint(url, opts = {}) {
|
|
700
|
-
const body = {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
application: opts.application ?? "default"
|
|
704
|
-
};
|
|
693
|
+
const body = { url };
|
|
694
|
+
const projectId = normalizeProjectId(opts.projectId);
|
|
695
|
+
if (projectId !== null) body.project_id = projectId;
|
|
705
696
|
if (opts.subscriptions !== void 0) body.subscriptions = opts.subscriptions;
|
|
706
697
|
if (opts.secret !== void 0) body.secret = opts.secret;
|
|
707
698
|
if (opts.maxAttempts !== void 0) body.max_attempts = opts.maxAttempts;
|
|
@@ -711,9 +702,11 @@ var WebhookdClient = class {
|
|
|
711
702
|
if (opts.deliveryTimeoutMs !== void 0) body.delivery_timeout_ms = opts.deliveryTimeoutMs;
|
|
712
703
|
return this.requestJson("POST", "/v1/endpoints", { body });
|
|
713
704
|
}
|
|
714
|
-
/** List endpoints for
|
|
705
|
+
/** List endpoints for a project. Omit `projectId` for the workspace's default project. */
|
|
715
706
|
async listEndpoints(opts = {}) {
|
|
716
|
-
const query = {
|
|
707
|
+
const query = {};
|
|
708
|
+
const projectId = normalizeProjectId(opts.projectId);
|
|
709
|
+
if (projectId !== null) query.project_id = projectId;
|
|
717
710
|
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
718
711
|
if (opts.limit !== void 0) query.limit = opts.limit;
|
|
719
712
|
return this.requestJson("GET", "/v1/endpoints", { query });
|
|
@@ -787,7 +780,7 @@ var WebhookdClient = class {
|
|
|
787
780
|
/**
|
|
788
781
|
* The single request path used by every method: builds the URL (+ query), attaches auth, and runs
|
|
789
782
|
* the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
|
|
790
|
-
* backoff), raising a typed {@link
|
|
783
|
+
* backoff), raising a typed {@link WebhooksApiError} on other 4xx/5xx. The caller reads the body.
|
|
791
784
|
*/
|
|
792
785
|
async request(method, path, opts = {}) {
|
|
793
786
|
let url = `${this.baseUrl}${path}`;
|
|
@@ -824,7 +817,7 @@ var WebhookdClient = class {
|
|
|
824
817
|
await sleep(backoffMs(attempt));
|
|
825
818
|
continue;
|
|
826
819
|
}
|
|
827
|
-
throw new
|
|
820
|
+
throw new WebhooksError(`request failed: ${String(err)}`);
|
|
828
821
|
}
|
|
829
822
|
if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {
|
|
830
823
|
await sleep(retryAfterMs(resp) ?? backoffMs(attempt));
|
|
@@ -833,9 +826,12 @@ var WebhookdClient = class {
|
|
|
833
826
|
if (resp.status >= 400) throw await apiError(resp);
|
|
834
827
|
return resp;
|
|
835
828
|
}
|
|
836
|
-
throw new
|
|
829
|
+
throw new WebhooksError(`request failed after retries: ${String(lastErr)}`);
|
|
837
830
|
}
|
|
838
831
|
};
|
|
832
|
+
function normalizeProjectId(projectId) {
|
|
833
|
+
return projectId === void 0 || projectId === null || projectId === "" ? null : projectId;
|
|
834
|
+
}
|
|
839
835
|
function backoffMs(attempt) {
|
|
840
836
|
return Math.min(2e3, 200 * 2 ** attempt);
|
|
841
837
|
}
|
|
@@ -854,14 +850,14 @@ async function apiError(resp) {
|
|
|
854
850
|
message = data.error?.message ?? message;
|
|
855
851
|
} catch {
|
|
856
852
|
}
|
|
857
|
-
return new
|
|
853
|
+
return new WebhooksApiError(resp.status, code, message);
|
|
858
854
|
}
|
|
859
855
|
function sleep(ms) {
|
|
860
856
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
861
857
|
}
|
|
862
858
|
|
|
863
859
|
// src/index.ts
|
|
864
|
-
var VERSION = "0.
|
|
860
|
+
var VERSION = "0.5.0";
|
|
865
861
|
// Annotate the CommonJS export names for ESM import in node:
|
|
866
862
|
0 && (module.exports = {
|
|
867
863
|
DEAD_NEXT_ATTEMPT_MS,
|
|
@@ -872,9 +868,9 @@ var VERSION = "0.3.0";
|
|
|
872
868
|
RedisStore,
|
|
873
869
|
SqliteStore,
|
|
874
870
|
VERSION,
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
871
|
+
WebhooksApiError,
|
|
872
|
+
WebhooksClient,
|
|
873
|
+
WebhooksError,
|
|
878
874
|
isDead,
|
|
879
875
|
sign,
|
|
880
876
|
verify
|