@hostwebhook/node 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,19 +92,20 @@ Options: `verify(payload, headers, secret, { maxAgeSec: 300 })` — default 300s
92
92
 
93
93
  Send webhook payloads through HostWebhook for reliable delivery with retries and monitoring.
94
94
 
95
- ### Class-based
95
+ ### Fire & forget (default)
96
96
 
97
97
  ```ts
98
98
  import { HostWebhook } from '@hostwebhook/node';
99
99
 
100
100
  const hw = new HostWebhook({ token: 'your_ingress_token' });
101
101
 
102
+ // Returns immediately with the event ID — you don't know what happened in the pipeline
102
103
  const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
103
104
  ```
104
105
 
105
- ### Wait for pipeline result (SSE streaming)
106
+ ### Wait for pipeline result (blocking)
106
107
 
107
- Use `waitForResult` to wait for the full pipeline to complete. The SDK opens an SSE stream behind the scenes and returns the result with all pipeline steps.
108
+ Use `waitForResult` to wait for the full pipeline to complete. The SDK opens an SSE stream behind the scenes and returns the result with all pipeline steps. **This blocks until the pipeline finishes** (~120s max).
108
109
 
109
110
  ```ts
110
111
  const result = await hw.send(
@@ -117,7 +118,34 @@ console.log(result.steps); // array of pipeline steps with nodeType, statu
117
118
  console.log(result.response); // response body from target URL
118
119
  ```
119
120
 
