@hostwebhook/node 1.0.7 → 1.1.1

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
@@ -102,6 +102,35 @@ const hw = new HostWebhook({ token: 'your_ingress_token' });
102
102
  const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
103
103
  ```
104
104
 
105
+ ### Wait for pipeline result (SSE streaming)
106
+
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
+
109
+ ```ts
110
+ const result = await hw.send(
111
+ { event: 'order.created', orderId: '123' },
112
+ { waitForResult: true },
113
+ );
114
+
115
+ console.log(result.syncStatus); // 200 = delivered, 422 = validation failed, 502 = failed, 408 = timeout
116
+ console.log(result.steps); // array of pipeline steps with nodeType, status, durationMs
117
+ console.log(result.response); // response body from target URL
118
+ ```
119
+
120
+ Works with any endpoint regardless of its response mode. Max wait: ~120 seconds.
121
+
122
+ ### Deduplication detection
123
+
124
+ When an event is deduplicated, the response includes `deduplicated: true`:
125
+
126
+ ```ts
127
+ const result = await hw.send({ event: 'order.created', orderId: '123' });
128
+
129
+ if (result.deduplicated) {
130
+ console.log('Duplicate! Original event:', result.originalEventId);
131
+ }
132
+ ```
133
+
105
134
  ### Batch
106
135
 
107
136
  ```ts
