@nimbusnexus/webhooks-sdk 0.2.0 → 0.3.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 +53 -0
- package/dist/index.cjs +585 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +237 -2
- package/dist/index.d.ts +237 -2
- package/dist/index.js +574 -1
- 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,6 +20,171 @@ 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
|
+
environment: string;
|
|
109
|
+
application: string;
|
|
110
|
+
source: string | null;
|
|
111
|
+
/** Epoch ms the record was first enqueued; `listPending` orders by this, oldest first. */
|
|
112
|
+
createdAt: number;
|
|
113
|
+
/** Delivery attempts made so far; starts at 0. */
|
|
114
|
+
attempts: number;
|
|
115
|
+
lastError: string | null;
|
|
116
|
+
/** Epoch ms the record next becomes due; initially `now`. `DEAD_NEXT_ATTEMPT_MS` when dead. */
|
|
117
|
+
nextAttemptAt: number;
|
|
118
|
+
}
|
|
119
|
+
/** Whether a record has been parked as dead (retry budget exhausted). */
|
|
120
|
+
declare function isDead(record: OutboxRecord): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* A durable buffer of pending events. Implementations may be sync or async; every method returns a
|
|
123
|
+
* value or a promise, and callers always `await`. Built-ins: {@link MemoryStore}, {@link FileStore},
|
|
124
|
+
* {@link SqliteStore}, and (optional-dep) `RedisStore` / `PostgresStore`.
|
|
125
|
+
*/
|
|
126
|
+
interface Store {
|
|
127
|
+
/** Insert-or-update by `record.id`. Idempotent: the same id overwrites, so enqueue is safe to repeat. */
|
|
128
|
+
save(record: OutboxRecord): void | Promise<void>;
|
|
129
|
+
/** Records not yet sent whose `nextAttemptAt <= now`, oldest first (by `createdAt`), capped at `limit`. */
|
|
130
|
+
listPending(limit: number): OutboxRecord[] | Promise<OutboxRecord[]>;
|
|
131
|
+
/** Remove (or flag sent) a record after a 2xx. */
|
|
132
|
+
markSent(id: string): void | Promise<void>;
|
|
133
|
+
/** Persist a failure + schedule the next retry (or park it dead via `DEAD_NEXT_ATTEMPT_MS`). */
|
|
134
|
+
markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void | Promise<void>;
|
|
135
|
+
/** Count of records still in the store (i.e. not yet sent), dead ones included. */
|
|
136
|
+
size(): number | Promise<number>;
|
|
137
|
+
/** Records parked dead, oldest first. */
|
|
138
|
+
listDead(limit?: number): OutboxRecord[] | Promise<OutboxRecord[]>;
|
|
139
|
+
/** Release any resources (file handles, DB connections, timers). */
|
|
140
|
+
close(): void | Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
/** In-process, non-durable store. The default for tests and single-process best-effort buffering. */
|
|
143
|
+
declare class MemoryStore implements Store {
|
|
144
|
+
private readonly records;
|
|
145
|
+
save(record: OutboxRecord): void;
|
|
146
|
+
listPending(limit: number): OutboxRecord[];
|
|
147
|
+
markSent(id: string): void;
|
|
148
|
+
markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void;
|
|
149
|
+
size(): number;
|
|
150
|
+
listDead(limit?: number): OutboxRecord[];
|
|
151
|
+
close(): void;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Durable store backed by a directory of per-record JSON files with atomic writes (write-tmp +
|
|
155
|
+
* rename). Survives process restarts. `markSent` unlinks the file. Suitable for a single process; it
|
|
156
|
+
* does not coordinate concurrent drainers across processes.
|
|
157
|
+
*/
|
|
158
|
+
declare class FileStore implements Store {
|
|
159
|
+
private readonly dir;
|
|
160
|
+
constructor(dir: string);
|
|
161
|
+
/** Map an arbitrary id to a safe filename (ids may be caller-supplied idempotency keys). */
|
|
162
|
+
private pathFor;
|
|
163
|
+
private readAll;
|
|
164
|
+
save(record: OutboxRecord): void;
|
|
165
|
+
listPending(limit: number): OutboxRecord[];
|
|
166
|
+
markSent(id: string): void;
|
|
167
|
+
markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void;
|
|
168
|
+
size(): number;
|
|
169
|
+
listDead(limit?: number): OutboxRecord[];
|
|
170
|
+
close(): void;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Durable, transactional store on the built-in `node:sqlite` (Node >= 22.5 — no native dependency).
|
|
174
|
+
* `markSent` deletes the row. Pass a file path to persist across restarts, or `":memory:"` for tests.
|
|
175
|
+
*/
|
|
176
|
+
declare class SqliteStore implements Store {
|
|
177
|
+
private readonly db;
|
|
178
|
+
constructor(path?: string);
|
|
179
|
+
save(record: OutboxRecord): void;
|
|
180
|
+
listPending(limit: number): OutboxRecord[];
|
|
181
|
+
markSent(id: string): void;
|
|
182
|
+
markFailed(id: string, error: string, attempts: number, nextAttemptAt: number): void;
|
|
183
|
+
size(): number;
|
|
184
|
+
listDead(limit?: number): OutboxRecord[];
|
|
185
|
+
close(): void;
|
|
186
|
+
}
|
|
187
|
+
|
|
20
188
|
/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */
|
|
21
189
|
interface WebhookdEvent {
|
|
22
190
|
id: string;
|
|
@@ -91,6 +259,40 @@ interface ClientOptions {
|
|
|
91
259
|
maxRetries?: number;
|
|
92
260
|
/** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */
|
|
93
261
|
fetchImpl?: typeof fetch;
|
|
262
|
+
/**
|
|
263
|
+
* Durable outbox store. When set, {@link WebhookdClient.enqueue} / {@link WebhookdClient.drain}
|
|
264
|
+
* (and the background drainer) become available. Omit to use only the live {@link WebhookdClient.publish}.
|
|
265
|
+
*/
|
|
266
|
+
store?: Store;
|
|
267
|
+
/** Max delivery attempts before a record is parked dead (default 10). */
|
|
268
|
+
maxAttempts?: number;
|
|
269
|
+
/** How many records a single {@link WebhookdClient.drain} pulls from the store (default 100). */
|
|
270
|
+
drainBatchLimit?: number;
|
|
271
|
+
/** Invoked once when `drain` parks a record dead (retry budget exhausted). */
|
|
272
|
+
onDead?: (record: OutboxRecord) => void;
|
|
273
|
+
/** Invoked when a background-drainer tick throws (a foreground `drain` still rejects normally). */
|
|
274
|
+
onDrainError?: (error: unknown) => void;
|
|
275
|
+
}
|
|
276
|
+
interface EnqueueOptions {
|
|
277
|
+
environment?: string;
|
|
278
|
+
application?: string;
|
|
279
|
+
source?: string;
|
|
280
|
+
/** The Idempotency-Key / record id. Defaults to a generated UUID v4. Re-enqueuing the same key is a no-op overwrite. */
|
|
281
|
+
idempotencyKey?: string;
|
|
282
|
+
}
|
|
283
|
+
interface DrainOptions {
|
|
284
|
+
/** Override the client's `drainBatchLimit` for this call. */
|
|
285
|
+
batchLimit?: number;
|
|
286
|
+
/** Override the client's `maxAttempts` for this call. */
|
|
287
|
+
maxAttempts?: number;
|
|
288
|
+
}
|
|
289
|
+
interface DrainResult {
|
|
290
|
+
/** Records delivered (2xx) and marked sent this drain. */
|
|
291
|
+
sent: number;
|
|
292
|
+
/** Records that failed this drain (rescheduled or newly parked dead). */
|
|
293
|
+
failed: number;
|
|
294
|
+
/** Records still buffered in the store afterwards (`store.size()`). */
|
|
295
|
+
remaining: number;
|
|
94
296
|
}
|
|
95
297
|
interface PublishOptions {
|
|
96
298
|
environment?: string;
|
|
@@ -149,8 +351,41 @@ declare class WebhookdClient {
|
|
|
149
351
|
private readonly timeoutMs;
|
|
150
352
|
private readonly maxRetries;
|
|
151
353
|
private readonly fetchImpl;
|
|
354
|
+
private readonly store?;
|
|
355
|
+
private readonly maxAttempts;
|
|
356
|
+
private readonly drainBatchLimit;
|
|
357
|
+
private readonly onDead?;
|
|
358
|
+
private readonly onDrainError?;
|
|
359
|
+
private drainTimer?;
|
|
360
|
+
private draining;
|
|
152
361
|
constructor(opts: ClientOptions);
|
|
153
362
|
publish(eventType: string, payload: Record<string, unknown>, opts?: PublishOptions): Promise<WebhookdEvent>;
|
|
363
|
+
/**
|
|
364
|
+
* Durably buffer an event and return IMMEDIATELY — NO network call. Builds an {@link OutboxRecord}
|
|
365
|
+
* (id = `opts.idempotencyKey` or a fresh UUID v4), saves it to the configured store, and returns the
|
|
366
|
+
* id. Ship it later with {@link drain} (or the background drainer). Requires a `store` in options.
|
|
367
|
+
*/
|
|
368
|
+
enqueue(eventType: string, payload: Record<string, unknown>, opts?: EnqueueOptions): Promise<{
|
|
369
|
+
id: string;
|
|
370
|
+
}>;
|
|
371
|
+
/**
|
|
372
|
+
* Send buffered records to webhookd. Pulls a due batch (oldest first), POSTs each to `/v1/events`
|
|
373
|
+
* with header `Idempotency-Key = record.id` (so a re-drain after a crash never double-publishes —
|
|
374
|
+
* webhookd dedupes). On 2xx the record is marked sent; on error `attempts` is bumped and the record
|
|
375
|
+
* is rescheduled with capped exponential backoff, or parked dead once it hits `maxAttempts` (the
|
|
376
|
+
* `onDead` hook fires, and it is retrievable via `store.listDead()`). Requires a `store`.
|
|
377
|
+
*/
|
|
378
|
+
drain(opts?: DrainOptions): Promise<DrainResult>;
|
|
379
|
+
/**
|
|
380
|
+
* Start a background loop calling {@link drain} every `intervalSeconds`. Overlapping ticks are
|
|
381
|
+
* skipped while a drain is in flight, and a tick that throws is swallowed (routed to `onDrainError`)
|
|
382
|
+
* so the loop keeps running. The timer is `unref`'d so it never blocks process exit. Idempotent.
|
|
383
|
+
*/
|
|
384
|
+
startDrainer(intervalSeconds: number): void;
|
|
385
|
+
/** Stop the background drainer started by {@link startDrainer}. Safe to call when not running. */
|
|
386
|
+
stopDrainer(): void;
|
|
387
|
+
private drainTick;
|
|
388
|
+
private requireStore;
|
|
154
389
|
/** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
|
|
155
390
|
createEndpoint(url: string, opts?: CreateEndpointOptions): Promise<Endpoint>;
|
|
156
391
|
/** List endpoints for an environment (defaults to `prod`). */
|
|
@@ -208,6 +443,6 @@ declare class WebhookdApiError extends WebhookdError {
|
|
|
208
443
|
* - `WebhookdClient` — publish events to webhookd (for producers).
|
|
209
444
|
*/
|
|
210
445
|
|
|
211
|
-
declare const VERSION = "0.
|
|
446
|
+
declare const VERSION = "0.3.0";
|
|
212
447
|
|
|
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 };
|
|
448
|
+
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 };
|