120
- Works with any endpoint regardless of its response mode. Max wait: ~120 seconds.
121
+ ### Non-blocking result (recommended for UIs)
122
+
123
+ Use `onResult` to get the pipeline result in a background callback. `send()` returns immediately so your UI never blocks. Perfect for forms and user-facing apps.
124
+
125
+ ```ts
126
+ const { eventId } = await hw.send(
127
+ { event: 'order.created', orderId: '123' },
128
+ {
129
+ onResult: (result) => {
130
+ // Fires when the pipeline completes (via SSE in background)
131
+ if (result.awaitingApproval) {
132
+ showToast('Awaiting approval from ' + result.approvalNodeName);
133
+ } else if (result.syncStatus === 200) {
134
+ showToast('Delivered successfully');
135
+ } else {
136
+ showToast('Pipeline failed: ' + result.error);
137
+ }
138
+ },
139
+ onStep: (step) => {
140
+ // Optional: fires for each pipeline node as it executes
141
+ updateProgress(step.nodeName, step.status);
142
+ },
143
+ },
144
+ );
145
+ // UI shows "Sent!" immediately here — never blocks
146
+ ```
147
+
148
+ > **Important:** `onResult` requires the endpoint to be in **None** response mode (async). In Sync mode, the server holds the HTTP connection open until the pipeline completes, which blocks `send()` regardless of callbacks. See [Choosing the right mode](#choosing-the-right-mode) below.
121
149
 
122
150
  ### Deduplication detection
123
151
 
@@ -151,6 +179,32 @@ const { eventId } = await send(
151
179
  );
152
180
  ```
153
181
 
182
+ ## Choosing the right mode
183
+
184
+ Your endpoint has a **Response Mode** setting (None or Sync) that affects how `send()` behaves:
185
+
186
+ | Scenario | Endpoint mode | SDK option | Behavior |
187
+ |----------|--------------|------------|----------|
188
+ | **Forms / UIs** | None | `onResult` | `send()` returns instantly. Pipeline result arrives in background callback. **UI never blocks.** |
189
+ | **Backend scripts** | None | `waitForResult` | `send()` blocks until pipeline completes via SSE (~120s max). |
190
+ | **API proxy / Stripe** | Sync | *(none)* | `send()` blocks until pipeline completes via HTTP (~120s max). Result is inline in the response. |
191
+ | **Testing (Postman)** | Sync | *(none)* | Single request, response includes full pipeline result. |
192
+
193
+ > **Warning: Never use Sync mode for user-facing forms or UIs.** Sync mode holds the HTTP connection open on the server, which blocks `send()` and freezes the UI until the pipeline completes. If the pipeline has approval nodes, merge nodes, or long delays, the SDK will timeout. Use **None** mode with `onResult` instead — the user sees "Sent!" immediately and the pipeline result arrives in the background.
194
+
195
+ ### When to use Sync mode
196
+
197
+ - The caller is a **server or script**, not a human looking at a UI
198
+ - The caller **needs the pipeline result in the same HTTP request** (e.g., Stripe expects a specific response body)
199
+ - The pipeline is **fast and linear** (filter -> transform -> action) with no human-gated nodes (approval, merge)
200
+
201
+ ### When to use None mode + onResult
202
+
203
+ - The caller is a **form, UI, or frontend app**
204
+ - You want **instant feedback** ("Sent!") without waiting for the pipeline
205
+ - The pipeline has **approval nodes, merge nodes, or delays**
206
+ - You want **real-time progress** via `onStep` callbacks
207
+
154
208
  ## Configuration
155
209
 
156
210
  | Option | Type | Default | Description |
@@ -169,7 +223,9 @@ const { eventId } = await send(
169
223
  | `headers` | `Record<string, string>` | — | Per-request headers |
170
224
  | `contentType` | `string` | `application/json` | Override content type |
171
225
  | `rawBody` | `string \| Buffer` | — | Send raw body instead of JSON |
172
- | `waitForResult` | `boolean` | `false` | Wait for pipeline result via SSE stream |
226
+ | `waitForResult` | `boolean` | `false` | Wait for pipeline result via SSE stream (blocking) |
227
+ | `onResult` | `(result) => void` | — | Non-blocking pipeline result callback via SSE |
228
+ | `onStep` | `(step) => void` | — | Real-time pipeline step callback (with `onResult` or `waitForResult`) |
173
229
 
174
230
  ## Error Handling
175
231
 
package/dist/index.js CHANGED
@@ -253,7 +253,7 @@ var HostWebhook = class {
253
253
  }
254
254
  const headers = {
255
255
  "Content-Type": options?.contentType ?? "application/json",
256
- "User-Agent": `HostWebhook-SDK/${"1.1.0"}`,
256
+ "User-Agent": `HostWebhook-SDK/${"1.3.0"}`,
257
257
  "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
258
258
  ...tz ? { "X-HostWebhook-Timezone": tz } : {},
259
259
  ...__privateGet(this, _defaultHeaders),
@@ -308,9 +308,16 @@ var HostWebhook = class {
308
308
  } finally {
309
309
  clearTimeout(timer);
310
310
  }
311
- if (ingressResult.syncStatus !== void 0) return ingressResult;
311
+ if (ingressResult.syncStatus !== void 0) {
312
+ if (options?.onResult) options.onResult(ingressResult);
313
+ return ingressResult;
314
+ }
312
315
  if (options?.waitForResult && ingressResult.streamToken) {
313
- return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult);
316
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
317
+ }
318
+ if (options?.onResult && ingressResult.streamToken) {
319
+ __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
320
+ });
314
321
  }
315
322
  return ingressResult;
316
323
  }
@@ -324,6 +331,53 @@ var HostWebhook = class {
324
331
  async sendBatch(payloads, options) {
325
332
  return Promise.all(payloads.map((p) => this.send(p, options)));
326
333
  }
334
+ /**
335
+ * Send a webhook with file attachments.
336
+ *
337
+ * Files are uploaded directly to storage via presigned URLs (zero memory on API),
338
+ * then file references are injected into the payload before sending.
339
+ *
340
+ * @param payload - The webhook payload
341
+ * @param files - Map of field name → file (Buffer or Blob with metadata)
342
+ * @param options - Optional per-request configuration
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * const hw = new HostWebhook({ token: 'your_token' });
347
+ * await hw.sendWithFiles(
348
+ * { orderId: '123', type: 'invoice' },
349
+ * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
350
+ * );
351
+ * ```
352
+ */
353
+ async sendWithFiles(payload, files, options) {
354
+ const enrichedPayload = { ...payload };
355
+ for (const [fieldName, file] of Object.entries(files)) {
356
+ const uploadRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/upload-url`, {
357
+ method: "POST",
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ "Authorization": `Bearer ${__privateGet(this, _token)}`
361
+ },
362
+ body: JSON.stringify({ fileName: file.fileName, mimeType: file.mimeType })
363
+ });
364
+ if (!uploadRes.ok) throw new SendError(`Failed to get upload URL for ${fieldName}`, uploadRes.status, await uploadRes.text());
365
+ const { fileId, uploadUrl, key } = await uploadRes.json();
366
+ const uploadBody = file.data instanceof Blob ? file.data : new Blob([new Uint8Array(file.data)], { type: file.mimeType });
367
+ const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: uploadBody });
368
+ if (!putRes.ok) throw new SendError(`Failed to upload ${fieldName} to storage`, putRes.status, await putRes.text());
369
+ const confirmRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/${fileId}/confirm`, {
370
+ method: "POST",
371
+ headers: { "Authorization": `Bearer ${__privateGet(this, _token)}` }
372
+ });
373
+ if (!confirmRes.ok) throw new SendError(`Failed to confirm upload for ${fieldName}`, confirmRes.status, await confirmRes.text());
374
+ const confirmed = await confirmRes.json();
375
+ enrichedPayload[fieldName] = {
376
+ _file: { id: confirmed.id, key, originalName: confirmed.originalName, mimeType: confirmed.mimeType, size: confirmed.size }
377
+ };
378
+ }
379
+ return this.send(enrichedPayload, options);
380
+ }
327
381
  };
