@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/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { Pool } from 'pg';
2
+ import { createClient } from 'redis';
3
+
1
4
  declare const DEFAULT_TOLERANCE_SECONDS = 300;
2
5
  /** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */
3
6
  declare function sign(secret: string | Buffer, rawBody: string | Buffer, timestamp?: number | null): string;
@@ -17,13 +20,182 @@ interface VerifyOptions {
17
20
  */
18
21
  declare function verify(secret: string | Buffer, rawBody: string | Buffer, signature: string, opts?: VerifyOptions): boolean;
19
22
 
23
+ /**
24
+ * Redis-backed {@link Store}. Durable ordering via a sorted set scored on `nextAttemptAt` (for due
25
+ * filtering) plus a hash of record bodies keyed by id. The `redis` driver is an OPTIONAL dependency,
26
+ * imported lazily inside {@link RedisStore.ensure} — the SDK core stays zero-runtime-dependency and
27
+ * importing this module never pulls in `redis` unless you actually construct the store.
28
+ */
29
+
30
+ type RedisClient = ReturnType<typeof createClient>;
31
+ interface RedisStoreOptions {
32
+ /** Redis connection URL, e.g. `redis://localhost:6379`. Ignored if `client` is supplied. */
33
+ url?: string;
34
+ /** Reuse an already-created (not necessarily connected) `redis` client instead of `url`. */
35
+ client?: RedisClient;
36
+ /** Namespace for the two keys this store uses. Default `webhookd:outbox`. */
37
+ keyPrefix?: string;
38
+ }
39
+ declare class RedisStore implements Store {
40
+ private readonly url?;
41
+ private readonly keyPrefix;
42
+ private client;
43
+ private connecting;
44
+ constructor(opts?: RedisStoreOptions);
45
+ private get zsetKey();
46
+ private get hashKey();
47
+ /** Lazily import the driver + connect exactly once. */
48
+ private ensure;
49
+ save(record: OutboxRecord): Promise<void>;
50
+ listPending(limit: number): Promise<OutboxRecord[]>;
51
+ markSent(id: string): Promise<void>;
52
+ markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): Promise<void>;
53
+ size(): Promise<number>;
54
+ listDead(limit?: number): Promise<OutboxRecord[]>;
55
+ close(): Promise<void>;
56
+ /** Load records for `ids` from the hash, drop any missing, sort oldest-first, cap at `limit`. */
57
+ private loadSorted;
58
+ }
59
+
60
+ /**
61
+ * Postgres-backed {@link Store}. A single `webhookd_outbox` table with a `sent` flag; upsert on `id`;
62
+ * pending = `WHERE NOT sent AND next_attempt_at <= now`. The `pg` driver is an OPTIONAL dependency,
63
+ * imported lazily inside {@link PostgresStore.ensure} — importing this module never pulls in `pg`
64
+ * unless you actually construct the store.
65
+ */
66
+
67
+ interface PostgresStoreOptions {
68
+ /** Postgres connection string, e.g. `postgres://user:pass@host:5432/db`. Ignored if `pool` is given. */
69
+ connectionString?: string;
70
+ /** Reuse an existing `pg` Pool instead of `connectionString`. */
71
+ pool?: Pool;
72
+ /** Table name (must be a plain identifier). Default `webhookd_outbox`. */
73
+ table?: string;
74
+ }
75
+ declare class PostgresStore implements Store {
76
+ private readonly connectionString?;
77
+ private readonly table;
78
+ private pool;
79
+ private ready;
80
+ constructor(opts?: PostgresStoreOptions);
81
+ /** Lazily import the driver, open the pool, and create the table exactly once. */
82
+ private ensure;
83
+ save(record: OutboxRecord): Promise<void>;
84
+ listPending(limit: number): Promise<OutboxRecord[]>;
85
+ markSent(id: string): Promise<void>;
86
+ markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): Promise<void>;
87
+ size(): Promise<number>;
88
+ listDead(limit?: number): Promise<OutboxRecord[]>;
89
+ close(): Promise<void>;
90
+ }
91
+
92
+ /**
93
+ * A sentinel `nextAttemptAt` (JS max timestamp) used to park a record that has exhausted its retry
94
+ * budget. It is never `<= now`, so {@link Store.listPending} never returns it again — the record
95
+ * stays durably in the store, flagged dead, retrievable via {@link Store.listDead}.
96
+ */
97
+ declare const DEAD_NEXT_ATTEMPT_MS = 8640000000000000;
98
+ /**
99
+ * A single buffered event. `id` doubles as the webhookd `Idempotency-Key`, so re-saving the same
100
+ * `id` (an idempotent enqueue) simply overwrites, and re-draining after a crash never double-sends.
101
+ * Timestamps are epoch milliseconds.
102
+ */
103
+ interface OutboxRecord {
104
+ /** The Idempotency-Key — caller-supplied or a generated UUID v4. */
105
+ id: string;
106
+ eventType: string;
107
+ payload: Record<string, unknown>;
108
+ /**
109
+ * The id of the project the event is published into (`prj_…`), or `null` for the workspace's default
110
+ * project. `null` means the `project_id` field is OMITTED from the publish body — the id is opaque
111
+ * and per-workspace, so only the server can resolve the default.
112
+ */
113
+ projectId: string | null;
114
+ source: string | null;
115
+ /** Epoch ms the record was first enqueued; `listPending` orders by this, oldest first. */
116
+ createdAt: number;
117
+ /** Delivery attempts made so far; starts at 0. */
118
+ attempts: number;
119
+ lastError: string | null;
120
+ /** Epoch ms the record next becomes due; initially `now`. `DEAD_NEXT_ATTEMPT_MS` when dead. */
121
+ nextAttemptAt: number;
122
+ }
123
+ /** Whether a record has been parked as dead (retry budget exhausted). */
124
+ declare function isDead(record: OutboxRecord): boolean;
125
+ /**
126
+ * A durable buffer of pending events. Implementations may be sync or async; every method returns a
127
+ * value or a promise, and callers always `await`. Built-ins: {@link MemoryStore}, {@link FileStore},
128
+ * {@link SqliteStore}, and (optional-dep) `RedisStore` / `PostgresStore`.
129
+ */
130
+ interface Store {
131
+ /** Insert-or-update by `record.id`. Idempotent: the same id overwrites, so enqueue is safe to repeat. */
132
+ save(record: OutboxRecord): void | Promise<void>;
133
+ /** Records not yet sent whose `nextAttemptAt <= now`, oldest first (by `createdAt`), capped at `limit`. */
134
+ listPending(limit: number): OutboxRecord[] | Promise<OutboxRecord[]>;
135
+ /** Remove (or flag sent) a record after a 2xx. */
136
+ markSent(id: string): void | Promise<void>;
137
+ /** Persist a failure + schedule the next retry (or park it dead via `DEAD_NEXT_ATTEMPT_MS`). */
138
+ markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void | Promise<void>;
139
+ /** Count of records still in the store (i.e. not yet sent), dead ones included. */
140
+ size(): number | Promise<number>;
141
+ /** Records parked dead, oldest first. */
142
+ listDead(limit?: number): OutboxRecord[] | Promise<OutboxRecord[]>;
143
+ /** Release any resources (file handles, DB connections, timers). */
144
+ close(): void | Promise<void>;
145
+ }
146
+ /** In-process, non-durable store. The default for tests and single-process best-effort buffering. */
147
+ declare class MemoryStore implements Store {
148
+ private readonly records;
149
+ save(record: OutboxRecord): void;
150
+ listPending(limit: number): OutboxRecord[];
151
+ markSent(id: string): void;
152
+ markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void;
153
+ size(): number;
154
+ listDead(limit?: number): OutboxRecord[];
155
+ close(): void;
156
+ }
157
+ /**
158
+ * Durable store backed by a directory of per-record JSON files with atomic writes (write-tmp +
159
+ * rename). Survives process restarts. `markSent` unlinks the file. Suitable for a single process; it
160
+ * does not coordinate concurrent drainers across processes.
161
+ */
162
+ declare class FileStore implements Store {
163
+ private readonly dir;
164
+ constructor(dir: string);
165
+ /** Map an arbitrary id to a safe filename (ids may be caller-supplied idempotency keys). */
166
+ private pathFor;
167
+ private readAll;
168
+ save(record: OutboxRecord): void;
169
+ listPending(limit: number): OutboxRecord[];
170
+ markSent(id: string): void;
171
+ markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void;
172
+ size(): number;
173
+ listDead(limit?: number): OutboxRecord[];
174
+ close(): void;
175
+ }
176
+ /**
177
+ * Durable, transactional store on the built-in `node:sqlite` (Node >= 22.5 — no native dependency).
178
+ * `markSent` deletes the row. Pass a file path to persist across restarts, or `":memory:"` for tests.
179
+ */
180
+ declare class SqliteStore implements Store {
181
+ private readonly db;
182
+ constructor(path?: string);
183
+ save(record: OutboxRecord): void;
184
+ listPending(limit: number): OutboxRecord[];
185
+ markSent(id: string): void;
186
+ markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void;
187
+ size(): number;
188
+ listDead(limit?: number): OutboxRecord[];
189
+ close(): void;
190
+ }
191
+
20
192
  /** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */
21
193
  interface WebhookdEvent {
22
194
  id: string;
23
195
  eventUid: string;
24
196
  eventType: string;
25
- application: string;
26
- environment: string;
197
+ /** The id of the project the event was published into (always resolved server-side). */
198
+ projectId: string;
27
199
  deliveriesCreated: number;
28
200
  source: string | null;
29
201
  }
@@ -40,8 +212,8 @@ interface Subscription {
40
212
  interface Endpoint {
41
213
  id: string;
42
214
  url: string;
43
- environment: string;
44
- application: string;
215
+ /** The id of the project the endpoint belongs to. */
216
+ project_id: string;
45
217
  status: string;
46
218
  subscriptions: Subscription[];
47
219
  /** The signing secret — returned ONCE, on create + rotate-secret only. */
@@ -85,24 +257,67 @@ interface Page<T> {
85
257
  }
86
258
  interface ClientOptions {
87
259
  baseUrl: string;
88
- /** A per-tenant API key (`whsk_…`) or a service token. */
260
+ /** A per-workspace API key (`whsk_…`) or a service token. */
89
261
  apiKey: string;
90
262
  timeoutMs?: number;
91
263
  maxRetries?: number;
92
264
  /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */
93
265
  fetchImpl?: typeof fetch;
266
+ /**
267
+ * Durable outbox store. When set, {@link WebhookdClient.enqueue} / {@link WebhookdClient.drain}
268
+ * (and the background drainer) become available. Omit to use only the live {@link WebhookdClient.publish}.
269
+ */
270
+ store?: Store;
271
+ /** Max delivery attempts before a record is parked dead (default 10). */
272
+ maxAttempts?: number;
273
+ /** How many records a single {@link WebhookdClient.drain} pulls from the store (default 100). */
274
+ drainBatchLimit?: number;
275
+ /** Invoked once when `drain` parks a record dead (retry budget exhausted). */
276
+ onDead?: (record: OutboxRecord) => void;
277
+ /** Invoked when a background-drainer tick throws (a foreground `drain` still rejects normally). */
278
+ onDrainError?: (error: unknown) => void;
279
+ }
280
+ interface EnqueueOptions {
281
+ /**
282
+ * The id of the project to publish into (`prj_…`). Omit (or pass an empty string) to target the
283
+ * workspace's default project — the server resolves it, there is no client-side sentinel.
284
+ */
285
+ projectId?: string;
286
+ source?: string;
287
+ /** The Idempotency-Key / record id. Defaults to a generated UUID v4. Re-enqueuing the same key is a no-op overwrite. */
288
+ idempotencyKey?: string;
289
+ }
290
+ interface DrainOptions {
291
+ /** Override the client's `drainBatchLimit` for this call. */
292
+ batchLimit?: number;
293
+ /** Override the client's `maxAttempts` for this call. */
294
+ maxAttempts?: number;
295
+ }
296
+ interface DrainResult {
297
+ /** Records delivered (2xx) and marked sent this drain. */
298
+ sent: number;
299
+ /** Records that failed this drain (rescheduled or newly parked dead). */
300
+ failed: number;
301
+ /** Records still buffered in the store afterwards (`store.size()`). */
302
+ remaining: number;
94
303
  }
95
304
  interface PublishOptions {
96
- environment?: string;
97
- application?: string;
305
+ /**
306
+ * The id of the project to publish into (`prj_…`). Omit (or pass an empty string) to target the
307
+ * workspace's default project — the server resolves it, there is no client-side sentinel.
308
+ */
309
+ projectId?: string;
98
310
  source?: string;
99
311
  /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */
100
312
  idempotencyKey?: string;
101
313
  }
102
314
  interface CreateEndpointOptions {
103
- environment?: string;
315
+ /**
316
+ * The id of the project to create the endpoint in (`prj_…`). Omit (or pass an empty string) for
317
+ * the workspace's default project.
318
+ */
319
+ projectId?: string;
104
320
  subscriptions?: Subscription[];
105
- application?: string;
106
321
  secret?: string;
107
322
  maxAttempts?: number;
108
323
  retrySchedule?: number[];
@@ -111,7 +326,11 @@ interface CreateEndpointOptions {
111
326
  deliveryTimeoutMs?: number;
112
327
  }
113
328
  interface ListEndpointsOptions {
114
- environment?: string;
329
+ /**
330
+ * The id of the project to list endpoints for (`prj_…`). Omit (or pass an empty string) for the
331
+ * workspace's default project.
332
+ */
333
+ projectId?: string;
115
334
  offset?: number;
116
335
  limit?: number;
117
336
  }
@@ -149,11 +368,44 @@ declare class WebhookdClient {
149
368
  private readonly timeoutMs;
150
369
  private readonly maxRetries;
151
370
  private readonly fetchImpl;
371
+ private readonly store?;
372
+ private readonly maxAttempts;
373
+ private readonly drainBatchLimit;
374
+ private readonly onDead?;
375
+ private readonly onDrainError?;
376
+ private drainTimer?;
377
+ private draining;
152
378
  constructor(opts: ClientOptions);
153
379
  publish(eventType: string, payload: Record<string, unknown>, opts?: PublishOptions): Promise<WebhookdEvent>;
380
+ /**
381
+ * Durably buffer an event and return IMMEDIATELY — NO network call. Builds an {@link OutboxRecord}
382
+ * (id = `opts.idempotencyKey` or a fresh UUID v4), saves it to the configured store, and returns the
383
+ * id. Ship it later with {@link drain} (or the background drainer). Requires a `store` in options.
384
+ */
385
+ enqueue(eventType: string, payload: Record<string, unknown>, opts?: EnqueueOptions): Promise<{
386
+ id: string;
387
+ }>;
388
+ /**
389
+ * Send buffered records to webhookd. Pulls a due batch (oldest first), POSTs each to `/v1/events`
390
+ * with header `Idempotency-Key = record.id` (so a re-drain after a crash never double-publishes —
391
+ * webhookd dedupes). On 2xx the record is marked sent; on error `attempts` is bumped and the record
392
+ * is rescheduled with capped exponential backoff, or parked dead once it hits `maxAttempts` (the
393
+ * `onDead` hook fires, and it is retrievable via `store.listDead()`). Requires a `store`.
394
+ */
395
+ drain(opts?: DrainOptions): Promise<DrainResult>;
396
+ /**
397
+ * Start a background loop calling {@link drain} every `intervalSeconds`. Overlapping ticks are
398
+ * skipped while a drain is in flight, and a tick that throws is swallowed (routed to `onDrainError`)
399
+ * so the loop keeps running. The timer is `unref`'d so it never blocks process exit. Idempotent.
400
+ */
401
+ startDrainer(intervalSeconds: number): void;
402
+ /** Stop the background drainer started by {@link startDrainer}. Safe to call when not running. */
403
+ stopDrainer(): void;
404
+ private drainTick;
405
+ private requireStore;
154
406
  /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
155
407
  createEndpoint(url: string, opts?: CreateEndpointOptions): Promise<Endpoint>;
156
- /** List endpoints for an environment (defaults to `prod`). */
408
+ /** List endpoints for a project. Omit `projectId` for the workspace's default project. */
157
409
  listEndpoints(opts?: ListEndpointsOptions): Promise<Page<Endpoint>>;
158
410
  /** Fetch a single endpoint by id. */
159
411
  getEndpoint(id: string): Promise<Endpoint>;
@@ -208,6 +460,6 @@ declare class WebhookdApiError extends WebhookdError {
208
460
  * - `WebhookdClient` — publish events to webhookd (for producers).
209
461
  */
210
462
 
211
- declare const VERSION = "0.2.0";
463
+ declare const VERSION = "0.4.0";
212
464
 
213
- export { type ApiKey, type ClientOptions, type CreateApiKeyOptions, type CreateEndpointOptions, DEFAULT_TOLERANCE_SECONDS, type Delivery, type Endpoint, type EndpointPatch, type ListDeliveriesOptions, type ListEndpointsOptions, type Page, type PublishOptions, type Subscription, VERSION, type VerifyOptions, WebhookdApiError, WebhookdClient, WebhookdError, type WebhookdEvent, sign, verify };
465
+ export { type ApiKey, type ClientOptions, type CreateApiKeyOptions, type CreateEndpointOptions, DEAD_NEXT_ATTEMPT_MS, DEFAULT_TOLERANCE_SECONDS, type Delivery, type DrainOptions, type DrainResult, type Endpoint, type EndpointPatch, type EnqueueOptions, FileStore, type ListDeliveriesOptions, type ListEndpointsOptions, MemoryStore, type OutboxRecord, type Page, PostgresStore, type PostgresStoreOptions, type PublishOptions, RedisStore, type RedisStoreOptions, SqliteStore, type Store, type Subscription, VERSION, type VerifyOptions, WebhookdApiError, WebhookdClient, WebhookdError, type WebhookdEvent, isDead, sign, verify };