@nimbusnexus/webhooks-sdk 0.1.1 → 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 +122 -13
- 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 +122 -13
- 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
|
@@ -103,12 +103,9 @@ var WebhookdClient = class {
|
|
|
103
103
|
application: opts.application ?? "default"
|
|
104
104
|
};
|
|
105
105
|
if (opts.source !== void 0) body.source = opts.source;
|
|
106
|
-
const headers = {
|
|
107
|
-
"Content-Type": "application/json",
|
|
108
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
109
|
-
};
|
|
106
|
+
const headers = {};
|
|
110
107
|
if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
111
|
-
const resp = await this.
|
|
108
|
+
const resp = await this.request("POST", "/v1/events", { body, headers });
|
|
112
109
|
const data = await resp.json();
|
|
113
110
|
return {
|
|
114
111
|
id: String(data.id),
|
|
@@ -120,14 +117,126 @@ var WebhookdClient = class {
|
|
|
120
117
|
source: data.source ?? null
|
|
121
118
|
};
|
|
122
119
|
}
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
234
|
let lastErr;
|
|
126
235
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
127
236
|
let resp;
|
|
128
237
|
try {
|
|
129
238
|
resp = await this.fetchImpl(url, {
|
|
130
|
-
method
|
|
239
|
+
method,
|
|
131
240
|
headers,
|
|
132
241
|
body,
|
|
133
242
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -159,14 +268,14 @@ function retryAfterMs(resp) {
|
|
|
159
268
|
return null;
|
|
160
269
|
}
|
|
161
270
|
async function apiError(resp) {
|
|
271
|
+
const text = await resp.text();
|
|
162
272
|
let code = "error";
|
|
163
|
-
let message =
|
|
273
|
+
let message = text;
|
|
164
274
|
try {
|
|
165
|
-
const data =
|
|
275
|
+
const data = JSON.parse(text);
|
|
166
276
|
code = data.error?.code ?? code;
|
|
167
|
-
message = data.error?.message ??
|
|
277
|
+
message = data.error?.message ?? message;
|
|
168
278
|
} catch {
|
|
169
|
-
message = "";
|
|
170
279
|
}
|
|
171
280
|
return new WebhookdApiError(resp.status, code, message);
|
|
172
281
|
}
|
|
@@ -175,7 +284,7 @@ function sleep(ms) {
|
|
|
175
284
|
}
|
|
176
285
|
|
|
177
286
|
// src/index.ts
|
|
178
|
-
var VERSION = "0.
|
|
287
|
+
var VERSION = "0.2.0";
|
|
179
288
|
// Annotate the CommonJS export names for ESM import in node:
|
|
180
289
|
0 && (module.exports = {
|
|
181
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 // 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 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
|
@@ -71,12 +71,9 @@ var WebhookdClient = class {
|
|
|
71
71
|
application: opts.application ?? "default"
|
|
72
72
|
};
|
|
73
73
|
if (opts.source !== void 0) body.source = opts.source;
|
|
74
|
-
const headers = {
|
|
75
|
-
"Content-Type": "application/json",
|
|
76
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
77
|
-
};
|
|
74
|
+
const headers = {};
|
|
78
75
|
if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
79
|
-
const resp = await this.
|
|
76
|
+
const resp = await this.request("POST", "/v1/events", { body, headers });
|
|
80
77
|
const data = await resp.json();
|
|
81
78
|
return {
|
|
82
79
|
id: String(data.id),
|
|
@@ -88,14 +85,126 @@ var WebhookdClient = class {
|
|
|
88
85
|
source: data.source ?? null
|
|
89
86
|
};
|
|
90
87
|
}
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
202
|
let lastErr;
|
|
94
203
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
95
204
|
let resp;
|
|
96
205
|
try {
|
|
97
206
|
resp = await this.fetchImpl(url, {
|
|
98
|
-
method
|
|
207
|
+
method,
|
|
99
208
|
headers,
|
|
100
209
|
body,
|
|
101
210
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -127,14 +236,14 @@ function retryAfterMs(resp) {
|
|
|
127
236
|
return null;
|
|
128
237
|
}
|
|
129
238
|
async function apiError(resp) {
|
|
239
|
+
const text = await resp.text();
|
|
130
240
|
let code = "error";
|
|
131
|
-
let message =
|
|
241
|
+
let message = text;
|
|
132
242
|
try {
|
|
133
|
-
const data =
|
|
243
|
+
const data = JSON.parse(text);
|
|
134
244
|
code = data.error?.code ?? code;
|
|
135
|
-
message = data.error?.message ??
|
|
245
|
+
message = data.error?.message ?? message;
|
|
136
246
|
} catch {
|
|
137
|
-
message = "";
|
|
138
247
|
}
|
|
139
248
|
return new WebhookdApiError(resp.status, code, message);
|
|
140
249
|
}
|
|
@@ -143,7 +252,7 @@ function sleep(ms) {
|
|
|
143
252
|
}
|
|
144
253
|
|
|
145
254
|
// src/index.ts
|
|
146
|
-
var VERSION = "0.
|
|
255
|
+
var VERSION = "0.2.0";
|
|
147
256
|
export {
|
|
148
257
|
DEFAULT_TOLERANCE_SECONDS,
|
|
149
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 // 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 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