328
382
  _token = new WeakMap();
329
383
  _baseUrl = new WeakMap();
@@ -332,7 +386,7 @@ _signature = new WeakMap();
332
386
  _defaultHeaders = new WeakMap();
333
387
  _timeoutMs = new WeakMap();
334
388
  _HostWebhook_instances = new WeakSet();
335
- waitForStream_fn = async function(ingressResult) {
389
+ waitForStream_fn = async function(ingressResult, onStep) {
336
390
  const { eventId, streamToken } = ingressResult;
337
391
  const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
338
392
  const controller = new AbortController();
@@ -364,6 +418,7 @@ waitForStream_fn = async function(ingressResult) {
364
418
  const parsed = JSON.parse(data);
365
419
  if (currentEvent === "step") {
366
420
  steps.push(parsed);
421
+ if (onStep) onStep(parsed);
367
422
  } else if (currentEvent === "done") {
368
423
  doneResult = parsed;
369
424
  } else if (currentEvent === "timeout") {
package/dist/index.mjs CHANGED
@@ -220,7 +220,7 @@ var HostWebhook = class {
220
220
  }
221
221
  const headers = {
222
222
  "Content-Type": options?.contentType ?? "application/json",
223
- "User-Agent": `HostWebhook-SDK/${"1.1.0"}`,
223
+ "User-Agent": `HostWebhook-SDK/${"1.3.0"}`,
224
224
  "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
225
225
  ...tz ? { "X-HostWebhook-Timezone": tz } : {},
226
226
  ...__privateGet(this, _defaultHeaders),
@@ -275,9 +275,16 @@ var HostWebhook = class {
275
275
  } finally {
276
276
  clearTimeout(timer);
277
277
  }
278
- if (ingressResult.syncStatus !== void 0) return ingressResult;
278
+ if (ingressResult.syncStatus !== void 0) {
279
+ if (options?.onResult) options.onResult(ingressResult);
280
+ return ingressResult;
281
+ }
279
282
  if (options?.waitForResult && ingressResult.streamToken) {
280
- return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult);
283
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
284
+ }
285
+ if (options?.onResult && ingressResult.streamToken) {
286
+ __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
287
+ });
281
288
  }
282
289
  return ingressResult;
283
290
  }
@@ -291,6 +298,53 @@ var HostWebhook = class {
291
298
  async sendBatch(payloads, options) {
292
299
  return Promise.all(payloads.map((p) => this.send(p, options)));
293
300
  }
301
+ /**
302
+ * Send a webhook with file attachments.
303
+ *
304
+ * Files are uploaded directly to storage via presigned URLs (zero memory on API),
305
+ * then file references are injected into the payload before sending.
306
+ *
307
+ * @param payload - The webhook payload
308
+ * @param files - Map of field name → file (Buffer or Blob with metadata)
309
+ * @param options - Optional per-request configuration
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * const hw = new HostWebhook({ token: 'your_token' });
314
+ * await hw.sendWithFiles(
315
+ * { orderId: '123', type: 'invoice' },
316
+ * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
317
+ * );
318
+ * ```
319
+ */
320
+ async sendWithFiles(payload, files, options) {
321
+ const enrichedPayload = { ...payload };
322
+ for (const [fieldName, file] of Object.entries(files)) {
323
+ const uploadRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/upload-url`, {
324
+ method: "POST",
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ "Authorization": `Bearer ${__privateGet(this, _token)}`
328
+ },
329
+ body: JSON.stringify({ fileName: file.fileName, mimeType: file.mimeType })
330
+ });
331
+ if (!uploadRes.ok) throw new SendError(`Failed to get upload URL for ${fieldName}`, uploadRes.status, await uploadRes.text());
332
+ const { fileId, uploadUrl, key } = await uploadRes.json();
333
+ const uploadBody = file.data instanceof Blob ? file.data : new Blob([new Uint8Array(file.data)], { type: file.mimeType });
334
+ const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: uploadBody });
335
+ if (!putRes.ok) throw new SendError(`Failed to upload ${fieldName} to storage`, putRes.status, await putRes.text());
336
+ const confirmRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/${fileId}/confirm`, {
337
+ method: "POST",
338
+ headers: { "Authorization": `Bearer ${__privateGet(this, _token)}` }
339
+ });
340
+ if (!confirmRes.ok) throw new SendError(`Failed to confirm upload for ${fieldName}`, confirmRes.status, await confirmRes.text());
341
+ const confirmed = await confirmRes.json();
342
+ enrichedPayload[fieldName] = {
343
+ _file: { id: confirmed.id, key, originalName: confirmed.originalName, mimeType: confirmed.mimeType, size: confirmed.size }
344
+ };
345
+ }
346
+ return this.send(enrichedPayload, options);
347
+ }
294
348
  };
