@nimbusnexus/webhooks-sdk 0.2.0 → 0.4.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 CHANGED
@@ -35,15 +35,84 @@ 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
 
59
+ ## Outbox / durable buffering (producers)
60
+
61
+ `publish()` calls webhookd synchronously — if webhookd is unreachable it rejects and the event is
62
+ lost. The **write-first outbox** decouples the two: `enqueue()` durably persists the event to a
63
+ pluggable `Store` and resolves IMMEDIATELY (no network); `drain()` (or a background drainer) ships the
64
+ buffered events later. Every send carries `Idempotency-Key = record.id`, so a re-drain after a crash
65
+ or a lost response never double-publishes — webhookd dedupes. Delivery is **at-least-once**: nothing
66
+ is lost while webhookd is down.
67
+
68
+ ```ts
69
+ import { WebhookdClient, SqliteStore } from "@nimbusnexus/webhooks-sdk";
70
+
71
+ // 1. Configure a durable store (survives process restarts; needs Node >= 22.5 for node:sqlite).
72
+ const store = new SqliteStore("outbox.db");
73
+
74
+ const wh = new WebhookdClient({
75
+ baseUrl: "https://webhooks.example.com",
76
+ apiKey: "whsk_…",
77
+ store,
78
+ });
79
+
80
+ // 2. enqueue() instead of publish() — writes to the store and resolves at once, NO network call.
81
+ const { id } = await wh.enqueue("order.created", { orderId: "ord_123", total: 4200 });
82
+
83
+ // 3a. Drain on demand (resolves to { sent, failed, remaining }):
84
+ await wh.drain();
85
+
86
+ // 3b. …or run a background drainer that calls drain() every 5s until you stop it.
87
+ wh.startDrainer(5);
88
+ // ... your app keeps enqueuing; the drainer ships in the background ...
89
+ wh.stopDrainer();
90
+ ```
91
+
92
+ **Idempotency guarantee.** `id` is the `idempotencyKey` you pass (or a generated UUID v4) and becomes
93
+ the `Idempotency-Key` header on every delivery attempt for that record. If the process crashes after
94
+ a send but before the response is recorded, the next `drain()` re-sends with the *same* key and
95
+ webhookd returns the original event without re-fanning-out. A record that keeps failing is retried
96
+ with capped exponential backoff up to `maxAttempts` (default 10), then parked **dead** (never retried
97
+ again, retrievable via `store.listDead()`) and passed to the optional `onDead` callback.
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
+
103
+ **Built-in stores** — pass one as `store` in `ClientOptions`:
104
+
105
+ | Store | Durable? | Extra needed |
106
+ | --- | --- | --- |
107
+ | `MemoryStore` | No (in-process) | — (built-in) |
108
+ | `FileStore(dir)` | Yes (per-record JSON files) | — (built-in) |
109
+ | `SqliteStore(path)` | Yes (transactional) | — (built-in `node:sqlite`, Node ≥ 22.5) |
110
+ | `RedisStore({ url })` | Yes | `npm install redis` |
111
+ | `PostgresStore({ connectionString })` | Yes | `npm install pg` |
112
+
113
+ The core SDK stays zero-dependency; `redis` / `pg` are `optionalDependencies`, imported lazily only
114
+ when you construct `RedisStore` / `PostgresStore`.
115
+
47
116
  ## Manage endpoints, keys & deliveries (operators)
48
117
 
49
118
  The same client wraps the control-plane API — register receivers, mint keys, and drain the
@@ -58,13 +127,15 @@ const wh = new WebhookdClient({ baseUrl: "https://webhooks.example.com", apiKey:
58
127
 
59
128
  // --- Endpoints ---------------------------------------------------------------
60
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.
61
131
  const ep = await wh.createEndpoint("https://your-app.example/webhooks", {
62
132
  subscriptions: [{ match_kind: "prefix", pattern: "order." }],
63
133
  description: "orders service",
64
134
  });
65
135
  const { id: endpointId, secret: signingSecret } = ep;
66
136
 
67
- await wh.listEndpoints({ environment: "prod" }); // { items, next_offset }
137
+ await wh.listEndpoints(); // default project — { items, next_offset }
138
+ await wh.listEndpoints({ projectId: "prj_3f9a…" }); // a specific project, by id
68
139
  await wh.getEndpoint(endpointId);
69
140
 
70
141
  // PATCH — send only the keys you want to change (omitted = unchanged, null = cleared):