@@ -129,9 +158,19 @@ const { eventId } = await send(
129
158
  | `token` | `string` | *required* | Endpoint ingress token |
130
159
  | `baseUrl` | `string` | `https://api.hostwebhook.com` | API base URL |
131
160
  | `signingSecret` | `string` | — | Signs outgoing requests with HMAC-SHA256 |
161
+ | `signature` | `StaticSignature` | — | Static secret signature (alternative to HMAC) |
132
162
  | `defaultHeaders` | `Record<string, string>` | — | Headers added to every request |
133
163
  | `timeoutMs` | `number` | `10000` | Request timeout in ms |
134
164
 
165
+ ### SendOptions
166
+
167
+ | Option | Type | Default | Description |
168
+ |--------|------|---------|-------------|
169
+ | `headers` | `Record<string, string>` | — | Per-request headers |
170
+ | `contentType` | `string` | `application/json` | Override content type |
171
+ | `rawBody` | `string \| Buffer` | — | Send raw body instead of JSON |
172
+ | `waitForResult` | `boolean` | `false` | Wait for pipeline result via SSE stream |
173
+
135
174
  ## Error Handling
136
175
 
137
176
  ```ts
package/dist/index.d.mts CHANGED
@@ -184,6 +184,18 @@ interface HostWebhookConfig {
184
184
  /** Request timeout in milliseconds. Default: 10000 (10s). */
185
185
  timeoutMs?: number;
186
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
+ }
187
199
  /** Options for a single send call. */
188
200
  interface SendOptions {
189
201
  /** Custom headers for this specific request (merged with defaultHeaders). */
@@ -195,25 +207,36 @@ interface SendOptions {
195
207
  * When set, the `payload` parameter to `send()` is ignored.
196
208
  */
197
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;
198
217
  }
199
218
  /** Result returned after a successful send. */
200
219
  interface SendResult {
201
220
  /** The event ID assigned by HostWebhook. */
202
221
  eventId: string;
203
- /** Sync mode: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
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) */
204
225
  syncStatus?: number;
205
- /** Sync mode: delivery status ('delivered') */
226
+ /** Sync/stream: delivery status ('delivered') */
206
227
  deliveryStatus?: string;
207
- /** Sync mode: HTTP status code from the target URL */
228
+ /** Sync/stream: HTTP status code from the target URL */
208
229
  statusCode?: number;
209
- /** Sync mode: response body from the target URL (truncated to 4KB) */
230
+ /** Sync/stream: response body from the target URL (truncated to 4KB) */
210
231
  response?: string;
211
- /** Sync mode: delivery latency in ms */
232
+ /** Sync/stream: delivery latency in ms */
212
233
  latencyMs?: number;
213
- /** Sync mode: error message if pipeline/delivery failed */
234
+ /** Sync/stream: error message if pipeline/delivery failed */
214
235
  error?: string;
215
- /** Sync mode: validation error messages (schema validator) */
236
+ /** Sync/stream: validation error messages (schema validator) */
216
237
  message?: string[];
238
+ /** SSE stream: pipeline execution steps (only when waitForResult is used) */
239
+ steps?: PipelineStep[];
217
240
  /** Any additional fields from the sync response */
218
241
  [key: string]: unknown;
219
242
  }
@@ -269,4 +292,4 @@ declare class HostWebhook {
269
292
  */
270
293
  declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
271
294
 
272
- export { HostWebhook, type HostWebhookConfig, SendError, type SendOptions, type SendResult, type StaticSignature, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
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 CHANGED
@@ -184,6 +184,18 @@ interface HostWebhookConfig {
184
184
  /** Request timeout in milliseconds. Default: 10000 (10s). */
185
185
  timeoutMs?: number;
186
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
+ }
187
199
  /** Options for a single send call. */
188
200
  interface SendOptions {
189
201
  /** Custom headers for this specific request (merged with defaultHeaders). */
@@ -195,25 +207,36 @@ interface SendOptions {
195
207
  * When set, the `payload` parameter to `send()` is ignored.
196
208
  */
197
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;
198
217
  }
199
218
  /** Result returned after a successful send. */
200
219
  interface SendResult {
201
220
  /** The event ID assigned by HostWebhook. */
202
221
  eventId: string;
203
- /** Sync mode: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
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) */
204
225
  syncStatus?: number;
205
- /** Sync mode: delivery status ('delivered') */
226
+ /** Sync/stream: delivery status ('delivered') */
206
227
  deliveryStatus?: string;
207
- /** Sync mode: HTTP status code from the target URL */
228
+ /** Sync/stream: HTTP status code from the target URL */
208
229
  statusCode?: number;
209
- /** Sync mode: response body from the target URL (truncated to 4KB) */
230
+ /** Sync/stream: response body from the target URL (truncated to 4KB) */
210
231
  response?: string;
211
- /** Sync mode: delivery latency in ms */
232
+ /** Sync/stream: delivery latency in ms */
212
233
  latencyMs?: number;
213
- /** Sync mode: error message if pipeline/delivery failed */
234
+ /** Sync/stream: error message if pipeline/delivery failed */
214
235
  error?: string;
215
- /** Sync mode: validation error messages (schema validator) */
236
+ /** Sync/stream: validation error messages (schema validator) */
216
237
  message?: string[];
238
+ /** SSE stream: pipeline execution steps (only when waitForResult is used) */
239
+ steps?: PipelineStep[];
217
240
  /** Any additional fields from the sync response */
218
241
  [key: string]: unknown;
219
242
  }
@@ -269,4 +292,4 @@ declare class HostWebhook {
269
292
  */
270
293
  declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
271
294
 
272
- export { HostWebhook, type HostWebhookConfig, SendError, type SendOptions, type SendResult, type StaticSignature, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
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.js CHANGED
@@ -23,6 +23,7 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
23
23
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
24
24
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
25
25
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
26
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
26
27
 
27
28
  // src/index.ts
28
29
  var index_exports = {};
@@ -206,9 +207,10 @@ var SendError = class extends Error {
206
207
  // src/client.ts
207
208
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
208
209
  var DEFAULT_TIMEOUT = 1e4;
209
- var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs;
210
+ var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, waitForStream_fn;
210
211
  var HostWebhook = class {
211
212
  constructor(config) {
213
+ __privateAdd(this, _HostWebhook_instances);
212
214
  __privateAdd(this, _token);
213
215
  __privateAdd(this, _baseUrl);
214
216
  __privateAdd(this, _signingSecret);
@@ -251,7 +253,7 @@ var HostWebhook = class {
251
253
  }
252
254
  const headers = {
253
255
  "Content-Type": options?.contentType ?? "application/json",
254
- "User-Agent": `HostWebhook-SDK/${"1.0.6"}`,
256
+ "User-Agent": `HostWebhook-SDK/${"1.1.0"}`,
255
257
  "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
256
258
  ...tz ? { "X-HostWebhook-Timezone": tz } : {},
257
259
  ...__privateGet(this, _defaultHeaders),
@@ -272,6 +274,7 @@ var HostWebhook = class {
272
274
  const url = `${__privateGet(this, _baseUrl)}/api/in/${__privateGet(this, _token)}`;
273
275
  const controller = new AbortController();
274
276
  const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
277
+ let ingressResult;
275
278
  try {
276
279
  const res = await fetch(url, {
277
280
  method: "POST",
@@ -288,9 +291,9 @@ var HostWebhook = class {
288
291
  );
289
292
  }
290
293
  try {
291
- return JSON.parse(text);
294
+ ingressResult = JSON.parse(text);
292
295
  } catch {
293
- return { eventId: text };
296
+ ingressResult = { eventId: text };
294
297
  }
295
298
  } catch (err) {
296
299
  if (err instanceof SendError) throw err;
@@ -305,6 +308,11 @@ var HostWebhook = class {
305
308
  } finally {
306
309
  clearTimeout(timer);
307
310
  }
311
+ if (ingressResult.syncStatus !== void 0) return ingressResult;
312
+ if (options?.waitForResult && ingressResult.streamToken) {
313
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult);
314
+ }
315
+ return ingressResult;
308
316
  }
309
317
  /**
310
318
  * Convenience: send multiple payloads in parallel.
@@ -323,6 +331,70 @@ _signingSecret = new WeakMap();
323
331
  _signature = new WeakMap();
324
332
  _defaultHeaders = new WeakMap();
325
333
  _timeoutMs = new WeakMap();
334
+ _HostWebhook_instances = new WeakSet();
335
+ waitForStream_fn = async function(ingressResult) {
336
+ const { eventId, streamToken } = ingressResult;
337
+ const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
338
+ const controller = new AbortController();
339
+ const timer = setTimeout(() => controller.abort(), 125e3);
340
+ try {
341
+ const res = await fetch(streamUrl, { signal: controller.signal });
342
+ if (!res.ok) {
343
+ const text = await res.text();
344
+ throw new SendError(`SSE stream error (${res.status}): ${text}`, res.status, text);
345
+ }
346
+ const steps = [];
347
+ let doneResult = null;
348
+ const reader = res.body.getReader();
349
+ const decoder = new TextDecoder();
350
+ let buffer = "";
351
+ let currentEvent = "";
352
+ while (true) {
353
+ const { done, value } = await reader.read();
354
+ if (done) break;
355
+ buffer += decoder.decode(value, { stream: true });
356
+ const lines = buffer.split("\n");
357
+ buffer = lines.pop();
358
+ for (const line of lines) {
359
+ if (line.startsWith("event: ")) {
360
+ currentEvent = line.slice(7).trim();
361
+ } else if (line.startsWith("data: ")) {
362
+ const data = line.slice(6);
363
+ try {
364
+ const parsed = JSON.parse(data);
365
+ if (currentEvent === "step") {
366
+ steps.push(parsed);
367
+ } else if (currentEvent === "done") {
368
+ doneResult = parsed;
369
+ } else if (currentEvent === "timeout") {
370
+ return { ...ingressResult, steps, syncStatus: 408, error: "Pipeline timeout" };
371
+ }
372
+ } catch {
373
+ }
374
+ currentEvent = "";
375
+ }
376
+ }
377
+ if (doneResult) break;
378
+ }
379
+ if (doneResult) {
380
+ return {
381
+ ...ingressResult,
382
+ steps,
383
+ syncStatus: doneResult.status ?? 200,
384
+ ...doneResult.body ?? doneResult
385
+ };
386
+ }
387
+ return { ...ingressResult, steps, syncStatus: 408, error: "Stream ended without result" };
388
+ } catch (err) {
389
+ if (err instanceof SendError) throw err;
390
+ if (err instanceof DOMException && err.name === "AbortError") {
391
+ throw new SendError("Pipeline stream timed out after 125s", 0, "");
392
+ }
393
+ throw err;
394
+ } finally {
395
+ clearTimeout(timer);
396
+ }
397
+ };
326
398
  async function send(config, payload, options) {
327
399
  const client = new HostWebhook(config);
328
400
  return client.send(payload, options);
package/dist/index.mjs CHANGED
@@ -5,6 +5,7 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
5
5
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
6
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
7
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
8
9
 
9
10
  // src/verify.ts
10
11
  import { createHmac, timingSafeEqual } from "crypto";
@@ -173,9 +174,10 @@ var SendError = class extends Error {
173
174
  // src/client.ts
174
175
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
175
176
  var DEFAULT_TIMEOUT = 1e4;
176
- var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs;
177
+ var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, waitForStream_fn;
177
178
  var HostWebhook = class {
178
179
  constructor(config) {
180
+ __privateAdd(this, _HostWebhook_instances);
179
181
  __privateAdd(this, _token);
180
182
  __privateAdd(this, _baseUrl);
181
183
  __privateAdd(this, _signingSecret);
@@ -218,7 +220,7 @@ var HostWebhook = class {
218
220
  }
219
221
  const headers = {
220
222
  "Content-Type": options?.contentType ?? "application/json",
221
- "User-Agent": `HostWebhook-SDK/${"1.0.6"}`,
223
+ "User-Agent": `HostWebhook-SDK/${"1.1.0"}`,
222
224
  "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
223
225
  ...tz ? { "X-HostWebhook-Timezone": tz } : {},
224
226
  ...__privateGet(this, _defaultHeaders),
@@ -239,6 +241,7 @@ var HostWebhook = class {
239
241
  const url = `${__privateGet(this, _baseUrl)}/api/in/${__privateGet(this, _token)}`;
240
242
  const controller = new AbortController();
241
243
  const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
244
+ let ingressResult;
242
245
  try {
243
246
  const res = await fetch(url, {
244
247
  method: "POST",
@@ -255,9 +258,9 @@ var HostWebhook = class {
255
258
  );
256
259
  }
257
260
  try {
258
- return JSON.parse(text);
261
+ ingressResult = JSON.parse(text);
259
262
  } catch {
260
- return { eventId: text };
263
+ ingressResult = { eventId: text };
261
264
  }
262
265
  } catch (err) {
263
266
  if (err instanceof SendError) throw err;
@@ -272,6 +275,11 @@ var HostWebhook = class {
272
275
  } finally {
273
276
  clearTimeout(timer);
274
277
  }
278
+ if (ingressResult.syncStatus !== void 0) return ingressResult;
279
+ if (options?.waitForResult && ingressResult.streamToken) {
280
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult);
281
+ }
282
+ return ingressResult;
275
283
  }
276
284
  /**
277
285
  * Convenience: send multiple payloads in parallel.
@@ -290,6 +298,70 @@ _signingSecret = new WeakMap();
290
298
  _signature = new WeakMap();
291
299
  _defaultHeaders = new WeakMap();
292
300
  _timeoutMs = new WeakMap();
301
+ _HostWebhook_instances = new WeakSet();
302
+ waitForStream_fn = async function(ingressResult) {
303
+ const { eventId, streamToken } = ingressResult;
304
+ const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
305
+ const controller = new AbortController();
306
+ const timer = setTimeout(() => controller.abort(), 125e3);
307
+ try {
308
+ const res = await fetch(streamUrl, { signal: controller.signal });
309
+ if (!res.ok) {
310
+ const text = await res.text();
311
+ throw new SendError(`SSE stream error (${res.status}): ${text}`, res.status, text);
312
+ }
313
+ const steps = [];
314
+ let doneResult = null;
315
+ const reader = res.body.getReader();
316
+ const decoder = new TextDecoder();
317
+ let buffer = "";
318
+ let currentEvent = "";
319
+ while (true) {
320
+ const { done, value } = await reader.read();
321
+ if (done) break;
322
+ buffer += decoder.decode(value, { stream: true });
323
+ const lines = buffer.split("\n");
324
+ buffer = lines.pop();
325
+ for (const line of lines) {
326
+ if (line.startsWith("event: ")) {
327
+ currentEvent = line.slice(7).trim();
328
+ } else if (line.startsWith("data: ")) {
329
+ const data = line.slice(6);
330
+ try {
331
+ const parsed = JSON.parse(data);
332
+ if (currentEvent === "step") {
333
+ steps.push(parsed);
334
+ } else if (currentEvent === "done") {
335
+ doneResult = parsed;
336
+ } else if (currentEvent === "timeout") {
337
+ return { ...ingressResult, steps, syncStatus: 408, error: "Pipeline timeout" };
338
+ }
339
+ } catch {
340
+ }
341
+ currentEvent = "";
342
+ }
343
+ }
344
+ if (doneResult) break;
345
+ }
346
+ if (doneResult) {
347
+ return {
348
+ ...ingressResult,
349
+ steps,
350
+ syncStatus: doneResult.status ?? 200,
351
+ ...doneResult.body ?? doneResult
352
+ };
353
+ }
354
+ return { ...ingressResult, steps, syncStatus: 408, error: "Stream ended without result" };
355
+ } catch (err) {
356
+ if (err instanceof SendError) throw err;
357
+ if (err instanceof DOMException && err.name === "AbortError") {
358
+ throw new SendError("Pipeline stream timed out after 125s", 0, "");
359
+ }
360
+ throw err;
361
+ } finally {
362
+ clearTimeout(timer);
363
+ }
364
+ };
293
365
  async function send(config, payload, options) {
294
366
  const client = new HostWebhook(config);
295
367
  return client.send(payload, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
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",