295
349
  _token = new WeakMap();
296
350
  _baseUrl = new WeakMap();
@@ -299,7 +353,7 @@ _signature = new WeakMap();
299
353
  _defaultHeaders = new WeakMap();
300
354
  _timeoutMs = new WeakMap();
301
355
  _HostWebhook_instances = new WeakSet();
302
- waitForStream_fn = async function(ingressResult) {
356
+ waitForStream_fn = async function(ingressResult, onStep) {
303
357
  const { eventId, streamToken } = ingressResult;
304
358
  const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
305
359
  const controller = new AbortController();
@@ -331,6 +385,7 @@ waitForStream_fn = async function(ingressResult) {
331
385
  const parsed = JSON.parse(data);
332
386
  if (currentEvent === "step") {
333
387
  steps.push(parsed);
388
+ if (onStep) onStep(parsed);
334
389
  } else if (currentEvent === "done") {
335
390
  doneResult = parsed;
336
391
  } else if (currentEvent === "timeout") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Official HostWebhook SDK — verify signatures, parse headers, and send webhooks",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/dist/index.d.mts DELETED
@@ -1,295 +0,0 @@
1
- /**
2
- * Thrown when signature verification fails.
3
- */
4
- declare class VerificationError extends Error {
5
- constructor(message: string);
6
- }
7
- interface VerifyOptions {
8
- /** Max allowed age of the signature timestamp in seconds. Default: 300 (5 min). Set 0 to disable. */
9
- maxAgeSec?: number;
10
- }
11
- /**
12
- * Verify a HostWebhook signature.
13
- *
14
- * Matches the format produced by HostWebhook's delivery processor:
15
- * `X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>`
16
- *
17
- * The HMAC is computed as: HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
18
- *
19
- * @param payload - The raw request body (string or Buffer)
20
- * @param headers - The request headers object
21
- * @param secret - Your signing secret (e.g. `whsec_...`)
22
- * @param options - Optional configuration
23
- * @throws {VerificationError} If the signature is missing, malformed, expired, or invalid
24
- */
25
- declare function verify(payload: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyOptions): void;
26
-
27
- /**
28
- * Typed representation of HostWebhook delivery headers.
29
- */
30
- interface WebhookHeaders {
31
- /** Unique event ID (`X-HostWebhook-Event-Id`) */
32
- eventId: string;
33
- /** Delivery attempt number (`X-HostWebhook-Attempt`) */
34
- attempt: number;
35
- /** Signature timestamp in milliseconds */
36
- timestamp: number;
37
- /** Raw HMAC-SHA256 hex signature */
38
- signature: string;
39
- /** Chain step number, if this is a chained delivery */
40
- chainStep?: number;
41
- /** Original chain payload (base64-decoded JSON) */
42
- chainOriginal?: unknown;
43
- /** Previous chain step response (base64-decoded JSON) */
44
- chainPrevious?: unknown;
45
- /** Chain response metadata (base64-decoded JSON) */
46
- chainResponse?: {
47
- status: number;
48
- body: unknown;
49
- [key: string]: unknown;
50
- };
51
- /** Whether the chain response was truncated */
52
- chainResponseTruncated?: boolean;
53
- /** Original size of the chain response before truncation */
54
- chainResponseOriginalSize?: number;
55
- }
56
- /**
57
- * Parse all `X-HostWebhook-*` headers into a typed object.
58
- *
59
- * @param headers - The raw request headers
60
- * @returns Parsed webhook headers
61
- */
62
- declare function parseHeaders(headers: Record<string, string | string[] | undefined>): WebhookHeaders;
63
-
64
- type NextFn = (err?: unknown) => void;
65
- interface ExpressRequest {
66
- body: string | Buffer;
67
- headers: Record<string, string | string[] | undefined>;
68
- }
69
- interface ExpressResponse {
70
- status(code: number): ExpressResponse;
71
- json(body: unknown): void;
72
- }
73
- /**
74
- * Express middleware that verifies HostWebhook signatures.
75
- *
76
- * IMPORTANT: You must use `express.raw({ type: 'application/json' })` before
77
- * this middleware so that `req.body` is a Buffer (not parsed JSON).
78
- *
79
- * @example
80
- * ```ts
81
- * import express from 'express';
82
- * import { expressMiddleware } from '@hostwebhook/node';
83
- *
84
- * const app = express();
85
- * app.post('/webhooks',
86
- * express.raw({ type: 'application/json' }),
87
- * expressMiddleware(process.env.SIGNING_SECRET!),
88
- * (req, res) => {
89
- * const payload = JSON.parse(req.body.toString());
90
- * res.status(200).json({ received: true });
91
- * },
92
- * );
93
- * ```
94
- */
95
- declare function expressMiddleware(secret: string, options?: VerifyOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFn) => void;
96
- /**
97
- * NestJS guard factory. Returns a class that can be used with @UseGuards().
98
- *
99
- * @example
100
- * ```ts
101
- * import { Controller, Post, UseGuards, Req } from '@nestjs/common';
102
- * import { createNestGuard } from '@hostwebhook/node';
103
- *
104
- * const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
105
- *
106
- * @Controller('webhooks')
107
- * export class WebhooksController {
108
- * @Post()
109
- * @UseGuards(WebhookGuard)
110
- * handle(@Req() req: Request) {
111
- * return { received: true };
112
- * }
113
- * }
114
- * ```
115
- */
116
- declare function createNestGuard(secret: string, options?: VerifyOptions): {
117
- new (): {
118
- canActivate(context: {
119
- switchToHttp(): {
120
- getRequest(): ExpressRequest;
121
- };
122
- }): boolean;
123
- };
124
- };
125
- /**
126
- * Fastify preHandler hook that verifies HostWebhook signatures.
127
- *
128
- * @example
129
- * ```ts
130
- * import Fastify from 'fastify';
131
- * import { fastifyHook } from '@hostwebhook/node';
132
- *
133
- * const app = Fastify({ bodyLimit: 1_048_576 });
134
- * app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
135
- *
136
- * app.post('/webhooks', { preHandler: fastifyHook(process.env.SIGNING_SECRET!) }, (req, reply) => {
137
- * reply.send({ received: true });
138
- * });
139
- * ```
140
- */
141
- declare function fastifyHook(secret: string, options?: VerifyOptions): (req: ExpressRequest, reply: ExpressResponse, done: NextFn) => void;
142
-
143
- /** Static secret signature — sends the secret as-is in a header (no HMAC). */
144
- interface StaticSignature {
145
- type: 'static';
146
- /** The secret/token value to send. */
147
- secret: string;
148
- /**
149
- * Header name to send the secret in.
150
- * Default: `Authorization` with `Bearer <secret>` format.
151
- * @example 'X-My-Custom-Key'
152
- */
153
- header?: string;
154
- }
155
- /** Configuration for the HostWebhook client. */
156
- interface HostWebhookConfig {
157
- /** Ingress token from your endpoint settings. */
158
- token: string;
159
- /** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
160
- baseUrl?: string;
161
- /**
162
- * HMAC-SHA256 signing secret. When set, every request is signed with the
163
- * HostWebhook signature format (`X-HostWebhook-Signature: t=<ts>,v1=<hmac>`).
164
- * Pair with `incomingSignatureType: 'hostwebhook'` on the endpoint.
165
- * @deprecated Prefer `signature` for new integrations.
166
- */
167
- signingSecret?: string;
168
- /**
169
- * Signature configuration for incoming webhook verification.
170
- * Cannot be used together with `signingSecret`.
171
- *
172
- * @example Static with default header (Authorization: Bearer)
173
- * ```ts
174
- * { type: 'static', secret: 'my-secret' }
175
- * ```
176
- * @example Static with custom header
177
- * ```ts
178
- * { type: 'static', secret: 'my-secret', header: 'X-My-Custom-Key' }
179
- * ```
180
- */
181
- signature?: StaticSignature;
182
- /** Custom headers to include in every request. */
183
- defaultHeaders?: Record<string, string>;
184
- /** Request timeout in milliseconds. Default: 10000 (10s). */
185
- timeoutMs?: number;
186
- }
187
- /** A single pipeline execution step received via SSE stream. */
188
- interface PipelineStep {
189
- eventId?: string;
190
- nodeType: string;
191
- nodeId: string;
192
- nodeName?: string;
193
- status: 'running' | 'success' | 'error' | 'blocked';
194
- statusCode?: number;
195
- durationMs?: number;
196
- error?: string;
197
- lastPayload?: Record<string, unknown>;
198
- }
199
- /** Options for a single send call. */
200
- interface SendOptions {
201
- /** Custom headers for this specific request (merged with defaultHeaders). */
202
- headers?: Record<string, string>;
203
- /** Override the Content-Type. Default: `application/json`. */
204
- contentType?: string;
205
- /**
206
- * Send raw string/Buffer body instead of JSON-stringifying a payload object.
207
- * When set, the `payload` parameter to `send()` is ignored.
208
- */
209
- rawBody?: string | Buffer;
210
- /**
211
- * Wait for the full pipeline result via SSE streaming.
212
- * The SDK will POST the payload, then open an SSE stream to receive
213
- * pipeline steps and the final result. Returns when the pipeline completes.
214
- * Timeout: ~125s (slightly above the server's 120s SSE timeout).
215
- */
216
- waitForResult?: boolean;
217
- }
218
- /** Result returned after a successful send. */
219
- interface SendResult {
220
- /** The event ID assigned by HostWebhook. */
221
- eventId: string;
222
- /** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
223
- streamToken?: string;
224
- /** Sync/stream: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
225
- syncStatus?: number;
226
- /** Sync/stream: delivery status ('delivered') */
227
- deliveryStatus?: string;
228
- /** Sync/stream: HTTP status code from the target URL */
229
- statusCode?: number;
230
- /** Sync/stream: response body from the target URL (truncated to 4KB) */
231
- response?: string;
232
- /** Sync/stream: delivery latency in ms */
233
- latencyMs?: number;
234
- /** Sync/stream: error message if pipeline/delivery failed */
235
- error?: string;
236
- /** Sync/stream: validation error messages (schema validator) */
237
- message?: string[];
238
- /** SSE stream: pipeline execution steps (only when waitForResult is used) */
239
- steps?: PipelineStep[];
240
- /** Any additional fields from the sync response */
241
- [key: string]: unknown;
242
- }
243
- /** Thrown when the HostWebhook API returns a non-2xx response. */
244
- declare class SendError extends Error {
245
- readonly statusCode: number;
246
- readonly responseBody: string;
247
- constructor(message: string, statusCode: number, responseBody: string);
248
- }
249
-
250
- /**
251
- * HostWebhook client for sending webhooks through the platform.
252
- *
253
- * @example
254
- * ```ts
255
- * const hw = new HostWebhook({ token: 'your_ingress_token' });
256
- * const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
257
- * ```
258
- */
259
- declare class HostWebhook {
260
- #private;
261
- constructor(config: HostWebhookConfig);
262
- /**
263
- * Send a webhook payload through HostWebhook.
264
- *
265
- * The payload is JSON-stringified and POST'd to the ingress endpoint.
266
- * HostWebhook then handles delivery to the target URL with retries,
267
- * monitoring, transforms, and signing.
268
- *
269
- * @param payload - The webhook payload (will be JSON.stringify'd)
270
- * @param options - Optional per-request configuration
271
- * @returns The event ID assigned by HostWebhook
272
- * @throws {SendError} If the API returns a non-2xx status
273
- */
274
- send(payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
275
- /**
276
- * Convenience: send multiple payloads in parallel.
277
- *
278
- * @param payloads - Array of payloads to send
279
- * @param options - Optional per-request configuration (shared for all)
280
- * @returns Array of results (in same order as payloads)
281
- */
282
- sendBatch(payloads: Record<string, unknown>[], options?: SendOptions): Promise<SendResult[]>;
283
- }
284
- /**
285
- * Standalone send function (no class instantiation needed).
286
- *
287
- * @example
288
- * ```ts
289
- * import { send } from '@hostwebhook/node';
290
- * await send({ token: 'your_token' }, { event: 'test' });
291
- * ```
292
- */
293
- declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
294
-
295
- export { HostWebhook, type HostWebhookConfig, type PipelineStep, SendError, type SendOptions, type SendResult, type StaticSignature, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
package/dist/index.d.ts DELETED
@@ -1,295 +0,0 @@
1
- /**
2
- * Thrown when signature verification fails.
3
- */
4
- declare class VerificationError extends Error {
5
- constructor(message: string);
6
- }
7
- interface VerifyOptions {
8
- /** Max allowed age of the signature timestamp in seconds. Default: 300 (5 min). Set 0 to disable. */
9
- maxAgeSec?: number;
10
- }
11
- /**
12
- * Verify a HostWebhook signature.
13
- *
14
- * Matches the format produced by HostWebhook's delivery processor:
15
- * `X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>`
16
- *
17
- * The HMAC is computed as: HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
18
- *
19
- * @param payload - The raw request body (string or Buffer)
20
- * @param headers - The request headers object
21
- * @param secret - Your signing secret (e.g. `whsec_...`)
22
- * @param options - Optional configuration
23
- * @throws {VerificationError} If the signature is missing, malformed, expired, or invalid
24
- */
25
- declare function verify(payload: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyOptions): void;
26
-
27
- /**
28
- * Typed representation of HostWebhook delivery headers.
29
- */
30
- interface WebhookHeaders {
31
- /** Unique event ID (`X-HostWebhook-Event-Id`) */
32
- eventId: string;
33
- /** Delivery attempt number (`X-HostWebhook-Attempt`) */
34
- attempt: number;
35
- /** Signature timestamp in milliseconds */
36
- timestamp: number;
37
- /** Raw HMAC-SHA256 hex signature */
38
- signature: string;
39
- /** Chain step number, if this is a chained delivery */
40
- chainStep?: number;
41
- /** Original chain payload (base64-decoded JSON) */
42
- chainOriginal?: unknown;
43
- /** Previous chain step response (base64-decoded JSON) */
44
- chainPrevious?: unknown;
45
- /** Chain response metadata (base64-decoded JSON) */
46
- chainResponse?: {
47
- status: number;
48
- body: unknown;
49
- [key: string]: unknown;
50
- };
51
- /** Whether the chain response was truncated */
52
- chainResponseTruncated?: boolean;
53
- /** Original size of the chain response before truncation */
54
- chainResponseOriginalSize?: number;
55
- }
56
- /**
57
- * Parse all `X-HostWebhook-*` headers into a typed object.
58
- *
59
- * @param headers - The raw request headers
60
- * @returns Parsed webhook headers
61
- */
62
- declare function parseHeaders(headers: Record<string, string | string[] | undefined>): WebhookHeaders;
63
-
64
- type NextFn = (err?: unknown) => void;
65
- interface ExpressRequest {
66
- body: string | Buffer;
67
- headers: Record<string, string | string[] | undefined>;
68
- }
69
- interface ExpressResponse {
70
- status(code: number): ExpressResponse;
71
- json(body: unknown): void;
72
- }
73
- /**
74
- * Express middleware that verifies HostWebhook signatures.
75
- *
76
- * IMPORTANT: You must use `express.raw({ type: 'application/json' })` before
77
- * this middleware so that `req.body` is a Buffer (not parsed JSON).
78
- *
79
- * @example
80
- * ```ts
81
- * import express from 'express';
82
- * import { expressMiddleware } from '@hostwebhook/node';
83
- *
84
- * const app = express();
85
- * app.post('/webhooks',
86
- * express.raw({ type: 'application/json' }),
87
- * expressMiddleware(process.env.SIGNING_SECRET!),
88
- * (req, res) => {
89
- * const payload = JSON.parse(req.body.toString());
90
- * res.status(200).json({ received: true });
91
- * },
92
- * );
93
- * ```
94
- */
95
- declare function expressMiddleware(secret: string, options?: VerifyOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFn) => void;
96
- /**
97
- * NestJS guard factory. Returns a class that can be used with @UseGuards().
98
- *
99
- * @example
100
- * ```ts
101
- * import { Controller, Post, UseGuards, Req } from '@nestjs/common';
102
- * import { createNestGuard } from '@hostwebhook/node';
103
- *
104
- * const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
105
- *
106
- * @Controller('webhooks')
107
- * export class WebhooksController {
108
- * @Post()
109
- * @UseGuards(WebhookGuard)
110
- * handle(@Req() req: Request) {
111
- * return { received: true };
112
- * }
113
- * }
114
- * ```
115
- */
116
- declare function createNestGuard(secret: string, options?: VerifyOptions): {
117
- new (): {
118
- canActivate(context: {
119
- switchToHttp(): {
120
- getRequest(): ExpressRequest;
121
- };
122
- }): boolean;
123
- };
124
- };
125
- /**
126
- * Fastify preHandler hook that verifies HostWebhook signatures.
127
- *
128
- * @example
129
- * ```ts
130
- * import Fastify from 'fastify';
131
- * import { fastifyHook } from '@hostwebhook/node';
132
- *
133
- * const app = Fastify({ bodyLimit: 1_048_576 });
134
- * app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
135
- *
136
- * app.post('/webhooks', { preHandler: fastifyHook(process.env.SIGNING_SECRET!) }, (req, reply) => {
137
- * reply.send({ received: true });
138
- * });
139
- * ```
140
- */
141
- declare function fastifyHook(secret: string, options?: VerifyOptions): (req: ExpressRequest, reply: ExpressResponse, done: NextFn) => void;
142
-
143
- /** Static secret signature — sends the secret as-is in a header (no HMAC). */
144
- interface StaticSignature {
145
- type: 'static';
146
- /** The secret/token value to send. */
147
- secret: string;
148
- /**
149
- * Header name to send the secret in.
150
- * Default: `Authorization` with `Bearer <secret>` format.
151
- * @example 'X-My-Custom-Key'
152
- */
153
- header?: string;
154
- }
155
- /** Configuration for the HostWebhook client. */
156
- interface HostWebhookConfig {
157
- /** Ingress token from your endpoint settings. */
158
- token: string;
159
- /** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
160
- baseUrl?: string;
161
- /**
162
- * HMAC-SHA256 signing secret. When set, every request is signed with the
163
- * HostWebhook signature format (`X-HostWebhook-Signature: t=<ts>,v1=<hmac>`).
164
- * Pair with `incomingSignatureType: 'hostwebhook'` on the endpoint.
165
- * @deprecated Prefer `signature` for new integrations.
166
- */
167
- signingSecret?: string;
168
- /**
169
- * Signature configuration for incoming webhook verification.
170
- * Cannot be used together with `signingSecret`.
171
- *
172
- * @example Static with default header (Authorization: Bearer)
173
- * ```ts
174
- * { type: 'static', secret: 'my-secret' }
175
- * ```
176
- * @example Static with custom header
177
- * ```ts
178
- * { type: 'static', secret: 'my-secret', header: 'X-My-Custom-Key' }
179
- * ```
180
- */
181
- signature?: StaticSignature;
182
- /** Custom headers to include in every request. */
183
- defaultHeaders?: Record<string, string>;
184
- /** Request timeout in milliseconds. Default: 10000 (10s). */
185
- timeoutMs?: number;
186
- }
187
- /** A single pipeline execution step received via SSE stream. */
188
- interface PipelineStep {
189
- eventId?: string;
190
- nodeType: string;
191
- nodeId: string;
192
- nodeName?: string;
193
- status: 'running' | 'success' | 'error' | 'blocked';
194
- statusCode?: number;
195
- durationMs?: number;
196
- error?: string;
197
- lastPayload?: Record<string, unknown>;
198
- }
199
- /** Options for a single send call. */
200
- interface SendOptions {
201
- /** Custom headers for this specific request (merged with defaultHeaders). */
202
- headers?: Record<string, string>;
203
- /** Override the Content-Type. Default: `application/json`. */
204
- contentType?: string;
205
- /**
206
- * Send raw string/Buffer body instead of JSON-stringifying a payload object.
207
- * When set, the `payload` parameter to `send()` is ignored.
208
- */
209
- rawBody?: string | Buffer;
210
- /**
211
- * Wait for the full pipeline result via SSE streaming.
212
- * The SDK will POST the payload, then open an SSE stream to receive
213
- * pipeline steps and the final result. Returns when the pipeline completes.
214
- * Timeout: ~125s (slightly above the server's 120s SSE timeout).
215
- */
216
- waitForResult?: boolean;
217
- }
218
- /** Result returned after a successful send. */
219
- interface SendResult {
220
- /** The event ID assigned by HostWebhook. */
221
- eventId: string;
222
- /** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
223
- streamToken?: string;
224
- /** Sync/stream: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
225
- syncStatus?: number;
226
- /** Sync/stream: delivery status ('delivered') */
227
- deliveryStatus?: string;
228
- /** Sync/stream: HTTP status code from the target URL */
229
- statusCode?: number;
230
- /** Sync/stream: response body from the target URL (truncated to 4KB) */
231
- response?: string;
232
- /** Sync/stream: delivery latency in ms */
233
- latencyMs?: number;
234
- /** Sync/stream: error message if pipeline/delivery failed */
235
- error?: string;
236
- /** Sync/stream: validation error messages (schema validator) */
237
- message?: string[];
238
- /** SSE stream: pipeline execution steps (only when waitForResult is used) */
239
- steps?: PipelineStep[];
240
- /** Any additional fields from the sync response */
241
- [key: string]: unknown;
242
- }
243
- /** Thrown when the HostWebhook API returns a non-2xx response. */
244
- declare class SendError extends Error {
245
- readonly statusCode: number;
246
- readonly responseBody: string;
247
- constructor(message: string, statusCode: number, responseBody: string);
248
- }
249
-
250
- /**
251
- * HostWebhook client for sending webhooks through the platform.
252
- *
253
- * @example
254
- * ```ts
255
- * const hw = new HostWebhook({ token: 'your_ingress_token' });
256
- * const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
257
- * ```
258
- */
259
- declare class HostWebhook {
260
- #private;
261
- constructor(config: HostWebhookConfig);
262
- /**
263
- * Send a webhook payload through HostWebhook.
264
- *
265
- * The payload is JSON-stringified and POST'd to the ingress endpoint.
266
- * HostWebhook then handles delivery to the target URL with retries,
267
- * monitoring, transforms, and signing.
268
- *
269
- * @param payload - The webhook payload (will be JSON.stringify'd)
270
- * @param options - Optional per-request configuration
271
- * @returns The event ID assigned by HostWebhook
272
- * @throws {SendError} If the API returns a non-2xx status
273
- */
274
- send(payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
275
- /**
276
- * Convenience: send multiple payloads in parallel.
277
- *
278
- * @param payloads - Array of payloads to send
279
- * @param options - Optional per-request configuration (shared for all)
280
- * @returns Array of results (in same order as payloads)
281
- */
282
- sendBatch(payloads: Record<string, unknown>[], options?: SendOptions): Promise<SendResult[]>;
283
- }
284
- /**
285
- * Standalone send function (no class instantiation needed).
286
- *
287
- * @example
288
- * ```ts
289
- * import { send } from '@hostwebhook/node';
290
- * await send({ token: 'your_token' }, { event: 'test' });
291
- * ```
292
- */
293
- declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
294
-
295
- export { HostWebhook, type HostWebhookConfig, type PipelineStep, SendError, type SendOptions, type SendResult, type StaticSignature, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };