@nimbusnexus/webhooks-sdk 0.1.0 → 0.2.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 +43 -2
- package/dist/index.cjs +128 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -3
- package/dist/index.d.ts +135 -3
- package/dist/index.js +128 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @nimbusnexus/webhooks-sdk (TypeScript)
|
|
2
2
|
|
|
3
|
-
Official TypeScript SDK for **NimbusNexus Webhooks** — publish events,
|
|
4
|
-
receive. Zero runtime dependencies (uses the built-in `fetch`
|
|
3
|
+
Official TypeScript SDK for **NimbusNexus Webhooks** — publish events, manage your endpoints / keys /
|
|
4
|
+
deliveries, and verify the webhooks you receive. Zero runtime dependencies (uses the built-in `fetch`
|
|
5
|
+
and `node:crypto`); Node ≥ 20.
|
|
5
6
|
|
|
6
7
|
```sh
|
|
7
8
|
npm install @nimbusnexus/webhooks-sdk
|
|
@@ -43,6 +44,46 @@ try {
|
|
|
43
44
|
Transient failures (network errors, `429`, `5xx`) are retried with backoff (a `429` honours
|
|
44
45
|
`Retry-After`); other `4xx` throw `WebhookdApiError` carrying the `{error:{code,message}}` envelope.
|
|
45
46
|
|
|
47
|
+
## Manage endpoints, keys & deliveries (operators)
|
|
48
|
+
|
|
49
|
+
The same client wraps the control-plane API — register receivers, mint keys, and drain the
|
|
50
|
+
dead-letter queue from code (needs an **admin**-scoped key). Management methods return the API's
|
|
51
|
+
snake_case JSON through typed interfaces (`Endpoint`, `ApiKey`, `Delivery`, `Page<T>`); list methods
|
|
52
|
+
return `{ items, next_offset }`; `deleteEndpoint` / `revokeApiKey` resolve to `void` (a `204`).
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { WebhookdClient } from "@nimbusnexus/webhooks-sdk";
|
|
56
|
+
|
|
57
|
+
const wh = new WebhookdClient({ baseUrl: "https://webhooks.example.com", apiKey: "whsk_admin_…" });
|
|
58
|
+
|
|
59
|
+
// --- Endpoints ---------------------------------------------------------------
|
|
60
|
+
// Create a receiver — its signing secret is in the response exactly once, so persist it now.
|
|
61
|
+
const ep = await wh.createEndpoint("https://your-app.example/webhooks", {
|
|
62
|
+
subscriptions: [{ match_kind: "prefix", pattern: "order." }],
|
|
63
|
+
description: "orders service",
|
|
64
|
+
});
|
|
65
|
+
const { id: endpointId, secret: signingSecret } = ep;
|
|
66
|
+
|
|
67
|
+
await wh.listEndpoints({ environment: "prod" }); // { items, next_offset }
|
|
68
|
+
await wh.getEndpoint(endpointId);
|
|
69
|
+
|
|
70
|
+
// PATCH — send only the keys you want to change (omitted = unchanged, null = cleared):
|
|
71
|
+
await wh.updateEndpoint(endpointId, { max_attempts: 10, status: "disabled" });
|
|
72
|
+
|
|
73
|
+
await wh.rotateEndpointSecret(endpointId); // returns the new secret, once
|
|
74
|
+
await wh.enableEndpoint(endpointId); // recover an auto-disabled endpoint
|
|
75
|
+
await wh.deleteEndpoint(endpointId); // -> void (204)
|
|
76
|
+
|
|
77
|
+
// --- API keys ----------------------------------------------------------------
|
|
78
|
+
const key = await wh.createApiKey({ name: "ci-publisher", scope: "publish", expiresInDays: 90 });
|
|
79
|
+
console.log(key.key); // shown once
|
|
80
|
+
await wh.revokeApiKey(key.id); // -> void (204)
|
|
81
|
+
|
|
82
|
+
// --- Deliveries / dead-letter recovery ---------------------------------------
|
|
83
|
+
const dead = await wh.listDeliveries({ status: "dead" });
|
|
84
|
+
for (const d of dead.items) await wh.redeliver(d.id);
|
|
85
|
+
```
|
|
86
|
+
|
|
46
87
|
## Develop
|
|
47
88
|
|
|
48
89
|
```sh
|
package/dist/index.cjs
CHANGED
|
@@ -54,8 +54,12 @@ function verify(secret, rawBody, signature, opts = {}) {
|
|
|
54
54
|
if (Math.abs(current - ts) > toleranceSeconds) return false;
|
|
55
55
|
}
|
|
56
56
|
const expected = Buffer.from(sign(secret, rawBody, ts));
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
for (const raw of signature.split(",")) {
|
|
58
|
+
const token = raw.trim();
|
|
59
|
+
const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);
|
|
60
|
+
if (expected.length === candidate.length && (0, import_node_crypto.timingSafeEqual)(expected, candidate)) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// src/errors.ts
|
|
@@ -99,12 +103,9 @@ var WebhookdClient = class {
|
|
|
99
103
|
application: opts.application ?? "default"
|
|
100
104
|
};
|
|
101
105
|
if (opts.source !== void 0) body.source = opts.source;
|
|
102
|
-
const headers = {
|
|
103
|
-
"Content-Type": "application/json",
|
|
104
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
105
|
-
};
|
|
106
|
+
const headers = {};
|
|
106
107
|
if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
107
|
-
const resp = await this.
|
|
108
|
+
const resp = await this.request("POST", "/v1/events", { body, headers });
|
|
108
109
|
const data = await resp.json();
|
|
109
110
|
return {
|
|
110
111
|
id: String(data.id),
|
|
@@ -116,14 +117,126 @@ var WebhookdClient = class {
|
|
|
116
117
|
source: data.source ?? null
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
// ── Endpoints ──────────────────────────────────────────────────────────────
|
|
121
|
+
/** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
|
|
122
|
+
async createEndpoint(url, opts = {}) {
|
|
123
|
+
const body = {
|
|
124
|
+
url,
|
|
125
|
+
environment: opts.environment ?? "prod",
|
|
126
|
+
application: opts.application ?? "default"
|
|
127
|
+
};
|
|
128
|
+
if (opts.subscriptions !== void 0) body.subscriptions = opts.subscriptions;
|
|
129
|
+
if (opts.secret !== void 0) body.secret = opts.secret;
|
|
130
|
+
if (opts.maxAttempts !== void 0) body.max_attempts = opts.maxAttempts;
|
|
131
|
+
if (opts.retrySchedule !== void 0) body.retry_schedule = opts.retrySchedule;
|
|
132
|
+
if (opts.description !== void 0) body.description = opts.description;
|
|
133
|
+
if (opts.customHeaders !== void 0) body.custom_headers = opts.customHeaders;
|
|
134
|
+
if (opts.deliveryTimeoutMs !== void 0) body.delivery_timeout_ms = opts.deliveryTimeoutMs;
|
|
135
|
+
return this.requestJson("POST", "/v1/endpoints", { body });
|
|
136
|
+
}
|
|
137
|
+
/** List endpoints for an environment (defaults to `prod`). */
|
|
138
|
+
async listEndpoints(opts = {}) {
|
|
139
|
+
const query = { environment: opts.environment ?? "prod" };
|
|
140
|
+
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
141
|
+
if (opts.limit !== void 0) query.limit = opts.limit;
|
|
142
|
+
return this.requestJson("GET", "/v1/endpoints", { query });
|
|
143
|
+
}
|
|
144
|
+
/** Fetch a single endpoint by id. */
|
|
145
|
+
async getEndpoint(id) {
|
|
146
|
+
return this.requestJson("GET", `/v1/endpoints/${encodeURIComponent(id)}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left
|
|
150
|
+
* unchanged, an explicit `null` clears the field. No defaults are injected.
|
|
151
|
+
*/
|
|
152
|
+
async updateEndpoint(id, patch) {
|
|
153
|
+
return this.requestJson("PATCH", `/v1/endpoints/${encodeURIComponent(id)}`, {
|
|
154
|
+
body: patch
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */
|
|
158
|
+
async deleteEndpoint(id) {
|
|
159
|
+
await this.request("DELETE", `/v1/endpoints/${encodeURIComponent(id)}`);
|
|
160
|
+
}
|
|
161
|
+
/** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */
|
|
162
|
+
async rotateEndpointSecret(id) {
|
|
163
|
+
return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);
|
|
164
|
+
}
|
|
165
|
+
/** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */
|
|
166
|
+
async enableEndpoint(id) {
|
|
167
|
+
return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/enable`);
|
|
168
|
+
}
|
|
169
|
+
// ── API keys ───────────────────────────────────────────────────────────────
|
|
170
|
+
/** Create an API key. The response includes the raw `key` exactly once — persist it. */
|
|
171
|
+
async createApiKey(opts = {}) {
|
|
172
|
+
const body = {
|
|
173
|
+
name: opts.name ?? "",
|
|
174
|
+
scope: opts.scope ?? "admin"
|
|
175
|
+
};
|
|
176
|
+
if (opts.expiresInDays !== void 0) body.expires_in_days = opts.expiresInDays;
|
|
177
|
+
return this.requestJson("POST", "/v1/api-keys", { body });
|
|
178
|
+
}
|
|
179
|
+
/** Revoke an API key. Returns nothing (the server replies `204 No Content`). */
|
|
180
|
+
async revokeApiKey(id) {
|
|
181
|
+
await this.request("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`);
|
|
182
|
+
}
|
|
183
|
+
// ── Deliveries ─────────────────────────────────────────────────────────────
|
|
184
|
+
/** List deliveries. Only the filters you provide are sent as query params. */
|
|
185
|
+
async listDeliveries(opts = {}) {
|
|
186
|
+
const query = {};
|
|
187
|
+
if (opts.status !== void 0) query.status = opts.status;
|
|
188
|
+
if (opts.endpointId !== void 0) query.endpoint_id = opts.endpointId;
|
|
189
|
+
if (opts.eventType !== void 0) query.event_type = opts.eventType;
|
|
190
|
+
if (opts.since !== void 0) query.since = opts.since;
|
|
191
|
+
if (opts.until !== void 0) query.until = opts.until;
|
|
192
|
+
if (opts.q !== void 0) query.q = opts.q;
|
|
193
|
+
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
194
|
+
if (opts.limit !== void 0) query.limit = opts.limit;
|
|
195
|
+
return this.requestJson("GET", "/v1/deliveries", { query });
|
|
196
|
+
}
|
|
197
|
+
/** Re-queue a delivery for another attempt. */
|
|
198
|
+
async redeliver(deliveryId) {
|
|
199
|
+
return this.requestJson(
|
|
200
|
+
"POST",
|
|
201
|
+
`/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
// ── Internals ──────────────────────────────────────────────────────────────
|
|
205
|
+
/** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */
|
|
206
|
+
async requestJson(method, path, opts = {}) {
|
|
207
|
+
const resp = await this.request(method, path, opts);
|
|
208
|
+
return await resp.json();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The single request path used by every method: builds the URL (+ query), attaches auth, and runs
|
|
212
|
+
* the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
|
|
213
|
+
* backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.
|
|
214
|
+
*/
|
|
215
|
+
async request(method, path, opts = {}) {
|
|
216
|
+
let url = `${this.baseUrl}${path}`;
|
|
217
|
+
if (opts.query) {
|
|
218
|
+
const qs = new URLSearchParams();
|
|
219
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
220
|
+
if (value !== void 0 && value !== null) qs.append(key, String(value));
|
|
221
|
+
}
|
|
222
|
+
const suffix = qs.toString();
|
|
223
|
+
if (suffix) url += `?${suffix}`;
|
|
224
|
+
}
|
|
225
|
+
const headers = {
|
|
226
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
227
|
+
...opts.headers
|
|
228
|
+
};
|
|
229
|
+
let body;
|
|
230
|
+
if (opts.body !== void 0) {
|
|
231
|
+
body = JSON.stringify(opts.body);
|
|
232
|
+
headers["Content-Type"] = "application/json";
|
|
233
|
+
}
|
|
121
234
|
let lastErr;
|
|
122
235
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
123
236
|
let resp;
|
|
124
237
|
try {
|
|
125
238
|
resp = await this.fetchImpl(url, {
|
|
126
|
-
method
|
|
239
|
+
method,
|
|
127
240
|
headers,
|
|
128
241
|
body,
|
|
129
242
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -155,14 +268,14 @@ function retryAfterMs(resp) {
|
|
|
155
268
|
return null;
|
|
156
269
|
}
|
|
157
270
|
async function apiError(resp) {
|
|
271
|
+
const text = await resp.text();
|
|
158
272
|
let code = "error";
|
|
159
|
-
let message =
|
|
273
|
+
let message = text;
|
|
160
274
|
try {
|
|
161
|
-
const data =
|
|
275
|
+
const data = JSON.parse(text);
|
|
162
276
|
code = data.error?.code ?? code;
|
|
163
|
-
message = data.error?.message ??
|
|
277
|
+
message = data.error?.message ?? message;
|
|
164
278
|
} catch {
|
|
165
|
-
message = "";
|
|
166
279
|
}
|
|
167
280
|
return new WebhookdApiError(resp.status, code, message);
|
|
168
281
|
}
|
|
@@ -171,7 +284,7 @@ function sleep(ms) {
|
|
|
171
284
|
}
|
|
172
285
|
|
|
173
286
|
// src/index.ts
|
|
174
|
-
var VERSION = "0.
|
|
287
|
+
var VERSION = "0.2.0";
|
|
175
288
|
// Annotate the CommonJS export names for ESM import in node:
|
|
176
289
|
0 && (module.exports = {
|
|
177
290
|
DEFAULT_TOLERANCE_SECONDS,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/signature.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type { ClientOptions, PublishOptions, WebhookdEvent } from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nexport const VERSION = \"0.1.0\";\n","/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n const candidate = Buffer.from(signature.startsWith(PREFIX) ? signature : `${PREFIX}${signature}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n return expected.length === candidate.length && timingSafeEqual(expected, candidate);\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n };\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.post(\"/v1/events\", JSON.stringify(body), headers);\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n private async post(\n path: string,\n body: string,\n headers: Record<string, string>,\n ): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n let code = \"error\";\n let message = \"\";\n try {\n const data = (await resp.json()) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? \"\";\n } catch {\n message = \"\";\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/signature.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type {\n ClientOptions,\n PublishOptions,\n WebhookdEvent,\n Subscription,\n Endpoint,\n ApiKey,\n Delivery,\n Page,\n CreateEndpointOptions,\n ListEndpointsOptions,\n EndpointPatch,\n CreateApiKeyOptions,\n ListDeliveriesOptions,\n} from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\n// Keep in lockstep with package.json + the release tag (see publish.yml's bump checklist).\nexport const VERSION = \"0.2.0\";\n","/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n // X-Webhook-Signature carries one token normally, or several comma-separated tokens during a\n // signing-secret rotation (webhookd dual-sign overlap) — accept if ANY token verifies, so a\n // subscriber configured with EITHER the current or the previous secret keeps working.\n for (const raw of signature.split(\",\")) {\n const token = raw.trim();\n const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) return true;\n }\n return false;\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish + management client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\n/** A subscription filter attached to an endpoint. */\nexport interface Subscription {\n match_kind: \"exact\" | \"prefix\" | \"suffix\" | \"all\";\n pattern: string;\n}\n\n/**\n * An endpoint, as returned by the management endpoints (webhookd's `EndpointOut`).\n * Fields mirror the server's snake_case wire shape verbatim. `secret` is present ONLY on the\n * create + rotate-secret responses (returned exactly once).\n */\nexport interface Endpoint {\n id: string;\n url: string;\n environment: string;\n application: string;\n status: string;\n subscriptions: Subscription[];\n /** The signing secret — returned ONCE, on create + rotate-secret only. */\n secret?: string;\n max_attempts?: number;\n retry_schedule?: number[];\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * An API key, as returned by `POST /v1/api-keys` (webhookd's `ApiKeyOut`). `key` is present ONLY on\n * the create response (returned exactly once).\n */\nexport interface ApiKey {\n id: string;\n name: string;\n scope: string;\n /** The raw key — returned ONCE, on create only. */\n key?: string;\n created_at?: string;\n expires_at?: string | null;\n}\n\n/** A delivery attempt record, as returned by the deliveries endpoints (webhookd's `DeliveryOut`). */\nexport interface Delivery {\n id: string;\n endpoint_id: string;\n event_id?: string;\n event_type?: string;\n status: string;\n attempts?: number;\n created_at?: string;\n updated_at?: string;\n}\n\n/** A single page of a list endpoint — items plus the cursor for the next page (`null` at the end). */\nexport interface Page<T> {\n items: T[];\n next_offset: number | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport interface CreateEndpointOptions {\n environment?: string;\n subscriptions?: Subscription[];\n application?: string;\n secret?: string;\n maxAttempts?: number;\n retrySchedule?: number[];\n description?: string;\n customHeaders?: Record<string, string>;\n deliveryTimeoutMs?: number;\n}\n\nexport interface ListEndpointsOptions {\n environment?: string;\n offset?: number;\n limit?: number;\n}\n\n/**\n * A raw PATCH mapping for `updateEndpoint`, keyed with the server's snake_case names. PATCH semantics:\n * an OMITTED key is left unchanged; an explicit `null` CLEARS the field. The object is sent verbatim.\n */\nexport interface EndpointPatch {\n url?: string;\n max_attempts?: number | null;\n retry_schedule?: number[] | null;\n status?: \"enabled\" | \"disabled\";\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n}\n\nexport interface CreateApiKeyOptions {\n name?: string;\n scope?: \"admin\" | \"publish\";\n expiresInDays?: number;\n}\n\nexport interface ListDeliveriesOptions {\n status?: \"queued\" | \"sending\" | \"sent\" | \"failed\" | \"dead\";\n endpointId?: string;\n eventType?: string;\n since?: string | number;\n until?: string | number;\n q?: string;\n offset?: number;\n limit?: number;\n}\n\ninterface RequestOptions {\n /** JSON body to send; omitted for GET/DELETE and bodyless POSTs (no `Content-Type` is set). */\n body?: unknown;\n /** Query params; `undefined`/`null` values are dropped so only provided params are sent. */\n query?: Record<string, unknown>;\n /** Extra request headers (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {};\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.request(\"POST\", \"/v1/events\", { body, headers });\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n // ── Endpoints ──────────────────────────────────────────────────────────────\n\n /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */\n async createEndpoint(url: string, opts: CreateEndpointOptions = {}): Promise<Endpoint> {\n const body: Record<string, unknown> = {\n url,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.subscriptions !== undefined) body.subscriptions = opts.subscriptions;\n if (opts.secret !== undefined) body.secret = opts.secret;\n if (opts.maxAttempts !== undefined) body.max_attempts = opts.maxAttempts;\n if (opts.retrySchedule !== undefined) body.retry_schedule = opts.retrySchedule;\n if (opts.description !== undefined) body.description = opts.description;\n if (opts.customHeaders !== undefined) body.custom_headers = opts.customHeaders;\n if (opts.deliveryTimeoutMs !== undefined) body.delivery_timeout_ms = opts.deliveryTimeoutMs;\n return this.requestJson<Endpoint>(\"POST\", \"/v1/endpoints\", { body });\n }\n\n /** List endpoints for an environment (defaults to `prod`). */\n async listEndpoints(opts: ListEndpointsOptions = {}): Promise<Page<Endpoint>> {\n const query: Record<string, unknown> = { environment: opts.environment ?? \"prod\" };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Endpoint>>(\"GET\", \"/v1/endpoints\", { query });\n }\n\n /** Fetch a single endpoint by id. */\n async getEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"GET\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /**\n * Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left\n * unchanged, an explicit `null` clears the field. No defaults are injected.\n */\n async updateEndpoint(id: string, patch: EndpointPatch): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"PATCH\", `/v1/endpoints/${encodeURIComponent(id)}`, {\n body: patch,\n });\n }\n\n /** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */\n async deleteEndpoint(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */\n async rotateEndpointSecret(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);\n }\n\n /** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */\n async enableEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/enable`);\n }\n\n // ── API keys ───────────────────────────────────────────────────────────────\n\n /** Create an API key. The response includes the raw `key` exactly once — persist it. */\n async createApiKey(opts: CreateApiKeyOptions = {}): Promise<ApiKey> {\n const body: Record<string, unknown> = {\n name: opts.name ?? \"\",\n scope: opts.scope ?? \"admin\",\n };\n if (opts.expiresInDays !== undefined) body.expires_in_days = opts.expiresInDays;\n return this.requestJson<ApiKey>(\"POST\", \"/v1/api-keys\", { body });\n }\n\n /** Revoke an API key. Returns nothing (the server replies `204 No Content`). */\n async revokeApiKey(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/api-keys/${encodeURIComponent(id)}`);\n }\n\n // ── Deliveries ─────────────────────────────────────────────────────────────\n\n /** List deliveries. Only the filters you provide are sent as query params. */\n async listDeliveries(opts: ListDeliveriesOptions = {}): Promise<Page<Delivery>> {\n const query: Record<string, unknown> = {};\n if (opts.status !== undefined) query.status = opts.status;\n if (opts.endpointId !== undefined) query.endpoint_id = opts.endpointId;\n if (opts.eventType !== undefined) query.event_type = opts.eventType;\n if (opts.since !== undefined) query.since = opts.since;\n if (opts.until !== undefined) query.until = opts.until;\n if (opts.q !== undefined) query.q = opts.q;\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Delivery>>(\"GET\", \"/v1/deliveries\", { query });\n }\n\n /** Re-queue a delivery for another attempt. */\n async redeliver(deliveryId: string): Promise<Delivery> {\n return this.requestJson<Delivery>(\n \"POST\",\n `/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`,\n );\n }\n\n // ── Internals ──────────────────────────────────────────────────────────────\n\n /** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */\n private async requestJson<T>(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<T> {\n const resp = await this.request(method, path, opts);\n return (await resp.json()) as T;\n }\n\n /**\n * The single request path used by every method: builds the URL (+ query), attaches auth, and runs\n * the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential\n * backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.\n */\n private async request(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<Response> {\n let url = `${this.baseUrl}${path}`;\n if (opts.query) {\n const qs = new URLSearchParams();\n for (const [key, value] of Object.entries(opts.query)) {\n if (value !== undefined && value !== null) qs.append(key, String(value));\n }\n const suffix = qs.toString();\n if (suffix) url += `?${suffix}`;\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n ...opts.headers,\n };\n let body: string | undefined;\n if (opts.body !== undefined) {\n body = JSON.stringify(opts.body);\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n // Read the body once; a non-envelope (or non-JSON) error body falls back to the raw text.\n const text = await resp.text();\n let code = \"error\";\n let message = text;\n try {\n const data = JSON.parse(text) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? message;\n } catch {\n // not JSON — keep the raw text as the message\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -27,6 +27,62 @@ interface WebhookdEvent {
|
|
|
27
27
|
deliveriesCreated: number;
|
|
28
28
|
source: string | null;
|
|
29
29
|
}
|
|
30
|
+
/** A subscription filter attached to an endpoint. */
|
|
31
|
+
interface Subscription {
|
|
32
|
+
match_kind: "exact" | "prefix" | "suffix" | "all";
|
|
33
|
+
pattern: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An endpoint, as returned by the management endpoints (webhookd's `EndpointOut`).
|
|
37
|
+
* Fields mirror the server's snake_case wire shape verbatim. `secret` is present ONLY on the
|
|
38
|
+
* create + rotate-secret responses (returned exactly once).
|
|
39
|
+
*/
|
|
40
|
+
interface Endpoint {
|
|
41
|
+
id: string;
|
|
42
|
+
url: string;
|
|
43
|
+
environment: string;
|
|
44
|
+
application: string;
|
|
45
|
+
status: string;
|
|
46
|
+
subscriptions: Subscription[];
|
|
47
|
+
/** The signing secret — returned ONCE, on create + rotate-secret only. */
|
|
48
|
+
secret?: string;
|
|
49
|
+
max_attempts?: number;
|
|
50
|
+
retry_schedule?: number[];
|
|
51
|
+
description?: string | null;
|
|
52
|
+
custom_headers?: Record<string, string> | null;
|
|
53
|
+
delivery_timeout_ms?: number | null;
|
|
54
|
+
created_at?: string;
|
|
55
|
+
updated_at?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* An API key, as returned by `POST /v1/api-keys` (webhookd's `ApiKeyOut`). `key` is present ONLY on
|
|
59
|
+
* the create response (returned exactly once).
|
|
60
|
+
*/
|
|
61
|
+
interface ApiKey {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
scope: string;
|
|
65
|
+
/** The raw key — returned ONCE, on create only. */
|
|
66
|
+
key?: string;
|
|
67
|
+
created_at?: string;
|
|
68
|
+
expires_at?: string | null;
|
|
69
|
+
}
|
|
70
|
+
/** A delivery attempt record, as returned by the deliveries endpoints (webhookd's `DeliveryOut`). */
|
|
71
|
+
interface Delivery {
|
|
72
|
+
id: string;
|
|
73
|
+
endpoint_id: string;
|
|
74
|
+
event_id?: string;
|
|
75
|
+
event_type?: string;
|
|
76
|
+
status: string;
|
|
77
|
+
attempts?: number;
|
|
78
|
+
created_at?: string;
|
|
79
|
+
updated_at?: string;
|
|
80
|
+
}
|
|
81
|
+
/** A single page of a list endpoint — items plus the cursor for the next page (`null` at the end). */
|
|
82
|
+
interface Page<T> {
|
|
83
|
+
items: T[];
|
|
84
|
+
next_offset: number | null;
|
|
85
|
+
}
|
|
30
86
|
interface ClientOptions {
|
|
31
87
|
baseUrl: string;
|
|
32
88
|
/** A per-tenant API key (`whsk_…`) or a service token. */
|
|
@@ -43,6 +99,50 @@ interface PublishOptions {
|
|
|
43
99
|
/** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */
|
|
44
100
|
idempotencyKey?: string;
|
|
45
101
|
}
|
|
102
|
+
interface CreateEndpointOptions {
|
|
103
|
+
environment?: string;
|
|
104
|
+
subscriptions?: Subscription[];
|
|
105
|
+
application?: string;
|
|
106
|
+
secret?: string;
|
|
107
|
+
maxAttempts?: number;
|
|
108
|
+
retrySchedule?: number[];
|
|
109
|
+
description?: string;
|
|
110
|
+
customHeaders?: Record<string, string>;
|
|
111
|
+
deliveryTimeoutMs?: number;
|
|
112
|
+
}
|
|
113
|
+
interface ListEndpointsOptions {
|
|
114
|
+
environment?: string;
|
|
115
|
+
offset?: number;
|
|
116
|
+
limit?: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A raw PATCH mapping for `updateEndpoint`, keyed with the server's snake_case names. PATCH semantics:
|
|
120
|
+
* an OMITTED key is left unchanged; an explicit `null` CLEARS the field. The object is sent verbatim.
|
|
121
|
+
*/
|
|
122
|
+
interface EndpointPatch {
|
|
123
|
+
url?: string;
|
|
124
|
+
max_attempts?: number | null;
|
|
125
|
+
retry_schedule?: number[] | null;
|
|
126
|
+
status?: "enabled" | "disabled";
|
|
127
|
+
description?: string | null;
|
|
128
|
+
custom_headers?: Record<string, string> | null;
|
|
129
|
+
delivery_timeout_ms?: number | null;
|
|
130
|
+
}
|
|
131
|
+
interface CreateApiKeyOptions {
|
|
132
|
+
name?: string;
|
|
133
|
+
scope?: "admin" | "publish";
|
|
134
|
+
expiresInDays?: number;
|
|
135
|
+
}
|
|
136
|
+
interface ListDeliveriesOptions {
|
|
137
|
+
status?: "queued" | "sending" | "sent" | "failed" | "dead";
|
|
138
|
+
endpointId?: string;
|
|
139
|
+
eventType?: string;
|
|
140
|
+
since?: string | number;
|
|
141
|
+
until?: string | number;
|
|
142
|
+
q?: string;
|
|
143
|
+
offset?: number;
|
|
144
|
+
limit?: number;
|
|
145
|
+
}
|
|
46
146
|
declare class WebhookdClient {
|
|
47
147
|
private readonly baseUrl;
|
|
48
148
|
private readonly apiKey;
|
|
@@ -51,7 +151,39 @@ declare class WebhookdClient {
|
|
|
51
151
|
private readonly fetchImpl;
|
|
52
152
|
constructor(opts: ClientOptions);
|
|
53
153
|
publish(eventType: string, payload: Record<string, unknown>, opts?: PublishOptions): Promise<WebhookdEvent>;
|
|
54
|
-
|
|
154
|
+
/** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
|
|
155
|
+
createEndpoint(url: string, opts?: CreateEndpointOptions): Promise<Endpoint>;
|
|
156
|
+
/** List endpoints for an environment (defaults to `prod`). */
|
|
157
|
+
listEndpoints(opts?: ListEndpointsOptions): Promise<Page<Endpoint>>;
|
|
158
|
+
/** Fetch a single endpoint by id. */
|
|
159
|
+
getEndpoint(id: string): Promise<Endpoint>;
|
|
160
|
+
/**
|
|
161
|
+
* Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left
|
|
162
|
+
* unchanged, an explicit `null` clears the field. No defaults are injected.
|
|
163
|
+
*/
|
|
164
|
+
updateEndpoint(id: string, patch: EndpointPatch): Promise<Endpoint>;
|
|
165
|
+
/** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */
|
|
166
|
+
deleteEndpoint(id: string): Promise<void>;
|
|
167
|
+
/** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */
|
|
168
|
+
rotateEndpointSecret(id: string): Promise<Endpoint>;
|
|
169
|
+
/** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */
|
|
170
|
+
enableEndpoint(id: string): Promise<Endpoint>;
|
|
171
|
+
/** Create an API key. The response includes the raw `key` exactly once — persist it. */
|
|
172
|
+
createApiKey(opts?: CreateApiKeyOptions): Promise<ApiKey>;
|
|
173
|
+
/** Revoke an API key. Returns nothing (the server replies `204 No Content`). */
|
|
174
|
+
revokeApiKey(id: string): Promise<void>;
|
|
175
|
+
/** List deliveries. Only the filters you provide are sent as query params. */
|
|
176
|
+
listDeliveries(opts?: ListDeliveriesOptions): Promise<Page<Delivery>>;
|
|
177
|
+
/** Re-queue a delivery for another attempt. */
|
|
178
|
+
redeliver(deliveryId: string): Promise<Delivery>;
|
|
179
|
+
/** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */
|
|
180
|
+
private requestJson;
|
|
181
|
+
/**
|
|
182
|
+
* The single request path used by every method: builds the URL (+ query), attaches auth, and runs
|
|
183
|
+
* the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
|
|
184
|
+
* backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.
|
|
185
|
+
*/
|
|
186
|
+
private request;
|
|
55
187
|
}
|
|
56
188
|
|
|
57
189
|
/** Base class for all webhookd SDK errors (network failures, etc.). */
|
|
@@ -76,6 +208,6 @@ declare class WebhookdApiError extends WebhookdError {
|
|
|
76
208
|
* - `WebhookdClient` — publish events to webhookd (for producers).
|
|
77
209
|
*/
|
|
78
210
|
|
|
79
|
-
declare const VERSION = "0.
|
|
211
|
+
declare const VERSION = "0.2.0";
|
|
80
212
|
|
|
81
|
-
export { type ClientOptions, DEFAULT_TOLERANCE_SECONDS, type PublishOptions, VERSION, type VerifyOptions, WebhookdApiError, WebhookdClient, WebhookdError, type WebhookdEvent, sign, verify };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,62 @@ interface WebhookdEvent {
|
|
|
27
27
|
deliveriesCreated: number;
|
|
28
28
|
source: string | null;
|
|
29
29
|
}
|
|
30
|
+
/** A subscription filter attached to an endpoint. */
|
|
31
|
+
interface Subscription {
|
|
32
|
+
match_kind: "exact" | "prefix" | "suffix" | "all";
|
|
33
|
+
pattern: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An endpoint, as returned by the management endpoints (webhookd's `EndpointOut`).
|
|
37
|
+
* Fields mirror the server's snake_case wire shape verbatim. `secret` is present ONLY on the
|
|
38
|
+
* create + rotate-secret responses (returned exactly once).
|
|
39
|
+
*/
|
|
40
|
+
interface Endpoint {
|
|
41
|
+
id: string;
|
|
42
|
+
url: string;
|
|
43
|
+
environment: string;
|
|
44
|
+
application: string;
|
|
45
|
+
status: string;
|
|
46
|
+
subscriptions: Subscription[];
|
|
47
|
+
/** The signing secret — returned ONCE, on create + rotate-secret only. */
|
|
48
|
+
secret?: string;
|
|
49
|
+
max_attempts?: number;
|
|
50
|
+
retry_schedule?: number[];
|
|
51
|
+
description?: string | null;
|
|
52
|
+
custom_headers?: Record<string, string> | null;
|
|
53
|
+
delivery_timeout_ms?: number | null;
|
|
54
|
+
created_at?: string;
|
|
55
|
+
updated_at?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* An API key, as returned by `POST /v1/api-keys` (webhookd's `ApiKeyOut`). `key` is present ONLY on
|
|
59
|
+
* the create response (returned exactly once).
|
|
60
|
+
*/
|
|
61
|
+
interface ApiKey {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
scope: string;
|
|
65
|
+
/** The raw key — returned ONCE, on create only. */
|
|
66
|
+
key?: string;
|
|
67
|
+
created_at?: string;
|
|
68
|
+
expires_at?: string | null;
|
|
69
|
+
}
|
|
70
|
+
/** A delivery attempt record, as returned by the deliveries endpoints (webhookd's `DeliveryOut`). */
|
|
71
|
+
interface Delivery {
|
|
72
|
+
id: string;
|
|
73
|
+
endpoint_id: string;
|
|
74
|
+
event_id?: string;
|
|
75
|
+
event_type?: string;
|
|
76
|
+
status: string;
|
|
77
|
+
attempts?: number;
|
|
78
|
+
created_at?: string;
|
|
79
|
+
updated_at?: string;
|
|
80
|
+
}
|
|
81
|
+
/** A single page of a list endpoint — items plus the cursor for the next page (`null` at the end). */
|
|
82
|
+
interface Page<T> {
|
|
83
|
+
items: T[];
|
|
84
|
+
next_offset: number | null;
|
|
85
|
+
}
|
|
30
86
|
interface ClientOptions {
|
|
31
87
|
baseUrl: string;
|
|
32
88
|
/** A per-tenant API key (`whsk_…`) or a service token. */
|
|
@@ -43,6 +99,50 @@ interface PublishOptions {
|
|
|
43
99
|
/** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */
|
|
44
100
|
idempotencyKey?: string;
|
|
45
101
|
}
|
|
102
|
+
interface CreateEndpointOptions {
|
|
103
|
+
environment?: string;
|
|
104
|
+
subscriptions?: Subscription[];
|
|
105
|
+
application?: string;
|
|
106
|
+
secret?: string;
|
|
107
|
+
maxAttempts?: number;
|
|
108
|
+
retrySchedule?: number[];
|
|
109
|
+
description?: string;
|
|
110
|
+
customHeaders?: Record<string, string>;
|
|
111
|
+
deliveryTimeoutMs?: number;
|
|
112
|
+
}
|
|
113
|
+
interface ListEndpointsOptions {
|
|
114
|
+
environment?: string;
|
|
115
|
+
offset?: number;
|
|
116
|
+
limit?: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A raw PATCH mapping for `updateEndpoint`, keyed with the server's snake_case names. PATCH semantics:
|
|
120
|
+
* an OMITTED key is left unchanged; an explicit `null` CLEARS the field. The object is sent verbatim.
|
|
121
|
+
*/
|
|
122
|
+
interface EndpointPatch {
|
|
123
|
+
url?: string;
|
|
124
|
+
max_attempts?: number | null;
|
|
125
|
+
retry_schedule?: number[] | null;
|
|
126
|
+
status?: "enabled" | "disabled";
|
|
127
|
+
description?: string | null;
|
|
128
|
+
custom_headers?: Record<string, string> | null;
|
|
129
|
+
delivery_timeout_ms?: number | null;
|
|
130
|
+
}
|
|
131
|
+
interface CreateApiKeyOptions {
|
|
132
|
+
name?: string;
|
|
133
|
+
scope?: "admin" | "publish";
|
|
134
|
+
expiresInDays?: number;
|
|
135
|
+
}
|
|
136
|
+
interface ListDeliveriesOptions {
|
|
137
|
+
status?: "queued" | "sending" | "sent" | "failed" | "dead";
|
|
138
|
+
endpointId?: string;
|
|
139
|
+
eventType?: string;
|
|
140
|
+
since?: string | number;
|
|
141
|
+
until?: string | number;
|
|
142
|
+
q?: string;
|
|
143
|
+
offset?: number;
|
|
144
|
+
limit?: number;
|
|
145
|
+
}
|
|
46
146
|
declare class WebhookdClient {
|
|
47
147
|
private readonly baseUrl;
|
|
48
148
|
private readonly apiKey;
|
|
@@ -51,7 +151,39 @@ declare class WebhookdClient {
|
|
|
51
151
|
private readonly fetchImpl;
|
|
52
152
|
constructor(opts: ClientOptions);
|
|
53
153
|
publish(eventType: string, payload: Record<string, unknown>, opts?: PublishOptions): Promise<WebhookdEvent>;
|
|
54
|
-
|
|
154
|
+
/** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
|
|
155
|
+
createEndpoint(url: string, opts?: CreateEndpointOptions): Promise<Endpoint>;
|
|
156
|
+
/** List endpoints for an environment (defaults to `prod`). */
|
|
157
|
+
listEndpoints(opts?: ListEndpointsOptions): Promise<Page<Endpoint>>;
|
|
158
|
+
/** Fetch a single endpoint by id. */
|
|
159
|
+
getEndpoint(id: string): Promise<Endpoint>;
|
|
160
|
+
/**
|
|
161
|
+
* Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left
|
|
162
|
+
* unchanged, an explicit `null` clears the field. No defaults are injected.
|
|
163
|
+
*/
|
|
164
|
+
updateEndpoint(id: string, patch: EndpointPatch): Promise<Endpoint>;
|
|
165
|
+
/** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */
|
|
166
|
+
deleteEndpoint(id: string): Promise<void>;
|
|
167
|
+
/** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */
|
|
168
|
+
rotateEndpointSecret(id: string): Promise<Endpoint>;
|
|
169
|
+
/** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */
|
|
170
|
+
enableEndpoint(id: string): Promise<Endpoint>;
|
|
171
|
+
/** Create an API key. The response includes the raw `key` exactly once — persist it. */
|
|
172
|
+
createApiKey(opts?: CreateApiKeyOptions): Promise<ApiKey>;
|
|
173
|
+
/** Revoke an API key. Returns nothing (the server replies `204 No Content`). */
|
|
174
|
+
revokeApiKey(id: string): Promise<void>;
|
|
175
|
+
/** List deliveries. Only the filters you provide are sent as query params. */
|
|
176
|
+
listDeliveries(opts?: ListDeliveriesOptions): Promise<Page<Delivery>>;
|
|
177
|
+
/** Re-queue a delivery for another attempt. */
|
|
178
|
+
redeliver(deliveryId: string): Promise<Delivery>;
|
|
179
|
+
/** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */
|
|
180
|
+
private requestJson;
|
|
181
|
+
/**
|
|
182
|
+
* The single request path used by every method: builds the URL (+ query), attaches auth, and runs
|
|
183
|
+
* the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
|
|
184
|
+
* backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.
|
|
185
|
+
*/
|
|
186
|
+
private request;
|
|
55
187
|
}
|
|
56
188
|
|
|
57
189
|
/** Base class for all webhookd SDK errors (network failures, etc.). */
|
|
@@ -76,6 +208,6 @@ declare class WebhookdApiError extends WebhookdError {
|
|
|
76
208
|
* - `WebhookdClient` — publish events to webhookd (for producers).
|
|
77
209
|
*/
|
|
78
210
|
|
|
79
|
-
declare const VERSION = "0.
|
|
211
|
+
declare const VERSION = "0.2.0";
|
|
80
212
|
|
|
81
|
-
export { type ClientOptions, DEFAULT_TOLERANCE_SECONDS, type PublishOptions, VERSION, type VerifyOptions, WebhookdApiError, WebhookdClient, WebhookdError, type WebhookdEvent, sign, verify };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -22,8 +22,12 @@ function verify(secret, rawBody, signature, opts = {}) {
|
|
|
22
22
|
if (Math.abs(current - ts) > toleranceSeconds) return false;
|
|
23
23
|
}
|
|
24
24
|
const expected = Buffer.from(sign(secret, rawBody, ts));
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
for (const raw of signature.split(",")) {
|
|
26
|
+
const token = raw.trim();
|
|
27
|
+
const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);
|
|
28
|
+
if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
// src/errors.ts
|
|
@@ -67,12 +71,9 @@ var WebhookdClient = class {
|
|
|
67
71
|
application: opts.application ?? "default"
|
|
68
72
|
};
|
|
69
73
|
if (opts.source !== void 0) body.source = opts.source;
|
|
70
|
-
const headers = {
|
|
71
|
-
"Content-Type": "application/json",
|
|
72
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
73
|
-
};
|
|
74
|
+
const headers = {};
|
|
74
75
|
if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
75
|
-
const resp = await this.
|
|
76
|
+
const resp = await this.request("POST", "/v1/events", { body, headers });
|
|
76
77
|
const data = await resp.json();
|
|
77
78
|
return {
|
|
78
79
|
id: String(data.id),
|
|
@@ -84,14 +85,126 @@ var WebhookdClient = class {
|
|
|
84
85
|
source: data.source ?? null
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// ── Endpoints ──────────────────────────────────────────────────────────────
|
|
89
|
+
/** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
|
|
90
|
+
async createEndpoint(url, opts = {}) {
|
|
91
|
+
const body = {
|
|
92
|
+
url,
|
|
93
|
+
environment: opts.environment ?? "prod",
|
|
94
|
+
application: opts.application ?? "default"
|
|
95
|
+
};
|
|
96
|
+
if (opts.subscriptions !== void 0) body.subscriptions = opts.subscriptions;
|
|
97
|
+
if (opts.secret !== void 0) body.secret = opts.secret;
|
|
98
|
+
if (opts.maxAttempts !== void 0) body.max_attempts = opts.maxAttempts;
|
|
99
|
+
if (opts.retrySchedule !== void 0) body.retry_schedule = opts.retrySchedule;
|
|
100
|
+
if (opts.description !== void 0) body.description = opts.description;
|
|
101
|
+
if (opts.customHeaders !== void 0) body.custom_headers = opts.customHeaders;
|
|
102
|
+
if (opts.deliveryTimeoutMs !== void 0) body.delivery_timeout_ms = opts.deliveryTimeoutMs;
|
|
103
|
+
return this.requestJson("POST", "/v1/endpoints", { body });
|
|
104
|
+
}
|
|
105
|
+
/** List endpoints for an environment (defaults to `prod`). */
|
|
106
|
+
async listEndpoints(opts = {}) {
|
|
107
|
+
const query = { environment: opts.environment ?? "prod" };
|
|
108
|
+
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
109
|
+
if (opts.limit !== void 0) query.limit = opts.limit;
|
|
110
|
+
return this.requestJson("GET", "/v1/endpoints", { query });
|
|
111
|
+
}
|
|
112
|
+
/** Fetch a single endpoint by id. */
|
|
113
|
+
async getEndpoint(id) {
|
|
114
|
+
return this.requestJson("GET", `/v1/endpoints/${encodeURIComponent(id)}`);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left
|
|
118
|
+
* unchanged, an explicit `null` clears the field. No defaults are injected.
|
|
119
|
+
*/
|
|
120
|
+
async updateEndpoint(id, patch) {
|
|
121
|
+
return this.requestJson("PATCH", `/v1/endpoints/${encodeURIComponent(id)}`, {
|
|
122
|
+
body: patch
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */
|
|
126
|
+
async deleteEndpoint(id) {
|
|
127
|
+
await this.request("DELETE", `/v1/endpoints/${encodeURIComponent(id)}`);
|
|
128
|
+
}
|
|
129
|
+
/** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */
|
|
130
|
+
async rotateEndpointSecret(id) {
|
|
131
|
+
return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);
|
|
132
|
+
}
|
|
133
|
+
/** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */
|
|
134
|
+
async enableEndpoint(id) {
|
|
135
|
+
return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/enable`);
|
|
136
|
+
}
|
|
137
|
+
// ── API keys ───────────────────────────────────────────────────────────────
|
|
138
|
+
/** Create an API key. The response includes the raw `key` exactly once — persist it. */
|
|
139
|
+
async createApiKey(opts = {}) {
|
|
140
|
+
const body = {
|
|
141
|
+
name: opts.name ?? "",
|
|
142
|
+
scope: opts.scope ?? "admin"
|
|
143
|
+
};
|
|
144
|
+
if (opts.expiresInDays !== void 0) body.expires_in_days = opts.expiresInDays;
|
|
145
|
+
return this.requestJson("POST", "/v1/api-keys", { body });
|
|
146
|
+
}
|
|
147
|
+
/** Revoke an API key. Returns nothing (the server replies `204 No Content`). */
|
|
148
|
+
async revokeApiKey(id) {
|
|
149
|
+
await this.request("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`);
|
|
150
|
+
}
|
|
151
|
+
// ── Deliveries ─────────────────────────────────────────────────────────────
|
|
152
|
+
/** List deliveries. Only the filters you provide are sent as query params. */
|
|
153
|
+
async listDeliveries(opts = {}) {
|
|
154
|
+
const query = {};
|
|
155
|
+
if (opts.status !== void 0) query.status = opts.status;
|
|
156
|
+
if (opts.endpointId !== void 0) query.endpoint_id = opts.endpointId;
|
|
157
|
+
if (opts.eventType !== void 0) query.event_type = opts.eventType;
|
|
158
|
+
if (opts.since !== void 0) query.since = opts.since;
|
|
159
|
+
if (opts.until !== void 0) query.until = opts.until;
|
|
160
|
+
if (opts.q !== void 0) query.q = opts.q;
|
|
161
|
+
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
162
|
+
if (opts.limit !== void 0) query.limit = opts.limit;
|
|
163
|
+
return this.requestJson("GET", "/v1/deliveries", { query });
|
|
164
|
+
}
|
|
165
|
+
/** Re-queue a delivery for another attempt. */
|
|
166
|
+
async redeliver(deliveryId) {
|
|
167
|
+
return this.requestJson(
|
|
168
|
+
"POST",
|
|
169
|
+
`/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
// ── Internals ──────────────────────────────────────────────────────────────
|
|
173
|
+
/** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */
|
|
174
|
+
async requestJson(method, path, opts = {}) {
|
|
175
|
+
const resp = await this.request(method, path, opts);
|
|
176
|
+
return await resp.json();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The single request path used by every method: builds the URL (+ query), attaches auth, and runs
|
|
180
|
+
* the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
|
|
181
|
+
* backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.
|
|
182
|
+
*/
|
|
183
|
+
async request(method, path, opts = {}) {
|
|
184
|
+
let url = `${this.baseUrl}${path}`;
|
|
185
|
+
if (opts.query) {
|
|
186
|
+
const qs = new URLSearchParams();
|
|
187
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
188
|
+
if (value !== void 0 && value !== null) qs.append(key, String(value));
|
|
189
|
+
}
|
|
190
|
+
const suffix = qs.toString();
|
|
191
|
+
if (suffix) url += `?${suffix}`;
|
|
192
|
+
}
|
|
193
|
+
const headers = {
|
|
194
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
195
|
+
...opts.headers
|
|
196
|
+
};
|
|
197
|
+
let body;
|
|
198
|
+
if (opts.body !== void 0) {
|
|
199
|
+
body = JSON.stringify(opts.body);
|
|
200
|
+
headers["Content-Type"] = "application/json";
|
|
201
|
+
}
|
|
89
202
|
let lastErr;
|
|
90
203
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
91
204
|
let resp;
|
|
92
205
|
try {
|
|
93
206
|
resp = await this.fetchImpl(url, {
|
|
94
|
-
method
|
|
207
|
+
method,
|
|
95
208
|
headers,
|
|
96
209
|
body,
|
|
97
210
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -123,14 +236,14 @@ function retryAfterMs(resp) {
|
|
|
123
236
|
return null;
|
|
124
237
|
}
|
|
125
238
|
async function apiError(resp) {
|
|
239
|
+
const text = await resp.text();
|
|
126
240
|
let code = "error";
|
|
127
|
-
let message =
|
|
241
|
+
let message = text;
|
|
128
242
|
try {
|
|
129
|
-
const data =
|
|
243
|
+
const data = JSON.parse(text);
|
|
130
244
|
code = data.error?.code ?? code;
|
|
131
|
-
message = data.error?.message ??
|
|
245
|
+
message = data.error?.message ?? message;
|
|
132
246
|
} catch {
|
|
133
|
-
message = "";
|
|
134
247
|
}
|
|
135
248
|
return new WebhookdApiError(resp.status, code, message);
|
|
136
249
|
}
|
|
@@ -139,7 +252,7 @@ function sleep(ms) {
|
|
|
139
252
|
}
|
|
140
253
|
|
|
141
254
|
// src/index.ts
|
|
142
|
-
var VERSION = "0.
|
|
255
|
+
var VERSION = "0.2.0";
|
|
143
256
|
export {
|
|
144
257
|
DEFAULT_TOLERANCE_SECONDS,
|
|
145
258
|
VERSION,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/signature.ts","../src/errors.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n const candidate = Buffer.from(signature.startsWith(PREFIX) ? signature : `${PREFIX}${signature}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n return expected.length === candidate.length && timingSafeEqual(expected, candidate);\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n };\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.post(\"/v1/events\", JSON.stringify(body), headers);\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n private async post(\n path: string,\n body: string,\n headers: Record<string, string>,\n ): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n let code = \"error\";\n let message = \"\";\n try {\n const data = (await resp.json()) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? \"\";\n } catch {\n message = \"\";\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type { ClientOptions, PublishOptions, WebhookdEvent } from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nexport const VERSION = \"0.1.0\";\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/signature.ts","../src/errors.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n // X-Webhook-Signature carries one token normally, or several comma-separated tokens during a\n // signing-secret rotation (webhookd dual-sign overlap) — accept if ANY token verifies, so a\n // subscriber configured with EITHER the current or the previous secret keeps working.\n for (const raw of signature.split(\",\")) {\n const token = raw.trim();\n const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) return true;\n }\n return false;\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish + management client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\n/** A subscription filter attached to an endpoint. */\nexport interface Subscription {\n match_kind: \"exact\" | \"prefix\" | \"suffix\" | \"all\";\n pattern: string;\n}\n\n/**\n * An endpoint, as returned by the management endpoints (webhookd's `EndpointOut`).\n * Fields mirror the server's snake_case wire shape verbatim. `secret` is present ONLY on the\n * create + rotate-secret responses (returned exactly once).\n */\nexport interface Endpoint {\n id: string;\n url: string;\n environment: string;\n application: string;\n status: string;\n subscriptions: Subscription[];\n /** The signing secret — returned ONCE, on create + rotate-secret only. */\n secret?: string;\n max_attempts?: number;\n retry_schedule?: number[];\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * An API key, as returned by `POST /v1/api-keys` (webhookd's `ApiKeyOut`). `key` is present ONLY on\n * the create response (returned exactly once).\n */\nexport interface ApiKey {\n id: string;\n name: string;\n scope: string;\n /** The raw key — returned ONCE, on create only. */\n key?: string;\n created_at?: string;\n expires_at?: string | null;\n}\n\n/** A delivery attempt record, as returned by the deliveries endpoints (webhookd's `DeliveryOut`). */\nexport interface Delivery {\n id: string;\n endpoint_id: string;\n event_id?: string;\n event_type?: string;\n status: string;\n attempts?: number;\n created_at?: string;\n updated_at?: string;\n}\n\n/** A single page of a list endpoint — items plus the cursor for the next page (`null` at the end). */\nexport interface Page<T> {\n items: T[];\n next_offset: number | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport interface CreateEndpointOptions {\n environment?: string;\n subscriptions?: Subscription[];\n application?: string;\n secret?: string;\n maxAttempts?: number;\n retrySchedule?: number[];\n description?: string;\n customHeaders?: Record<string, string>;\n deliveryTimeoutMs?: number;\n}\n\nexport interface ListEndpointsOptions {\n environment?: string;\n offset?: number;\n limit?: number;\n}\n\n/**\n * A raw PATCH mapping for `updateEndpoint`, keyed with the server's snake_case names. PATCH semantics:\n * an OMITTED key is left unchanged; an explicit `null` CLEARS the field. The object is sent verbatim.\n */\nexport interface EndpointPatch {\n url?: string;\n max_attempts?: number | null;\n retry_schedule?: number[] | null;\n status?: \"enabled\" | \"disabled\";\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n}\n\nexport interface CreateApiKeyOptions {\n name?: string;\n scope?: \"admin\" | \"publish\";\n expiresInDays?: number;\n}\n\nexport interface ListDeliveriesOptions {\n status?: \"queued\" | \"sending\" | \"sent\" | \"failed\" | \"dead\";\n endpointId?: string;\n eventType?: string;\n since?: string | number;\n until?: string | number;\n q?: string;\n offset?: number;\n limit?: number;\n}\n\ninterface RequestOptions {\n /** JSON body to send; omitted for GET/DELETE and bodyless POSTs (no `Content-Type` is set). */\n body?: unknown;\n /** Query params; `undefined`/`null` values are dropped so only provided params are sent. */\n query?: Record<string, unknown>;\n /** Extra request headers (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {};\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.request(\"POST\", \"/v1/events\", { body, headers });\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n // ── Endpoints ──────────────────────────────────────────────────────────────\n\n /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */\n async createEndpoint(url: string, opts: CreateEndpointOptions = {}): Promise<Endpoint> {\n const body: Record<string, unknown> = {\n url,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.subscriptions !== undefined) body.subscriptions = opts.subscriptions;\n if (opts.secret !== undefined) body.secret = opts.secret;\n if (opts.maxAttempts !== undefined) body.max_attempts = opts.maxAttempts;\n if (opts.retrySchedule !== undefined) body.retry_schedule = opts.retrySchedule;\n if (opts.description !== undefined) body.description = opts.description;\n if (opts.customHeaders !== undefined) body.custom_headers = opts.customHeaders;\n if (opts.deliveryTimeoutMs !== undefined) body.delivery_timeout_ms = opts.deliveryTimeoutMs;\n return this.requestJson<Endpoint>(\"POST\", \"/v1/endpoints\", { body });\n }\n\n /** List endpoints for an environment (defaults to `prod`). */\n async listEndpoints(opts: ListEndpointsOptions = {}): Promise<Page<Endpoint>> {\n const query: Record<string, unknown> = { environment: opts.environment ?? \"prod\" };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Endpoint>>(\"GET\", \"/v1/endpoints\", { query });\n }\n\n /** Fetch a single endpoint by id. */\n async getEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"GET\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /**\n * Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left\n * unchanged, an explicit `null` clears the field. No defaults are injected.\n */\n async updateEndpoint(id: string, patch: EndpointPatch): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"PATCH\", `/v1/endpoints/${encodeURIComponent(id)}`, {\n body: patch,\n });\n }\n\n /** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */\n async deleteEndpoint(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */\n async rotateEndpointSecret(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);\n }\n\n /** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */\n async enableEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/enable`);\n }\n\n // ── API keys ───────────────────────────────────────────────────────────────\n\n /** Create an API key. The response includes the raw `key` exactly once — persist it. */\n async createApiKey(opts: CreateApiKeyOptions = {}): Promise<ApiKey> {\n const body: Record<string, unknown> = {\n name: opts.name ?? \"\",\n scope: opts.scope ?? \"admin\",\n };\n if (opts.expiresInDays !== undefined) body.expires_in_days = opts.expiresInDays;\n return this.requestJson<ApiKey>(\"POST\", \"/v1/api-keys\", { body });\n }\n\n /** Revoke an API key. Returns nothing (the server replies `204 No Content`). */\n async revokeApiKey(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/api-keys/${encodeURIComponent(id)}`);\n }\n\n // ── Deliveries ─────────────────────────────────────────────────────────────\n\n /** List deliveries. Only the filters you provide are sent as query params. */\n async listDeliveries(opts: ListDeliveriesOptions = {}): Promise<Page<Delivery>> {\n const query: Record<string, unknown> = {};\n if (opts.status !== undefined) query.status = opts.status;\n if (opts.endpointId !== undefined) query.endpoint_id = opts.endpointId;\n if (opts.eventType !== undefined) query.event_type = opts.eventType;\n if (opts.since !== undefined) query.since = opts.since;\n if (opts.until !== undefined) query.until = opts.until;\n if (opts.q !== undefined) query.q = opts.q;\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Delivery>>(\"GET\", \"/v1/deliveries\", { query });\n }\n\n /** Re-queue a delivery for another attempt. */\n async redeliver(deliveryId: string): Promise<Delivery> {\n return this.requestJson<Delivery>(\n \"POST\",\n `/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`,\n );\n }\n\n // ── Internals ──────────────────────────────────────────────────────────────\n\n /** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */\n private async requestJson<T>(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<T> {\n const resp = await this.request(method, path, opts);\n return (await resp.json()) as T;\n }\n\n /**\n * The single request path used by every method: builds the URL (+ query), attaches auth, and runs\n * the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential\n * backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.\n */\n private async request(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<Response> {\n let url = `${this.baseUrl}${path}`;\n if (opts.query) {\n const qs = new URLSearchParams();\n for (const [key, value] of Object.entries(opts.query)) {\n if (value !== undefined && value !== null) qs.append(key, String(value));\n }\n const suffix = qs.toString();\n if (suffix) url += `?${suffix}`;\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n ...opts.headers,\n };\n let body: string | undefined;\n if (opts.body !== undefined) {\n body = JSON.stringify(opts.body);\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n // Read the body once; a non-envelope (or non-JSON) error body falls back to the raw text.\n const text = await resp.text();\n let code = \"error\";\n let message = text;\n try {\n const data = JSON.parse(text) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? message;\n } catch {\n // not JSON — keep the raw text as the message\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type {\n ClientOptions,\n PublishOptions,\n WebhookdEvent,\n Subscription,\n Endpoint,\n ApiKey,\n Delivery,\n Page,\n CreateEndpointOptions,\n ListEndpointsOptions,\n EndpointPatch,\n CreateApiKeyOptions,\n ListDeliveriesOptions,\n} from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\n// Keep in lockstep with package.json + the release tag (see publish.yml's bump checklist).\nexport const VERSION = \"0.2.0\";\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED