@nimbusnexus/webhooks-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @nimbusnexus/webhooks-sdk (TypeScript)
2
2
 
3
- Official TypeScript SDK for **NimbusNexus Webhooks** — publish events, and verify the webhooks you
4
- receive. Zero runtime dependencies (uses the built-in `fetch` and `node:crypto`); Node ≥ 20.
3
+ Official TypeScript SDK for **NimbusNexus Webhooks** — publish events, manage your endpoints / keys /
4
+ deliveries, and verify the webhooks you receive. Zero runtime dependencies (uses the built-in `fetch`
5
+ and `node:crypto`); Node ≥ 20.
5
6
 
6
7
  ```sh
7
8
  npm install @nimbusnexus/webhooks-sdk
@@ -43,6 +44,46 @@ try {
43
44
  Transient failures (network errors, `429`, `5xx`) are retried with backoff (a `429` honours
44
45
  `Retry-After`); other `4xx` throw `WebhookdApiError` carrying the `{error:{code,message}}` envelope.
45
46
 
47
+ ## Manage endpoints, keys & deliveries (operators)
48
+
49
+ The same client wraps the control-plane API — register receivers, mint keys, and drain the
50
+ dead-letter queue from code (needs an **admin**-scoped key). Management methods return the API's
51
+ snake_case JSON through typed interfaces (`Endpoint`, `ApiKey`, `Delivery`, `Page<T>`); list methods
52
+ return `{ items, next_offset }`; `deleteEndpoint` / `revokeApiKey` resolve to `void` (a `204`).
53
+
54
+ ```ts
55
+ import { WebhookdClient } from "@nimbusnexus/webhooks-sdk";
56
+
57
+ const wh = new WebhookdClient({ baseUrl: "https://webhooks.example.com", apiKey: "whsk_admin_…" });
58
+
59
+ // --- Endpoints ---------------------------------------------------------------
60
+ // Create a receiver — its signing secret is in the response exactly once, so persist it now.
61
+ const ep = await wh.createEndpoint("https://your-app.example/webhooks", {
62
+ subscriptions: [{ match_kind: "prefix", pattern: "order." }],
63
+ description: "orders service",
64
+ });
65
+ const { id: endpointId, secret: signingSecret } = ep;
66
+
67
+ await wh.listEndpoints({ environment: "prod" }); // { items, next_offset }
68
+ await wh.getEndpoint(endpointId);
69
+
70
+ // PATCH — send only the keys you want to change (omitted = unchanged, null = cleared):
71
+ await wh.updateEndpoint(endpointId, { max_attempts: 10, status: "disabled" });
72
+
73
+ await wh.rotateEndpointSecret(endpointId); // returns the new secret, once
74
+ await wh.enableEndpoint(endpointId); // recover an auto-disabled endpoint
75
+ await wh.deleteEndpoint(endpointId); // -> void (204)
76
+
77
+ // --- API keys ----------------------------------------------------------------
78
+ const key = await wh.createApiKey({ name: "ci-publisher", scope: "publish", expiresInDays: 90 });
79
+ console.log(key.key); // shown once
80
+ await wh.revokeApiKey(key.id); // -> void (204)
81
+
82
+ // --- Deliveries / dead-letter recovery ---------------------------------------
83
+ const dead = await wh.listDeliveries({ status: "dead" });
84
+ for (const d of dead.items) await wh.redeliver(d.id);
85
+ ```
86
+
46
87
  ## Develop
47
88
 
48
89
  ```sh
package/dist/index.cjs CHANGED
@@ -54,8 +54,12 @@ function verify(secret, rawBody, signature, opts = {}) {
54
54
  if (Math.abs(current - ts) > toleranceSeconds) return false;
55
55
  }
56
56
  const expected = Buffer.from(sign(secret, rawBody, ts));
57
- const candidate = Buffer.from(signature.startsWith(PREFIX) ? signature : `${PREFIX}${signature}`);
58
- return expected.length === candidate.length && (0, import_node_crypto.timingSafeEqual)(expected, candidate);
57
+ for (const raw of signature.split(",")) {
58
+ const token = raw.trim();
59
+ const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);
60
+ if (expected.length === candidate.length && (0, import_node_crypto.timingSafeEqual)(expected, candidate)) return true;
61
+ }
62
+ return false;
59
63
  }
60
64
 
61
65
  // src/errors.ts
@@ -99,12 +103,9 @@ var WebhookdClient = class {
99
103
  application: opts.application ?? "default"
100
104
  };
101
105
  if (opts.source !== void 0) body.source = opts.source;
102
- const headers = {
103
- "Content-Type": "application/json",
104
- Authorization: `Bearer ${this.apiKey}`
105
- };
106
+ const headers = {};
106
107
  if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
107
- const resp = await this.post("/v1/events", JSON.stringify(body), headers);
108
+ const resp = await this.request("POST", "/v1/events", { body, headers });
108
109
  const data = await resp.json();
109
110
  return {
110
111
  id: String(data.id),
@@ -116,14 +117,126 @@ var WebhookdClient = class {
116
117
  source: data.source ?? null
117
118
  };
118
119
  }
119
- async post(path, body, headers) {
120
- const url = `${this.baseUrl}${path}`;
120
+ // ── Endpoints ──────────────────────────────────────────────────────────────
121
+ /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
122
+ async createEndpoint(url, opts = {}) {
123
+ const body = {
124
+ url,
125
+ environment: opts.environment ?? "prod",
126
+ application: opts.application ?? "default"
127
+ };
128
+ if (opts.subscriptions !== void 0) body.subscriptions = opts.subscriptions;
129
+ if (opts.secret !== void 0) body.secret = opts.secret;
130
+ if (opts.maxAttempts !== void 0) body.max_attempts = opts.maxAttempts;
131
+ if (opts.retrySchedule !== void 0) body.retry_schedule = opts.retrySchedule;
132
+ if (opts.description !== void 0) body.description = opts.description;
133
+ if (opts.customHeaders !== void 0) body.custom_headers = opts.customHeaders;
134
+ if (opts.deliveryTimeoutMs !== void 0) body.delivery_timeout_ms = opts.deliveryTimeoutMs;
135
+ return this.requestJson("POST", "/v1/endpoints", { body });
136
+ }
137
+ /** List endpoints for an environment (defaults to `prod`). */
138
+ async listEndpoints(opts = {}) {
139
+ const query = { environment: opts.environment ?? "prod" };
140
+ if (opts.offset !== void 0) query.offset = opts.offset;
141
+ if (opts.limit !== void 0) query.limit = opts.limit;
142
+ return this.requestJson("GET", "/v1/endpoints", { query });
143
+ }
144
+ /** Fetch a single endpoint by id. */
145
+ async getEndpoint(id) {
146
+ return this.requestJson("GET", `/v1/endpoints/${encodeURIComponent(id)}`);
147
+ }
148
+ /**
149
+ * Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left
150
+ * unchanged, an explicit `null` clears the field. No defaults are injected.
151
+ */
152
+ async updateEndpoint(id, patch) {
153
+ return this.requestJson("PATCH", `/v1/endpoints/${encodeURIComponent(id)}`, {
154
+ body: patch
155
+ });
156
+ }
157
+ /** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */
158
+ async deleteEndpoint(id) {
159
+ await this.request("DELETE", `/v1/endpoints/${encodeURIComponent(id)}`);
160
+ }
161
+ /** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */
162
+ async rotateEndpointSecret(id) {
163
+ return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);
164
+ }
165
+ /** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */
166
+ async enableEndpoint(id) {
167
+ return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/enable`);
168
+ }
169
+ // ── API keys ───────────────────────────────────────────────────────────────
170
+ /** Create an API key. The response includes the raw `key` exactly once — persist it. */
171
+ async createApiKey(opts = {}) {
172
+ const body = {
173
+ name: opts.name ?? "",
174
+ scope: opts.scope ?? "admin"
175
+ };
176
+ if (opts.expiresInDays !== void 0) body.expires_in_days = opts.expiresInDays;
177
+ return this.requestJson("POST", "/v1/api-keys", { body });
178
+ }
179
+ /** Revoke an API key. Returns nothing (the server replies `204 No Content`). */
180
+ async revokeApiKey(id) {
181
+ await this.request("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`);
182
+ }
183
+ // ── Deliveries ─────────────────────────────────────────────────────────────
184
+ /** List deliveries. Only the filters you provide are sent as query params. */
185
+ async listDeliveries(opts = {}) {
186
+ const query = {};
187
+ if (opts.status !== void 0) query.status = opts.status;
188
+ if (opts.endpointId !== void 0) query.endpoint_id = opts.endpointId;
189
+ if (opts.eventType !== void 0) query.event_type = opts.eventType;
190
+ if (opts.since !== void 0) query.since = opts.since;
191
+ if (opts.until !== void 0) query.until = opts.until;
192
+ if (opts.q !== void 0) query.q = opts.q;
193
+ if (opts.offset !== void 0) query.offset = opts.offset;
194
+ if (opts.limit !== void 0) query.limit = opts.limit;
195
+ return this.requestJson("GET", "/v1/deliveries", { query });
196
+ }
197
+ /** Re-queue a delivery for another attempt. */
198
+ async redeliver(deliveryId) {
199
+ return this.requestJson(
200
+ "POST",
201
+ `/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`
202
+ );
203
+ }
204
+ // ── Internals ──────────────────────────────────────────────────────────────
205
+ /** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */
206
+ async requestJson(method, path, opts = {}) {
207
+ const resp = await this.request(method, path, opts);
208
+ return await resp.json();
209
+ }
210
+ /**
211
+ * The single request path used by every method: builds the URL (+ query), attaches auth, and runs
212
+ * the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
213
+ * backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.
214
+ */
215
+ async request(method, path, opts = {}) {
216
+ let url = `${this.baseUrl}${path}`;
217
+ if (opts.query) {
218
+ const qs = new URLSearchParams();
219
+ for (const [key, value] of Object.entries(opts.query)) {
220
+ if (value !== void 0 && value !== null) qs.append(key, String(value));
221
+ }
222
+ const suffix = qs.toString();
223
+ if (suffix) url += `?${suffix}`;
224
+ }
225
+ const headers = {
226
+ Authorization: `Bearer ${this.apiKey}`,
227
+ ...opts.headers
228
+ };
229
+ let body;
230
+ if (opts.body !== void 0) {
231
+ body = JSON.stringify(opts.body);
232
+ headers["Content-Type"] = "application/json";
233
+ }
121
234
  let lastErr;
122
235
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
123
236
  let resp;
124
237
  try {
125
238
  resp = await this.fetchImpl(url, {
126
- method: "POST",
239
+ method,
127
240
  headers,
128
241
  body,
129
242
  signal: AbortSignal.timeout(this.timeoutMs)
@@ -155,14 +268,14 @@ function retryAfterMs(resp) {
155
268
  return null;
156
269
  }
157
270
  async function apiError(resp) {
271
+ const text = await resp.text();
158
272
  let code = "error";
159
- let message = "";
273
+ let message = text;
160
274
  try {
161
- const data = await resp.json();
275
+ const data = JSON.parse(text);
162
276
  code = data.error?.code ?? code;
163
- message = data.error?.message ?? "";
277
+ message = data.error?.message ?? message;
164
278
  } catch {
165
- message = "";
166
279
  }
167
280
  return new WebhookdApiError(resp.status, code, message);
168
281
  }
@@ -171,7 +284,7 @@ function sleep(ms) {
171
284
  }
172
285
 
173
286
  // src/index.ts
174
- var VERSION = "0.1.0";
287
+ var VERSION = "0.2.0";
175
288
  // Annotate the CommonJS export names for ESM import in node:
176
289
  0 && (module.exports = {
177
290
  DEFAULT_TOLERANCE_SECONDS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/signature.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type { ClientOptions, PublishOptions, WebhookdEvent } from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nexport const VERSION = \"0.1.0\";\n","/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n const candidate = Buffer.from(signature.startsWith(PREFIX) ? signature : `${PREFIX}${signature}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n return expected.length === candidate.length && timingSafeEqual(expected, candidate);\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n };\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.post(\"/v1/events\", JSON.stringify(body), headers);\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n private async post(\n path: string,\n body: string,\n headers: Record<string, string>,\n ): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n let code = \"error\";\n let message = \"\";\n try {\n const data = (await resp.json()) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? \"\";\n } catch {\n message = \"\";\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/signature.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type {\n ClientOptions,\n PublishOptions,\n WebhookdEvent,\n Subscription,\n Endpoint,\n ApiKey,\n Delivery,\n Page,\n CreateEndpointOptions,\n ListEndpointsOptions,\n EndpointPatch,\n CreateApiKeyOptions,\n ListDeliveriesOptions,\n} from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\n// Keep in lockstep with package.json + the release tag (see publish.yml's bump checklist).\nexport const VERSION = \"0.2.0\";\n","/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n // X-Webhook-Signature carries one token normally, or several comma-separated tokens during a\n // signing-secret rotation (webhookd dual-sign overlap) — accept if ANY token verifies, so a\n // subscriber configured with EITHER the current or the previous secret keeps working.\n for (const raw of signature.split(\",\")) {\n const token = raw.trim();\n const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) return true;\n }\n return false;\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish + management client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\n/** A subscription filter attached to an endpoint. */\nexport interface Subscription {\n match_kind: \"exact\" | \"prefix\" | \"suffix\" | \"all\";\n pattern: string;\n}\n\n/**\n * An endpoint, as returned by the management endpoints (webhookd's `EndpointOut`).\n * Fields mirror the server's snake_case wire shape verbatim. `secret` is present ONLY on the\n * create + rotate-secret responses (returned exactly once).\n */\nexport interface Endpoint {\n id: string;\n url: string;\n environment: string;\n application: string;\n status: string;\n subscriptions: Subscription[];\n /** The signing secret — returned ONCE, on create + rotate-secret only. */\n secret?: string;\n max_attempts?: number;\n retry_schedule?: number[];\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * An API key, as returned by `POST /v1/api-keys` (webhookd's `ApiKeyOut`). `key` is present ONLY on\n * the create response (returned exactly once).\n */\nexport interface ApiKey {\n id: string;\n name: string;\n scope: string;\n /** The raw key — returned ONCE, on create only. */\n key?: string;\n created_at?: string;\n expires_at?: string | null;\n}\n\n/** A delivery attempt record, as returned by the deliveries endpoints (webhookd's `DeliveryOut`). */\nexport interface Delivery {\n id: string;\n endpoint_id: string;\n event_id?: string;\n event_type?: string;\n status: string;\n attempts?: number;\n created_at?: string;\n updated_at?: string;\n}\n\n/** A single page of a list endpoint — items plus the cursor for the next page (`null` at the end). */\nexport interface Page<T> {\n items: T[];\n next_offset: number | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport interface CreateEndpointOptions {\n environment?: string;\n subscriptions?: Subscription[];\n application?: string;\n secret?: string;\n maxAttempts?: number;\n retrySchedule?: number[];\n description?: string;\n customHeaders?: Record<string, string>;\n deliveryTimeoutMs?: number;\n}\n\nexport interface ListEndpointsOptions {\n environment?: string;\n offset?: number;\n limit?: number;\n}\n\n/**\n * A raw PATCH mapping for `updateEndpoint`, keyed with the server's snake_case names. PATCH semantics:\n * an OMITTED key is left unchanged; an explicit `null` CLEARS the field. The object is sent verbatim.\n */\nexport interface EndpointPatch {\n url?: string;\n max_attempts?: number | null;\n retry_schedule?: number[] | null;\n status?: \"enabled\" | \"disabled\";\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n}\n\nexport interface CreateApiKeyOptions {\n name?: string;\n scope?: \"admin\" | \"publish\";\n expiresInDays?: number;\n}\n\nexport interface ListDeliveriesOptions {\n status?: \"queued\" | \"sending\" | \"sent\" | \"failed\" | \"dead\";\n endpointId?: string;\n eventType?: string;\n since?: string | number;\n until?: string | number;\n q?: string;\n offset?: number;\n limit?: number;\n}\n\ninterface RequestOptions {\n /** JSON body to send; omitted for GET/DELETE and bodyless POSTs (no `Content-Type` is set). */\n body?: unknown;\n /** Query params; `undefined`/`null` values are dropped so only provided params are sent. */\n query?: Record<string, unknown>;\n /** Extra request headers (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {};\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.request(\"POST\", \"/v1/events\", { body, headers });\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n // ── Endpoints ──────────────────────────────────────────────────────────────\n\n /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */\n async createEndpoint(url: string, opts: CreateEndpointOptions = {}): Promise<Endpoint> {\n const body: Record<string, unknown> = {\n url,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.subscriptions !== undefined) body.subscriptions = opts.subscriptions;\n if (opts.secret !== undefined) body.secret = opts.secret;\n if (opts.maxAttempts !== undefined) body.max_attempts = opts.maxAttempts;\n if (opts.retrySchedule !== undefined) body.retry_schedule = opts.retrySchedule;\n if (opts.description !== undefined) body.description = opts.description;\n if (opts.customHeaders !== undefined) body.custom_headers = opts.customHeaders;\n if (opts.deliveryTimeoutMs !== undefined) body.delivery_timeout_ms = opts.deliveryTimeoutMs;\n return this.requestJson<Endpoint>(\"POST\", \"/v1/endpoints\", { body });\n }\n\n /** List endpoints for an environment (defaults to `prod`). */\n async listEndpoints(opts: ListEndpointsOptions = {}): Promise<Page<Endpoint>> {\n const query: Record<string, unknown> = { environment: opts.environment ?? \"prod\" };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Endpoint>>(\"GET\", \"/v1/endpoints\", { query });\n }\n\n /** Fetch a single endpoint by id. */\n async getEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"GET\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /**\n * Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left\n * unchanged, an explicit `null` clears the field. No defaults are injected.\n */\n async updateEndpoint(id: string, patch: EndpointPatch): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"PATCH\", `/v1/endpoints/${encodeURIComponent(id)}`, {\n body: patch,\n });\n }\n\n /** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */\n async deleteEndpoint(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */\n async rotateEndpointSecret(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);\n }\n\n /** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */\n async enableEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/enable`);\n }\n\n // ── API keys ───────────────────────────────────────────────────────────────\n\n /** Create an API key. The response includes the raw `key` exactly once — persist it. */\n async createApiKey(opts: CreateApiKeyOptions = {}): Promise<ApiKey> {\n const body: Record<string, unknown> = {\n name: opts.name ?? \"\",\n scope: opts.scope ?? \"admin\",\n };\n if (opts.expiresInDays !== undefined) body.expires_in_days = opts.expiresInDays;\n return this.requestJson<ApiKey>(\"POST\", \"/v1/api-keys\", { body });\n }\n\n /** Revoke an API key. Returns nothing (the server replies `204 No Content`). */\n async revokeApiKey(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/api-keys/${encodeURIComponent(id)}`);\n }\n\n // ── Deliveries ─────────────────────────────────────────────────────────────\n\n /** List deliveries. Only the filters you provide are sent as query params. */\n async listDeliveries(opts: ListDeliveriesOptions = {}): Promise<Page<Delivery>> {\n const query: Record<string, unknown> = {};\n if (opts.status !== undefined) query.status = opts.status;\n if (opts.endpointId !== undefined) query.endpoint_id = opts.endpointId;\n if (opts.eventType !== undefined) query.event_type = opts.eventType;\n if (opts.since !== undefined) query.since = opts.since;\n if (opts.until !== undefined) query.until = opts.until;\n if (opts.q !== undefined) query.q = opts.q;\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Delivery>>(\"GET\", \"/v1/deliveries\", { query });\n }\n\n /** Re-queue a delivery for another attempt. */\n async redeliver(deliveryId: string): Promise<Delivery> {\n return this.requestJson<Delivery>(\n \"POST\",\n `/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`,\n );\n }\n\n // ── Internals ──────────────────────────────────────────────────────────────\n\n /** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */\n private async requestJson<T>(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<T> {\n const resp = await this.request(method, path, opts);\n return (await resp.json()) as T;\n }\n\n /**\n * The single request path used by every method: builds the URL (+ query), attaches auth, and runs\n * the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential\n * backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.\n */\n private async request(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<Response> {\n let url = `${this.baseUrl}${path}`;\n if (opts.query) {\n const qs = new URLSearchParams();\n for (const [key, value] of Object.entries(opts.query)) {\n if (value !== undefined && value !== null) qs.append(key, String(value));\n }\n const suffix = qs.toString();\n if (suffix) url += `?${suffix}`;\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n ...opts.headers,\n };\n let body: string | undefined;\n if (opts.body !== undefined) {\n body = JSON.stringify(opts.body);\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n // Read the body once; a non-envelope (or non-JSON) error body falls back to the raw text.\n const text = await resp.text();\n let code = \"error\";\n let message = text;\n try {\n const data = JSON.parse(text) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? message;\n } catch {\n // not JSON — keep the raw text as the message\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACQA,yBAA4C;AAE5C,IAAM,SAAS;AACR,IAAM,4BAA4B;AAEzC,SAAS,SAAS,OAAgC;AAChD,SAAO,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,OAAO,MAAM;AACnE;AAEA,SAAS,YAAY,SAAiB,WAAkC;AACtE,MAAI,cAAc,KAAM,QAAO;AAC/B,SAAO,OAAO,OAAO,CAAC,OAAO,KAAK,GAAG,SAAS,KAAK,OAAO,GAAG,OAAO,CAAC;AACvE;AAGO,SAAS,KACd,QACA,SACA,YAA2B,MACnB;AACR,QAAM,aAAS,+BAAW,UAAU,MAAM,EACvC,OAAO,YAAY,SAAS,OAAO,GAAG,SAAS,CAAC,EAChD,OAAO,KAAK;AACf,SAAO,GAAG,MAAM,GAAG,MAAM;AAC3B;AAiBO,SAAS,OACd,QACA,SACA,WACA,OAAsB,CAAC,GACd;AACT,QAAM,EAAE,YAAY,MAAM,mBAAmB,2BAA2B,IAAI,IAAI;AAChF,QAAM,KAAK,cAAc,QAAQ,cAAc,SAAY,OAAO,OAAO,SAAS;AAClF,MAAI,OAAO,MAAM;AACf,QAAI,OAAO,MAAM,EAAE,EAAG,QAAO;AAC7B,UAAM,UAAU,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACnD,QAAI,KAAK,IAAI,UAAU,EAAE,IAAI,iBAAkB,QAAO;AAAA,EACxD;AACA,QAAM,WAAW,OAAO,KAAK,KAAK,QAAQ,SAAS,EAAE,CAAC;AAItD,aAAW,OAAO,UAAU,MAAM,GAAG,GAAG;AACtC,UAAM,QAAQ,IAAI,KAAK;AACvB,UAAM,YAAY,OAAO,KAAK,MAAM,WAAW,MAAM,IAAI,QAAQ,GAAG,MAAM,GAAG,KAAK,EAAE;AAEpF,QAAI,SAAS,WAAW,UAAU,cAAU,oCAAgB,UAAU,SAAS,EAAG,QAAO;AAAA,EAC3F;AACA,SAAO;AACT;;;ACxEO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACvC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,mBAAN,cAA+B,cAAc;AAAA,EACzC;AAAA,EACA;AAAA,EAET,YAAY,YAAoB,MAAc,SAAiB;AAC7D,UAAM,IAAI,UAAU,IAAI,IAAI,KAAK,OAAO,EAAE;AAC1C,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,OAAO;AAAA,EACd;AACF;;;ACpBA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AAsJjD,IAAM,iBAAN,MAAqB;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAqB;AAC/B,SAAK,UAAU,KAAK,QAAQ,QAAQ,QAAQ,EAAE;AAC9C,SAAK,SAAS,KAAK;AACnB,SAAK,YAAY,KAAK,aAAa;AACnC,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,YAAY,KAAK,aAAa;AAAA,EACrC;AAAA,EAEA,MAAM,QACJ,WACA,SACA,OAAuB,CAAC,GACA;AACxB,UAAM,OAAgC;AAAA,MACpC,YAAY;AAAA,MACZ;AAAA,MACA,aAAa,KAAK,eAAe;AAAA,MACjC,aAAa,KAAK,eAAe;AAAA,IACnC;AACA,QAAI,KAAK,WAAW,OAAW,MAAK,SAAS,KAAK;AAElD,UAAM,UAAkC,CAAC;AACzC,QAAI,KAAK,mBAAmB,OAAW,SAAQ,iBAAiB,IAAI,KAAK;AAEzE,UAAM,OAAO,MAAM,KAAK,QAAQ,QAAQ,cAAc,EAAE,MAAM,QAAQ,CAAC;AACvE,UAAM,OAAQ,MAAM,KAAK,KAAK;AAC9B,WAAO;AAAA,MACL,IAAI,OAAO,KAAK,EAAE;AAAA,MAClB,UAAU,OAAO,KAAK,SAAS;AAAA,MAC/B,WAAW,OAAO,KAAK,UAAU;AAAA,MACjC,aAAa,OAAO,KAAK,eAAe,SAAS;AAAA,MACjD,aAAa,OAAO,KAAK,eAAe,MAAM;AAAA,MAC9C,mBAAmB,OAAO,KAAK,sBAAsB,CAAC;AAAA,MACtD,QAAS,KAAK,UAA4B;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,KAAa,OAA8B,CAAC,GAAsB;AACrF,UAAM,OAAgC;AAAA,MACpC;AAAA,MACA,aAAa,KAAK,eAAe;AAAA,MACjC,aAAa,KAAK,eAAe;AAAA,IACnC;AACA,QAAI,KAAK,kBAAkB,OAAW,MAAK,gBAAgB,KAAK;AAChE,QAAI,KAAK,WAAW,OAAW,MAAK,SAAS,KAAK;AAClD,QAAI,KAAK,gBAAgB,OAAW,MAAK,eAAe,KAAK;AAC7D,QAAI,KAAK,kBAAkB,OAAW,MAAK,iBAAiB,KAAK;AACjE,QAAI,KAAK,gBAAgB,OAAW,MAAK,cAAc,KAAK;AAC5D,QAAI,KAAK,kBAAkB,OAAW,MAAK,iBAAiB,KAAK;AACjE,QAAI,KAAK,sBAAsB,OAAW,MAAK,sBAAsB,KAAK;AAC1E,WAAO,KAAK,YAAsB,QAAQ,iBAAiB,EAAE,KAAK,CAAC;AAAA,EACrE;AAAA;AAAA,EAGA,MAAM,cAAc,OAA6B,CAAC,GAA4B;AAC5E,UAAM,QAAiC,EAAE,aAAa,KAAK,eAAe,OAAO;AACjF,QAAI,KAAK,WAAW,OAAW,OAAM,SAAS,KAAK;AACnD,QAAI,KAAK,UAAU,OAAW,OAAM,QAAQ,KAAK;AACjD,WAAO,KAAK,YAA4B,OAAO,iBAAiB,EAAE,MAAM,CAAC;AAAA,EAC3E;AAAA;AAAA,EAGA,MAAM,YAAY,IAA+B;AAC/C,WAAO,KAAK,YAAsB,OAAO,iBAAiB,mBAAmB,EAAE,CAAC,EAAE;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAe,IAAY,OAAyC;AACxE,WAAO,KAAK,YAAsB,SAAS,iBAAiB,mBAAmB,EAAE,CAAC,IAAI;AAAA,MACpF,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,eAAe,IAA2B;AAC9C,UAAM,KAAK,QAAQ,UAAU,iBAAiB,mBAAmB,EAAE,CAAC,EAAE;AAAA,EACxE;AAAA;AAAA,EAGA,MAAM,qBAAqB,IAA+B;AACxD,WAAO,KAAK,YAAsB,QAAQ,iBAAiB,mBAAmB,EAAE,CAAC,gBAAgB;AAAA,EACnG;AAAA;AAAA,EAGA,MAAM,eAAe,IAA+B;AAClD,WAAO,KAAK,YAAsB,QAAQ,iBAAiB,mBAAmB,EAAE,CAAC,SAAS;AAAA,EAC5F;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,OAA4B,CAAC,GAAoB;AAClE,UAAM,OAAgC;AAAA,MACpC,MAAM,KAAK,QAAQ;AAAA,MACnB,OAAO,KAAK,SAAS;AAAA,IACvB;AACA,QAAI,KAAK,kBAAkB,OAAW,MAAK,kBAAkB,KAAK;AAClE,WAAO,KAAK,YAAoB,QAAQ,gBAAgB,EAAE,KAAK,CAAC;AAAA,EAClE;AAAA;AAAA,EAGA,MAAM,aAAa,IAA2B;AAC5C,UAAM,KAAK,QAAQ,UAAU,gBAAgB,mBAAmB,EAAE,CAAC,EAAE;AAAA,EACvE;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,OAA8B,CAAC,GAA4B;AAC9E,UAAM,QAAiC,CAAC;AACxC,QAAI,KAAK,WAAW,OAAW,OAAM,SAAS,KAAK;AACnD,QAAI,KAAK,eAAe,OAAW,OAAM,cAAc,KAAK;AAC5D,QAAI,KAAK,cAAc,OAAW,OAAM,aAAa,KAAK;AAC1D,QAAI,KAAK,UAAU,OAAW,OAAM,QAAQ,KAAK;AACjD,QAAI,KAAK,UAAU,OAAW,OAAM,QAAQ,KAAK;AACjD,QAAI,KAAK,MAAM,OAAW,OAAM,IAAI,KAAK;AACzC,QAAI,KAAK,WAAW,OAAW,OAAM,SAAS,KAAK;AACnD,QAAI,KAAK,UAAU,OAAW,OAAM,QAAQ,KAAK;AACjD,WAAO,KAAK,YAA4B,OAAO,kBAAkB,EAAE,MAAM,CAAC;AAAA,EAC5E;AAAA;AAAA,EAGA,MAAM,UAAU,YAAuC;AACrD,WAAO,KAAK;AAAA,MACV;AAAA,MACA,kBAAkB,mBAAmB,UAAU,CAAC;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,MAAc,YACZ,QACA,MACA,OAAuB,CAAC,GACZ;AACZ,UAAM,OAAO,MAAM,KAAK,QAAQ,QAAQ,MAAM,IAAI;AAClD,WAAQ,MAAM,KAAK,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,QACZ,QACA,MACA,OAAuB,CAAC,GACL;AACnB,QAAI,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAChC,QAAI,KAAK,OAAO;AACd,YAAM,KAAK,IAAI,gBAAgB;AAC/B,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG;AACrD,YAAI,UAAU,UAAa,UAAU,KAAM,IAAG,OAAO,KAAK,OAAO,KAAK,CAAC;AAAA,MACzE;AACA,YAAM,SAAS,GAAG,SAAS;AAC3B,UAAI,OAAQ,QAAO,IAAI,MAAM;AAAA,IAC/B;AAEA,UAAM,UAAkC;AAAA,MACtC,eAAe,UAAU,KAAK,MAAM;AAAA,MACpC,GAAG,KAAK;AAAA,IACV;AACA,QAAI;AACJ,QAAI,KAAK,SAAS,QAAW;AAC3B,aAAO,KAAK,UAAU,KAAK,IAAI;AAC/B,cAAQ,cAAc,IAAI;AAAA,IAC5B;AAEA,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,KAAK,YAAY,WAAW;AAC3D,UAAI;AACJ,UAAI;AACF,eAAO,MAAM,KAAK,UAAU,KAAK;AAAA,UAC/B;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,YAAY,QAAQ,KAAK,SAAS;AAAA,QAC5C,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,kBAAU;AACV,YAAI,UAAU,KAAK,YAAY;AAC7B,gBAAM,MAAM,UAAU,OAAO,CAAC;AAC9B;AAAA,QACF;AACA,cAAM,IAAI,cAAc,mBAAmB,OAAO,GAAG,CAAC,EAAE;AAAA,MAC1D;AAEA,UAAI,eAAe,IAAI,KAAK,MAAM,KAAK,UAAU,KAAK,YAAY;AAChE,cAAM,MAAM,aAAa,IAAI,KAAK,UAAU,OAAO,CAAC;AACpD;AAAA,MACF;AACA,UAAI,KAAK,UAAU,IAAK,OAAM,MAAM,SAAS,IAAI;AACjD,aAAO;AAAA,IACT;AACA,UAAM,IAAI,cAAc,iCAAiC,OAAO,OAAO,CAAC,EAAE;AAAA,EAC5E;AACF;AAEA,SAAS,UAAU,SAAyB;AAC1C,SAAO,KAAK,IAAI,KAAM,MAAM,KAAK,OAAO;AAC1C;AAEA,SAAS,aAAa,MAA+B;AACnD,QAAM,MAAM,KAAK,QAAQ,IAAI,aAAa;AAC1C,MAAI,OAAO,QAAQ,KAAK,GAAG,EAAG,QAAO,OAAO,GAAG,IAAI;AACnD,SAAO;AACT;AAEA,eAAe,SAAS,MAA2C;AAEjE,QAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,MAAI,OAAO;AACX,MAAI,UAAU;AACd,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,WAAO,KAAK,OAAO,QAAQ;AAC3B,cAAU,KAAK,OAAO,WAAW;AAAA,EACnC,QAAQ;AAAA,EAER;AACA,SAAO,IAAI,iBAAiB,KAAK,QAAQ,MAAM,OAAO;AACxD;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;;;AH/WO,IAAM,UAAU;","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
- private post;
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.1.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
- private post;
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.1.0";
211
+ declare const VERSION = "0.2.0";
80
212
 
81
- export { type ClientOptions, DEFAULT_TOLERANCE_SECONDS, type PublishOptions, VERSION, type VerifyOptions, WebhookdApiError, WebhookdClient, WebhookdError, type WebhookdEvent, sign, verify };
213
+ export { type ApiKey, type ClientOptions, type CreateApiKeyOptions, type CreateEndpointOptions, DEFAULT_TOLERANCE_SECONDS, type Delivery, type Endpoint, type EndpointPatch, type ListDeliveriesOptions, type ListEndpointsOptions, type Page, type PublishOptions, type Subscription, VERSION, type VerifyOptions, WebhookdApiError, WebhookdClient, WebhookdError, type WebhookdEvent, sign, verify };
package/dist/index.js CHANGED
@@ -22,8 +22,12 @@ function verify(secret, rawBody, signature, opts = {}) {
22
22
  if (Math.abs(current - ts) > toleranceSeconds) return false;
23
23
  }
24
24
  const expected = Buffer.from(sign(secret, rawBody, ts));
25
- const candidate = Buffer.from(signature.startsWith(PREFIX) ? signature : `${PREFIX}${signature}`);
26
- return expected.length === candidate.length && timingSafeEqual(expected, candidate);
25
+ for (const raw of signature.split(",")) {
26
+ const token = raw.trim();
27
+ const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);
28
+ if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) return true;
29
+ }
30
+ return false;
27
31
  }
28
32
 
29
33
  // src/errors.ts
@@ -67,12 +71,9 @@ var WebhookdClient = class {
67
71
  application: opts.application ?? "default"
68
72
  };
69
73
  if (opts.source !== void 0) body.source = opts.source;
70
- const headers = {
71
- "Content-Type": "application/json",
72
- Authorization: `Bearer ${this.apiKey}`
73
- };
74
+ const headers = {};
74
75
  if (opts.idempotencyKey !== void 0) headers["Idempotency-Key"] = opts.idempotencyKey;
75
- const resp = await this.post("/v1/events", JSON.stringify(body), headers);
76
+ const resp = await this.request("POST", "/v1/events", { body, headers });
76
77
  const data = await resp.json();
77
78
  return {
78
79
  id: String(data.id),
@@ -84,14 +85,126 @@ var WebhookdClient = class {
84
85
  source: data.source ?? null
85
86
  };
86
87
  }
87
- async post(path, body, headers) {
88
- const url = `${this.baseUrl}${path}`;
88
+ // ── Endpoints ──────────────────────────────────────────────────────────────
89
+ /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */
90
+ async createEndpoint(url, opts = {}) {
91
+ const body = {
92
+ url,
93
+ environment: opts.environment ?? "prod",
94
+ application: opts.application ?? "default"
95
+ };
96
+ if (opts.subscriptions !== void 0) body.subscriptions = opts.subscriptions;
97
+ if (opts.secret !== void 0) body.secret = opts.secret;
98
+ if (opts.maxAttempts !== void 0) body.max_attempts = opts.maxAttempts;
99
+ if (opts.retrySchedule !== void 0) body.retry_schedule = opts.retrySchedule;
100
+ if (opts.description !== void 0) body.description = opts.description;
101
+ if (opts.customHeaders !== void 0) body.custom_headers = opts.customHeaders;
102
+ if (opts.deliveryTimeoutMs !== void 0) body.delivery_timeout_ms = opts.deliveryTimeoutMs;
103
+ return this.requestJson("POST", "/v1/endpoints", { body });
104
+ }
105
+ /** List endpoints for an environment (defaults to `prod`). */
106
+ async listEndpoints(opts = {}) {
107
+ const query = { environment: opts.environment ?? "prod" };
108
+ if (opts.offset !== void 0) query.offset = opts.offset;
109
+ if (opts.limit !== void 0) query.limit = opts.limit;
110
+ return this.requestJson("GET", "/v1/endpoints", { query });
111
+ }
112
+ /** Fetch a single endpoint by id. */
113
+ async getEndpoint(id) {
114
+ return this.requestJson("GET", `/v1/endpoints/${encodeURIComponent(id)}`);
115
+ }
116
+ /**
117
+ * Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left
118
+ * unchanged, an explicit `null` clears the field. No defaults are injected.
119
+ */
120
+ async updateEndpoint(id, patch) {
121
+ return this.requestJson("PATCH", `/v1/endpoints/${encodeURIComponent(id)}`, {
122
+ body: patch
123
+ });
124
+ }
125
+ /** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */
126
+ async deleteEndpoint(id) {
127
+ await this.request("DELETE", `/v1/endpoints/${encodeURIComponent(id)}`);
128
+ }
129
+ /** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */
130
+ async rotateEndpointSecret(id) {
131
+ return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);
132
+ }
133
+ /** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */
134
+ async enableEndpoint(id) {
135
+ return this.requestJson("POST", `/v1/endpoints/${encodeURIComponent(id)}/enable`);
136
+ }
137
+ // ── API keys ───────────────────────────────────────────────────────────────
138
+ /** Create an API key. The response includes the raw `key` exactly once — persist it. */
139
+ async createApiKey(opts = {}) {
140
+ const body = {
141
+ name: opts.name ?? "",
142
+ scope: opts.scope ?? "admin"
143
+ };
144
+ if (opts.expiresInDays !== void 0) body.expires_in_days = opts.expiresInDays;
145
+ return this.requestJson("POST", "/v1/api-keys", { body });
146
+ }
147
+ /** Revoke an API key. Returns nothing (the server replies `204 No Content`). */
148
+ async revokeApiKey(id) {
149
+ await this.request("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`);
150
+ }
151
+ // ── Deliveries ─────────────────────────────────────────────────────────────
152
+ /** List deliveries. Only the filters you provide are sent as query params. */
153
+ async listDeliveries(opts = {}) {
154
+ const query = {};
155
+ if (opts.status !== void 0) query.status = opts.status;
156
+ if (opts.endpointId !== void 0) query.endpoint_id = opts.endpointId;
157
+ if (opts.eventType !== void 0) query.event_type = opts.eventType;
158
+ if (opts.since !== void 0) query.since = opts.since;
159
+ if (opts.until !== void 0) query.until = opts.until;
160
+ if (opts.q !== void 0) query.q = opts.q;
161
+ if (opts.offset !== void 0) query.offset = opts.offset;
162
+ if (opts.limit !== void 0) query.limit = opts.limit;
163
+ return this.requestJson("GET", "/v1/deliveries", { query });
164
+ }
165
+ /** Re-queue a delivery for another attempt. */
166
+ async redeliver(deliveryId) {
167
+ return this.requestJson(
168
+ "POST",
169
+ `/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`
170
+ );
171
+ }
172
+ // ── Internals ──────────────────────────────────────────────────────────────
173
+ /** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */
174
+ async requestJson(method, path, opts = {}) {
175
+ const resp = await this.request(method, path, opts);
176
+ return await resp.json();
177
+ }
178
+ /**
179
+ * The single request path used by every method: builds the URL (+ query), attaches auth, and runs
180
+ * the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential
181
+ * backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.
182
+ */
183
+ async request(method, path, opts = {}) {
184
+ let url = `${this.baseUrl}${path}`;
185
+ if (opts.query) {
186
+ const qs = new URLSearchParams();
187
+ for (const [key, value] of Object.entries(opts.query)) {
188
+ if (value !== void 0 && value !== null) qs.append(key, String(value));
189
+ }
190
+ const suffix = qs.toString();
191
+ if (suffix) url += `?${suffix}`;
192
+ }
193
+ const headers = {
194
+ Authorization: `Bearer ${this.apiKey}`,
195
+ ...opts.headers
196
+ };
197
+ let body;
198
+ if (opts.body !== void 0) {
199
+ body = JSON.stringify(opts.body);
200
+ headers["Content-Type"] = "application/json";
201
+ }
89
202
  let lastErr;
90
203
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
91
204
  let resp;
92
205
  try {
93
206
  resp = await this.fetchImpl(url, {
94
- method: "POST",
207
+ method,
95
208
  headers,
96
209
  body,
97
210
  signal: AbortSignal.timeout(this.timeoutMs)
@@ -123,14 +236,14 @@ function retryAfterMs(resp) {
123
236
  return null;
124
237
  }
125
238
  async function apiError(resp) {
239
+ const text = await resp.text();
126
240
  let code = "error";
127
- let message = "";
241
+ let message = text;
128
242
  try {
129
- const data = await resp.json();
243
+ const data = JSON.parse(text);
130
244
  code = data.error?.code ?? code;
131
- message = data.error?.message ?? "";
245
+ message = data.error?.message ?? message;
132
246
  } catch {
133
- message = "";
134
247
  }
135
248
  return new WebhookdApiError(resp.status, code, message);
136
249
  }
@@ -139,7 +252,7 @@ function sleep(ms) {
139
252
  }
140
253
 
141
254
  // src/index.ts
142
- var VERSION = "0.1.0";
255
+ var VERSION = "0.2.0";
143
256
  export {
144
257
  DEFAULT_TOLERANCE_SECONDS,
145
258
  VERSION,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/signature.ts","../src/errors.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n const candidate = Buffer.from(signature.startsWith(PREFIX) ? signature : `${PREFIX}${signature}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n return expected.length === candidate.length && timingSafeEqual(expected, candidate);\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n };\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.post(\"/v1/events\", JSON.stringify(body), headers);\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n private async post(\n path: string,\n body: string,\n headers: Record<string, string>,\n ): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n let code = \"error\";\n let message = \"\";\n try {\n const data = (await resp.json()) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? \"\";\n } catch {\n message = \"\";\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type { ClientOptions, PublishOptions, WebhookdEvent } from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nexport const VERSION = \"0.1.0\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/signature.ts","../src/errors.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * Verify webhookd webhook signatures.\n *\n * webhookd signs every delivery as `HMAC_SHA256(secret, \"<timestamp>.\" + rawBody)` (its default\n * timestamped mode) and sends `X-Webhook-Signature: sha256=<hex>` plus `X-Webhook-Timestamp`\n * (unix seconds). A subscriber MUST verify the signature to prove the request genuinely came from\n * webhookd and wasn't tampered with. Mirrors `delivery_core.webhook_outbox.{sign,verify}`.\n */\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst PREFIX = \"sha256=\";\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction toBuffer(value: string | Buffer): Buffer {\n return Buffer.isBuffer(value) ? value : Buffer.from(value, \"utf8\");\n}\n\nfunction signedBytes(rawBody: Buffer, timestamp: number | null): Buffer {\n if (timestamp === null) return rawBody;\n return Buffer.concat([Buffer.from(`${timestamp}.`, \"ascii\"), rawBody]);\n}\n\n/** The `sha256=<hex>` signature webhookd would send for `rawBody` (+ optional timestamp). */\nexport function sign(\n secret: string | Buffer,\n rawBody: string | Buffer,\n timestamp: number | null = null,\n): string {\n const digest = createHmac(\"sha256\", secret)\n .update(signedBytes(toBuffer(rawBody), timestamp))\n .digest(\"hex\");\n return `${PREFIX}${digest}`;\n}\n\nexport interface VerifyOptions {\n /** The `X-Webhook-Timestamp` header value. When given, the replay window is enforced — always pass it. */\n timestamp?: number | string | null;\n /** Replay window in seconds; webhookd's default is 300. */\n toleranceSeconds?: number;\n /** Override the current unix time (for tests). */\n now?: number;\n}\n\n/**\n * Return `true` iff `signature` is a valid webhookd signature for `rawBody`.\n *\n * Pass the EXACT bytes you received as `rawBody` (a string or Buffer) — do not re-serialize the\n * JSON, or the signature won't match. Comparison is constant-time.\n */\nexport function verify(\n secret: string | Buffer,\n rawBody: string | Buffer,\n signature: string,\n opts: VerifyOptions = {},\n): boolean {\n const { timestamp = null, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, now } = opts;\n const ts = timestamp === null || timestamp === undefined ? null : Number(timestamp);\n if (ts !== null) {\n if (Number.isNaN(ts)) return false;\n const current = now ?? Math.floor(Date.now() / 1000);\n if (Math.abs(current - ts) > toleranceSeconds) return false;\n }\n const expected = Buffer.from(sign(secret, rawBody, ts));\n // X-Webhook-Signature carries one token normally, or several comma-separated tokens during a\n // signing-secret rotation (webhookd dual-sign overlap) — accept if ANY token verifies, so a\n // subscriber configured with EITHER the current or the previous secret keeps working.\n for (const raw of signature.split(\",\")) {\n const token = raw.trim();\n const candidate = Buffer.from(token.startsWith(PREFIX) ? token : `${PREFIX}${token}`);\n // timingSafeEqual requires equal-length buffers; a length mismatch is already a non-match.\n if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) return true;\n }\n return false;\n}\n","/** Base class for all webhookd SDK errors (network failures, etc.). */\nexport class WebhookdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"WebhookdError\";\n }\n}\n\n/**\n * A non-2xx response from the webhookd API. Carries the M5a error envelope: a stable machine\n * `code` (e.g. `\"rate_limited\"`, `\"not_found\"`, `\"validation_error\"`) and a human `message`,\n * plus the HTTP `statusCode`.\n */\nexport class WebhookdApiError extends WebhookdError {\n readonly statusCode: number;\n readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(`[${statusCode} ${code}] ${message}`);\n this.name = \"WebhookdApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n","/** Typed publish + management client for the webhookd API (zero runtime deps — uses the global `fetch`). */\nimport { WebhookdApiError, WebhookdError } from \"./errors\";\n\nconst RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** The published event, as returned by `POST /v1/events` (webhookd's `EventOut`). */\nexport interface WebhookdEvent {\n id: string;\n eventUid: string;\n eventType: string;\n application: string;\n environment: string;\n deliveriesCreated: number;\n source: string | null;\n}\n\n/** A subscription filter attached to an endpoint. */\nexport interface Subscription {\n match_kind: \"exact\" | \"prefix\" | \"suffix\" | \"all\";\n pattern: string;\n}\n\n/**\n * An endpoint, as returned by the management endpoints (webhookd's `EndpointOut`).\n * Fields mirror the server's snake_case wire shape verbatim. `secret` is present ONLY on the\n * create + rotate-secret responses (returned exactly once).\n */\nexport interface Endpoint {\n id: string;\n url: string;\n environment: string;\n application: string;\n status: string;\n subscriptions: Subscription[];\n /** The signing secret — returned ONCE, on create + rotate-secret only. */\n secret?: string;\n max_attempts?: number;\n retry_schedule?: number[];\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * An API key, as returned by `POST /v1/api-keys` (webhookd's `ApiKeyOut`). `key` is present ONLY on\n * the create response (returned exactly once).\n */\nexport interface ApiKey {\n id: string;\n name: string;\n scope: string;\n /** The raw key — returned ONCE, on create only. */\n key?: string;\n created_at?: string;\n expires_at?: string | null;\n}\n\n/** A delivery attempt record, as returned by the deliveries endpoints (webhookd's `DeliveryOut`). */\nexport interface Delivery {\n id: string;\n endpoint_id: string;\n event_id?: string;\n event_type?: string;\n status: string;\n attempts?: number;\n created_at?: string;\n updated_at?: string;\n}\n\n/** A single page of a list endpoint — items plus the cursor for the next page (`null` at the end). */\nexport interface Page<T> {\n items: T[];\n next_offset: number | null;\n}\n\nexport interface ClientOptions {\n baseUrl: string;\n /** A per-tenant API key (`whsk_…`) or a service token. */\n apiKey: string;\n timeoutMs?: number;\n maxRetries?: number;\n /** Inject a `fetch` implementation (defaults to the global `fetch`); used for tests. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface PublishOptions {\n environment?: string;\n application?: string;\n source?: string;\n /** Makes the publish safe to retry — a replay returns the original event without re-fanning-out. */\n idempotencyKey?: string;\n}\n\nexport interface CreateEndpointOptions {\n environment?: string;\n subscriptions?: Subscription[];\n application?: string;\n secret?: string;\n maxAttempts?: number;\n retrySchedule?: number[];\n description?: string;\n customHeaders?: Record<string, string>;\n deliveryTimeoutMs?: number;\n}\n\nexport interface ListEndpointsOptions {\n environment?: string;\n offset?: number;\n limit?: number;\n}\n\n/**\n * A raw PATCH mapping for `updateEndpoint`, keyed with the server's snake_case names. PATCH semantics:\n * an OMITTED key is left unchanged; an explicit `null` CLEARS the field. The object is sent verbatim.\n */\nexport interface EndpointPatch {\n url?: string;\n max_attempts?: number | null;\n retry_schedule?: number[] | null;\n status?: \"enabled\" | \"disabled\";\n description?: string | null;\n custom_headers?: Record<string, string> | null;\n delivery_timeout_ms?: number | null;\n}\n\nexport interface CreateApiKeyOptions {\n name?: string;\n scope?: \"admin\" | \"publish\";\n expiresInDays?: number;\n}\n\nexport interface ListDeliveriesOptions {\n status?: \"queued\" | \"sending\" | \"sent\" | \"failed\" | \"dead\";\n endpointId?: string;\n eventType?: string;\n since?: string | number;\n until?: string | number;\n q?: string;\n offset?: number;\n limit?: number;\n}\n\ninterface RequestOptions {\n /** JSON body to send; omitted for GET/DELETE and bodyless POSTs (no `Content-Type` is set). */\n body?: unknown;\n /** Query params; `undefined`/`null` values are dropped so only provided params are sent. */\n query?: Record<string, unknown>;\n /** Extra request headers (e.g. `Idempotency-Key`). */\n headers?: Record<string, string>;\n}\n\nexport class WebhookdClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n private readonly maxRetries: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.apiKey = opts.apiKey;\n this.timeoutMs = opts.timeoutMs ?? 10_000;\n this.maxRetries = opts.maxRetries ?? 2;\n this.fetchImpl = opts.fetchImpl ?? fetch;\n }\n\n async publish(\n eventType: string,\n payload: Record<string, unknown>,\n opts: PublishOptions = {},\n ): Promise<WebhookdEvent> {\n const body: Record<string, unknown> = {\n event_type: eventType,\n payload,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.source !== undefined) body.source = opts.source;\n\n const headers: Record<string, string> = {};\n if (opts.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = opts.idempotencyKey;\n\n const resp = await this.request(\"POST\", \"/v1/events\", { body, headers });\n const data = (await resp.json()) as Record<string, unknown>;\n return {\n id: String(data.id),\n eventUid: String(data.event_uid),\n eventType: String(data.event_type),\n application: String(data.application ?? \"default\"),\n environment: String(data.environment ?? \"prod\"),\n deliveriesCreated: Number(data.deliveries_created ?? 0),\n source: (data.source as string | null) ?? null,\n };\n }\n\n // ── Endpoints ──────────────────────────────────────────────────────────────\n\n /** Create an endpoint. The response includes the signing `secret` exactly once — persist it. */\n async createEndpoint(url: string, opts: CreateEndpointOptions = {}): Promise<Endpoint> {\n const body: Record<string, unknown> = {\n url,\n environment: opts.environment ?? \"prod\",\n application: opts.application ?? \"default\",\n };\n if (opts.subscriptions !== undefined) body.subscriptions = opts.subscriptions;\n if (opts.secret !== undefined) body.secret = opts.secret;\n if (opts.maxAttempts !== undefined) body.max_attempts = opts.maxAttempts;\n if (opts.retrySchedule !== undefined) body.retry_schedule = opts.retrySchedule;\n if (opts.description !== undefined) body.description = opts.description;\n if (opts.customHeaders !== undefined) body.custom_headers = opts.customHeaders;\n if (opts.deliveryTimeoutMs !== undefined) body.delivery_timeout_ms = opts.deliveryTimeoutMs;\n return this.requestJson<Endpoint>(\"POST\", \"/v1/endpoints\", { body });\n }\n\n /** List endpoints for an environment (defaults to `prod`). */\n async listEndpoints(opts: ListEndpointsOptions = {}): Promise<Page<Endpoint>> {\n const query: Record<string, unknown> = { environment: opts.environment ?? \"prod\" };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Endpoint>>(\"GET\", \"/v1/endpoints\", { query });\n }\n\n /** Fetch a single endpoint by id. */\n async getEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"GET\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /**\n * Partially update an endpoint. `patch` is sent verbatim (snake_case keys): an omitted key is left\n * unchanged, an explicit `null` clears the field. No defaults are injected.\n */\n async updateEndpoint(id: string, patch: EndpointPatch): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"PATCH\", `/v1/endpoints/${encodeURIComponent(id)}`, {\n body: patch,\n });\n }\n\n /** Delete an endpoint. Returns nothing (the server replies `204 No Content`). */\n async deleteEndpoint(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/endpoints/${encodeURIComponent(id)}`);\n }\n\n /** Rotate an endpoint's signing secret. The response includes the new `secret` exactly once. */\n async rotateEndpointSecret(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/rotate-secret`);\n }\n\n /** Re-enable an endpoint that webhookd auto-disabled after repeated delivery failures. */\n async enableEndpoint(id: string): Promise<Endpoint> {\n return this.requestJson<Endpoint>(\"POST\", `/v1/endpoints/${encodeURIComponent(id)}/enable`);\n }\n\n // ── API keys ───────────────────────────────────────────────────────────────\n\n /** Create an API key. The response includes the raw `key` exactly once — persist it. */\n async createApiKey(opts: CreateApiKeyOptions = {}): Promise<ApiKey> {\n const body: Record<string, unknown> = {\n name: opts.name ?? \"\",\n scope: opts.scope ?? \"admin\",\n };\n if (opts.expiresInDays !== undefined) body.expires_in_days = opts.expiresInDays;\n return this.requestJson<ApiKey>(\"POST\", \"/v1/api-keys\", { body });\n }\n\n /** Revoke an API key. Returns nothing (the server replies `204 No Content`). */\n async revokeApiKey(id: string): Promise<void> {\n await this.request(\"DELETE\", `/v1/api-keys/${encodeURIComponent(id)}`);\n }\n\n // ── Deliveries ─────────────────────────────────────────────────────────────\n\n /** List deliveries. Only the filters you provide are sent as query params. */\n async listDeliveries(opts: ListDeliveriesOptions = {}): Promise<Page<Delivery>> {\n const query: Record<string, unknown> = {};\n if (opts.status !== undefined) query.status = opts.status;\n if (opts.endpointId !== undefined) query.endpoint_id = opts.endpointId;\n if (opts.eventType !== undefined) query.event_type = opts.eventType;\n if (opts.since !== undefined) query.since = opts.since;\n if (opts.until !== undefined) query.until = opts.until;\n if (opts.q !== undefined) query.q = opts.q;\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.limit !== undefined) query.limit = opts.limit;\n return this.requestJson<Page<Delivery>>(\"GET\", \"/v1/deliveries\", { query });\n }\n\n /** Re-queue a delivery for another attempt. */\n async redeliver(deliveryId: string): Promise<Delivery> {\n return this.requestJson<Delivery>(\n \"POST\",\n `/v1/deliveries/${encodeURIComponent(deliveryId)}/redeliver`,\n );\n }\n\n // ── Internals ──────────────────────────────────────────────────────────────\n\n /** Issue a request and parse the JSON response as `T`. Never use for 204s (there's no body). */\n private async requestJson<T>(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<T> {\n const resp = await this.request(method, path, opts);\n return (await resp.json()) as T;\n }\n\n /**\n * The single request path used by every method: builds the URL (+ query), attaches auth, and runs\n * the shared retry loop (connection errors + 429 honoring Retry-After + 5xx, capped exponential\n * backoff), raising a typed {@link WebhookdApiError} on other 4xx/5xx. The caller reads the body.\n */\n private async request(\n method: string,\n path: string,\n opts: RequestOptions = {},\n ): Promise<Response> {\n let url = `${this.baseUrl}${path}`;\n if (opts.query) {\n const qs = new URLSearchParams();\n for (const [key, value] of Object.entries(opts.query)) {\n if (value !== undefined && value !== null) qs.append(key, String(value));\n }\n const suffix = qs.toString();\n if (suffix) url += `?${suffix}`;\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n ...opts.headers,\n };\n let body: string | undefined;\n if (opts.body !== undefined) {\n body = JSON.stringify(opts.body);\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n let lastErr: unknown;\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n let resp: Response;\n try {\n resp = await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (err) {\n lastErr = err;\n if (attempt < this.maxRetries) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new WebhookdError(`request failed: ${String(err)}`);\n }\n\n if (RETRY_STATUSES.has(resp.status) && attempt < this.maxRetries) {\n await sleep(retryAfterMs(resp) ?? backoffMs(attempt));\n continue;\n }\n if (resp.status >= 400) throw await apiError(resp);\n return resp;\n }\n throw new WebhookdError(`request failed after retries: ${String(lastErr)}`);\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(2000, 200 * 2 ** attempt);\n}\n\nfunction retryAfterMs(resp: Response): number | null {\n const raw = resp.headers.get(\"Retry-After\");\n if (raw && /^\\d+$/.test(raw)) return Number(raw) * 1000;\n return null;\n}\n\nasync function apiError(resp: Response): Promise<WebhookdApiError> {\n // Read the body once; a non-envelope (or non-JSON) error body falls back to the raw text.\n const text = await resp.text();\n let code = \"error\";\n let message = text;\n try {\n const data = JSON.parse(text) as { error?: { code?: string; message?: string } };\n code = data.error?.code ?? code;\n message = data.error?.message ?? message;\n } catch {\n // not JSON — keep the raw text as the message\n }\n return new WebhookdApiError(resp.status, code, message);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * @nimbusnexus/webhooks-sdk — the official TypeScript client for NimbusNexus Webhooks.\n *\n * - `verify` — verify an incoming webhook's HMAC signature (for subscribers).\n * - `WebhookdClient` — publish events to webhookd (for producers).\n */\nexport { DEFAULT_TOLERANCE_SECONDS, sign, verify } from \"./signature\";\nexport type { VerifyOptions } from \"./signature\";\nexport { WebhookdClient } from \"./client\";\nexport type {\n ClientOptions,\n PublishOptions,\n WebhookdEvent,\n Subscription,\n Endpoint,\n ApiKey,\n Delivery,\n Page,\n CreateEndpointOptions,\n ListEndpointsOptions,\n EndpointPatch,\n CreateApiKeyOptions,\n ListDeliveriesOptions,\n} from \"./client\";\nexport { WebhookdApiError, WebhookdError } from \"./errors\";\n\n// Keep in lockstep with package.json + the release tag (see publish.yml's bump checklist).\nexport const VERSION = \"0.2.0\";\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbusnexus/webhooks-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Official TypeScript SDK for NimbusNexus Webhooks — publish events + verify webhook signatures.",
6
6
  "license": "MIT",