@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 +73 -2
- package/dist/index.cjs +593 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -13
- package/dist/index.d.ts +265 -13
- package/dist/index.js +582 -13
- package/dist/index.js.map +1 -1
- package/package.json +22 -3
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 };
|