@hostwebhook/node 1.3.0 → 1.4.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/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
29
29
  var index_exports = {};
30
30
  __export(index_exports, {
31
31
  HostWebhook: () => HostWebhook,
32
+ PipelineStream: () => PipelineStream,
32
33
  SendError: () => SendError,
33
34
  VerificationError: () => VerificationError,
34
35
  createNestGuard: () => createNestGuard,
@@ -207,24 +208,26 @@ var SendError = class extends Error {
207
208
  // src/client.ts
208
209
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
209
210
  var DEFAULT_TIMEOUT = 1e4;
210
- var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, waitForStream_fn;
211
+ var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
211
212
  var HostWebhook = class {
212
213
  constructor(config) {
213
214
  __privateAdd(this, _HostWebhook_instances);
215
+ __privateAdd(this, _apiKey);
214
216
  __privateAdd(this, _token);
215
217
  __privateAdd(this, _baseUrl);
216
218
  __privateAdd(this, _signingSecret);
217
219
  __privateAdd(this, _signature);
218
220
  __privateAdd(this, _defaultHeaders);
219
221
  __privateAdd(this, _timeoutMs);
220
- if (!config.token) {
221
- throw new Error("HostWebhook: token is required");
222
+ if (!config.apiKey && !config.token) {
223
+ throw new Error("HostWebhook: apiKey or token is required");
222
224
  }
223
225
  if (config.signingSecret && config.signature) {
224
226
  throw new Error(
225
227
  "HostWebhook: use either signingSecret or signature, not both"
226
228
  );
227
229
  }
230
+ __privateSet(this, _apiKey, config.apiKey);
228
231
  __privateSet(this, _token, config.token);
229
232
  __privateSet(this, _baseUrl, (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""));
230
233
  __privateSet(this, _signingSecret, config.signingSecret);
@@ -235,91 +238,29 @@ var HostWebhook = class {
235
238
  /**
236
239
  * Send a webhook payload through HostWebhook.
237
240
  *
238
- * The payload is JSON-stringified and POST'd to the ingress endpoint.
239
- * HostWebhook then handles delivery to the target URL with retries,
240
- * monitoring, transforms, and signing.
241
+ * @overload send(payload, options?) — uses default token from constructor
242
+ * @overload send(token, payload, options?) — specify token per call
241
243
  *
242
- * @param payload - The webhook payload (will be JSON.stringify'd)
243
- * @param options - Optional per-request configuration
244
- * @returns The event ID assigned by HostWebhook
245
244
  * @throws {SendError} If the API returns a non-2xx status
245
+ * @throws {Error} If no token is available
246
246
  */
247
- async send(payload, options) {
248
- const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
249
- let tz = "";
250
- try {
251
- tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
252
- } catch {
253
- }
254
- const headers = {
255
- "Content-Type": options?.contentType ?? "application/json",
256
- "User-Agent": `HostWebhook-SDK/${"1.3.0"}`,
257
- "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
258
- ...tz ? { "X-HostWebhook-Timezone": tz } : {},
259
- ...__privateGet(this, _defaultHeaders),
260
- ...options?.headers
261
- };
262
- if (__privateGet(this, _signature)?.type === "static") {
263
- const { secret, header } = __privateGet(this, _signature);
264
- if (header) {
265
- headers[header] = secret;
266
- } else {
267
- headers["Authorization"] = `Bearer ${secret}`;
268
- }
269
- } else if (__privateGet(this, _signingSecret)) {
270
- const timestamp = Math.floor(Date.now() / 1e3);
271
- const sig = (0, import_crypto2.createHmac)("sha256", __privateGet(this, _signingSecret)).update(`${timestamp}.${body}`).digest("hex");
272
- headers["X-HostWebhook-Signature"] = `t=${timestamp},v1=${sig}`;
273
- }
274
- const url = `${__privateGet(this, _baseUrl)}/api/in/${__privateGet(this, _token)}`;
275
- const controller = new AbortController();
276
- const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
277
- let ingressResult;
278
- try {
279
- const res = await fetch(url, {
280
- method: "POST",
281
- headers,
282
- body,
283
- signal: controller.signal
284
- });
285
- const text = await res.text();
286
- if (!res.ok) {
287
- throw new SendError(
288
- `HostWebhook ingress error (${res.status}): ${text}`,
289
- res.status,
290
- text
291
- );
292
- }
293
- try {
294
- ingressResult = JSON.parse(text);
295
- } catch {
296
- ingressResult = { eventId: text };
297
- }
298
- } catch (err) {
299
- if (err instanceof SendError) throw err;
300
- if (err instanceof DOMException && err.name === "AbortError") {
301
- throw new SendError(
302
- `HostWebhook request timed out after ${__privateGet(this, _timeoutMs)}ms`,
303
- 0,
304
- ""
305
- );
247
+ async send(tokenOrPayload, payloadOrOptions, maybeOptions) {
248
+ let token;
249
+ let payload;
250
+ let options;
251
+ if (typeof tokenOrPayload === "string") {
252
+ token = tokenOrPayload;
253
+ payload = payloadOrOptions ?? {};
254
+ options = maybeOptions;
255
+ } else {
256
+ if (!__privateGet(this, _token)) {
257
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument: send(token, payload)");
306
258
  }
307
- throw err;
308
- } finally {
309
- clearTimeout(timer);
310
- }
311
- if (ingressResult.syncStatus !== void 0) {
312
- if (options?.onResult) options.onResult(ingressResult);
313
- return ingressResult;
259
+ token = __privateGet(this, _token);
260
+ payload = tokenOrPayload;
261
+ options = payloadOrOptions;
314
262
  }
315
- if (options?.waitForResult && ingressResult.streamToken) {
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
- });
321
- }
322
- return ingressResult;
263
+ return __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, payload, options);
323
264
  }
324
265
  /**
325
266
  * Convenience: send multiple payloads in parallel.
@@ -334,41 +275,60 @@ var HostWebhook = class {
334
275
  /**
335
276
  * Send a webhook with file attachments.
336
277
  *
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.
278
+ * Requires `apiKey` with `canvas:write` scope for file operations.
279
+ * Files are uploaded via presigned URLs, then references are injected into the payload.
339
280
  *
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
281
+ * @overload sendWithFiles(payload, files, options?) — uses default token
282
+ * @overload sendWithFiles(token, payload, files, options?) — specify token per call
343
283
  *
344
284
  * @example
345
285
  * ```ts
346
- * const hw = new HostWebhook({ token: 'your_token' });
286
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
347
287
  * await hw.sendWithFiles(
348
- * { orderId: '123', type: 'invoice' },
288
+ * { orderId: '123' },
349
289
  * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
350
290
  * );
351
291
  * ```
352
292
  */
353
- async sendWithFiles(payload, files, options) {
293
+ async sendWithFiles(tokenOrPayload, payloadOrFiles, filesOrOptions, maybeOptions) {
294
+ let token;
295
+ let payload;
296
+ let files;
297
+ let options;
298
+ if (typeof tokenOrPayload === "string") {
299
+ token = tokenOrPayload;
300
+ payload = payloadOrFiles;
301
+ files = filesOrOptions ?? {};
302
+ options = maybeOptions;
303
+ } else {
304
+ if (!__privateGet(this, _token)) {
305
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument");
306
+ }
307
+ token = __privateGet(this, _token);
308
+ payload = tokenOrPayload;
309
+ files = payloadOrFiles ?? {};
310
+ options = filesOrOptions;
311
+ }
312
+ if (!__privateGet(this, _apiKey)) {
313
+ throw new Error("HostWebhook: apiKey is required for sendWithFiles (needs canvas:write scope)");
314
+ }
354
315
  const enrichedPayload = { ...payload };
355
316
  for (const [fieldName, file] of Object.entries(files)) {
356
317
  const uploadRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/upload-url`, {
357
318
  method: "POST",
358
319
  headers: {
359
320
  "Content-Type": "application/json",
360
- "Authorization": `Bearer ${__privateGet(this, _token)}`
321
+ "Authorization": `Bearer ${__privateGet(this, _apiKey)}`
361
322
  },
362
323
  body: JSON.stringify({ fileName: file.fileName, mimeType: file.mimeType })
363
324
  });
364
325
  if (!uploadRes.ok) throw new SendError(`Failed to get upload URL for ${fieldName}`, uploadRes.status, await uploadRes.text());
365
326
  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 });
327
+ const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: file.data });
368
328
  if (!putRes.ok) throw new SendError(`Failed to upload ${fieldName} to storage`, putRes.status, await putRes.text());
369
329
  const confirmRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/${fileId}/confirm`, {
370
330
  method: "POST",
371
- headers: { "Authorization": `Bearer ${__privateGet(this, _token)}` }
331
+ headers: { "Authorization": `Bearer ${__privateGet(this, _apiKey)}` }
372
332
  });
373
333
  if (!confirmRes.ok) throw new SendError(`Failed to confirm upload for ${fieldName}`, confirmRes.status, await confirmRes.text());
374
334
  const confirmed = await confirmRes.json();
@@ -376,9 +336,51 @@ var HostWebhook = class {
376
336
  _file: { id: confirmed.id, key, originalName: confirmed.originalName, mimeType: confirmed.mimeType, size: confirmed.size }
377
337
  };
378
338
  }
379
- return this.send(enrichedPayload, options);
339
+ return __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, enrichedPayload, options);
340
+ }
341
+ /**
342
+ * Send a webhook and stream pipeline steps as an async iterable.
343
+ *
344
+ * Much simpler than `waitForResult` or `onResult` callbacks.
345
+ * Iteration ends when the pipeline completes. Access `stream.result`
346
+ * after the loop for the final pipeline result.
347
+ *
348
+ * @overload stream(payload) — uses default token
349
+ * @overload stream(token, payload) — specify token per call
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
354
+ * const stream = await hw.stream({ event: 'order.created' });
355
+ *
356
+ * for await (const step of stream) {
357
+ * console.log(`${step.nodeType}: ${step.status}`);
358
+ * }
359
+ * console.log('Done:', stream.result);
360
+ * ```
361
+ */
362
+ async stream(tokenOrPayload, maybePayload) {
363
+ let token;
364
+ let payload;
365
+ if (typeof tokenOrPayload === "string") {
366
+ token = tokenOrPayload;
367
+ payload = maybePayload ?? {};
368
+ } else {
369
+ if (!__privateGet(this, _token)) {
370
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument: stream(token, payload)");
371
+ }
372
+ token = __privateGet(this, _token);
373
+ payload = tokenOrPayload;
374
+ }
375
+ const ingressResult = await __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, payload);
376
+ if (!ingressResult.streamToken) {
377
+ return new PipelineStream([], ingressResult);
378
+ }
379
+ const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${ingressResult.eventId}/stream?token=${ingressResult.streamToken}`;
380
+ return PipelineStream.connect(streamUrl, ingressResult);
380
381
  }
381
382
  };
383
+ _apiKey = new WeakMap();
382
384
  _token = new WeakMap();
383
385
  _baseUrl = new WeakMap();
384
386
  _signingSecret = new WeakMap();
@@ -386,6 +388,88 @@ _signature = new WeakMap();
386
388
  _defaultHeaders = new WeakMap();
387
389
  _timeoutMs = new WeakMap();
388
390
  _HostWebhook_instances = new WeakSet();
391
+ doSend_fn = async function(token, payload, options) {
392
+ const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
393
+ let tz = "";
394
+ try {
395
+ tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
396
+ } catch {
397
+ }
398
+ const headers = {
399
+ "Content-Type": options?.contentType ?? "application/json",
400
+ "User-Agent": `HostWebhook-SDK/${"1.4.1"}`,
401
+ "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
402
+ ...tz ? { "X-HostWebhook-Timezone": tz } : {},
403
+ ...__privateGet(this, _defaultHeaders),
404
+ ...options?.headers
405
+ };
406
+ if (__privateGet(this, _apiKey)) {
407
+ headers["Authorization"] = `Bearer ${__privateGet(this, _apiKey)}`;
408
+ }
409
+ if (!__privateGet(this, _apiKey)) {
410
+ if (__privateGet(this, _signature)?.type === "static") {
411
+ const { secret, header } = __privateGet(this, _signature);
412
+ if (header) {
413
+ headers[header] = secret;
414
+ } else {
415
+ headers["Authorization"] = `Bearer ${secret}`;
416
+ }
417
+ } else if (__privateGet(this, _signingSecret)) {
418
+ const timestamp = Math.floor(Date.now() / 1e3);
419
+ const sig = (0, import_crypto2.createHmac)("sha256", __privateGet(this, _signingSecret)).update(`${timestamp}.${body}`).digest("hex");
420
+ headers["X-HostWebhook-Signature"] = `t=${timestamp},v1=${sig}`;
421
+ }
422
+ }
423
+ const url = `${__privateGet(this, _baseUrl)}/api/in/${token}`;
424
+ const controller = new AbortController();
425
+ const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
426
+ let ingressResult;
427
+ try {
428
+ const res = await fetch(url, {
429
+ method: "POST",
430
+ headers,
431
+ body,
432
+ signal: controller.signal
433
+ });
434
+ const text = await res.text();
435
+ if (!res.ok) {
436
+ throw new SendError(
437
+ `HostWebhook ingress error (${res.status}): ${text}`,
438
+ res.status,
439
+ text
440
+ );
441
+ }
442
+ try {
443
+ ingressResult = JSON.parse(text);
444
+ } catch {
445
+ ingressResult = { eventId: text };
446
+ }
447
+ } catch (err) {
448
+ if (err instanceof SendError) throw err;
449
+ if (err instanceof DOMException && err.name === "AbortError") {
450
+ throw new SendError(
451
+ `HostWebhook request timed out after ${__privateGet(this, _timeoutMs)}ms`,
452
+ 0,
453
+ ""
454
+ );
455
+ }
456
+ throw err;
457
+ } finally {
458
+ clearTimeout(timer);
459
+ }
460
+ if (ingressResult.syncStatus !== void 0) {
461
+ if (options?.onResult) options.onResult(ingressResult);
462
+ return ingressResult;
463
+ }
464
+ if (options?.waitForResult && ingressResult.streamToken) {
465
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
466
+ }
467
+ if (options?.onResult && ingressResult.streamToken) {
468
+ __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
469
+ });
470
+ }
471
+ return ingressResult;
472
+ };
389
473
  waitForStream_fn = async function(ingressResult, onStep) {
390
474
  const { eventId, streamToken } = ingressResult;
391
475
  const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
@@ -450,6 +534,114 @@ waitForStream_fn = async function(ingressResult, onStep) {
450
534
  clearTimeout(timer);
451
535
  }
452
536
  };
537
+ var _steps, _pendingSteps, _done, _doneResult, _resolve, _reader, _abortController;
538
+ var _PipelineStream = class _PipelineStream {
539
+ constructor(steps, result) {
540
+ __privateAdd(this, _steps);
541
+ __privateAdd(this, _pendingSteps, []);
542
+ __privateAdd(this, _done, false);
543
+ __privateAdd(this, _doneResult, null);
544
+ __privateAdd(this, _resolve, null);
545
+ __privateAdd(this, _reader, null);
546
+ __privateAdd(this, _abortController, null);
547
+ __privateSet(this, _steps, steps);
548
+ this.result = result;
549
+ __privateSet(this, _done, true);
550
+ }
551
+ static async connect(streamUrl, ingressResult) {
552
+ const stream = new _PipelineStream([], ingressResult);
553
+ __privateSet(stream, _done, false);
554
+ const controller = new AbortController();
555
+ __privateSet(stream, _abortController, controller);
556
+ const timer = setTimeout(() => controller.abort(), 125e3);
557
+ try {
558
+ const res = await fetch(streamUrl, { signal: controller.signal });
559
+ if (!res.ok) {
560
+ clearTimeout(timer);
561
+ __privateSet(stream, _done, true);
562
+ return stream;
563
+ }
564
+ __privateSet(stream, _reader, res.body.getReader());
565
+ const decoder = new TextDecoder();
566
+ let buffer = "";
567
+ let currentEvent = "";
568
+ (async () => {
569
+ var _a, _b, _c, _d;
570
+ try {
571
+ while (true) {
572
+ const { done, value } = await __privateGet(stream, _reader).read();
573
+ if (done) break;
574
+ buffer += decoder.decode(value, { stream: true });
575
+ const lines = buffer.split("\n");
576
+ buffer = lines.pop();
577
+ for (const line of lines) {
578
+ if (line.startsWith("event: ")) {
579
+ currentEvent = line.slice(7).trim();
580
+ } else if (line.startsWith("data: ")) {
581
+ try {
582
+ const parsed = JSON.parse(line.slice(6));
583
+ if (currentEvent === "step") {
584
+ __privateGet(stream, _steps).push(parsed);
585
+ __privateGet(stream, _pendingSteps).push(parsed);
586
+ (_a = __privateGet(stream, _resolve)) == null ? void 0 : _a.call(stream);
587
+ } else if (currentEvent === "done") {
588
+ __privateSet(stream, _doneResult, parsed);
589
+ stream.result = {
590
+ ...ingressResult,
591
+ steps: __privateGet(stream, _steps),
592
+ syncStatus: parsed.status ?? 200,
593
+ ...parsed.body ?? parsed
594
+ };
595
+ __privateSet(stream, _done, true);
596
+ (_b = __privateGet(stream, _resolve)) == null ? void 0 : _b.call(stream);
597
+ return;
598
+ } else if (currentEvent === "timeout") {
599
+ stream.result = { ...ingressResult, steps: __privateGet(stream, _steps), syncStatus: 408, error: "Pipeline timeout" };
600
+ __privateSet(stream, _done, true);
601
+ (_c = __privateGet(stream, _resolve)) == null ? void 0 : _c.call(stream);
602
+ return;
603
+ }
604
+ } catch {
605
+ }
606
+ currentEvent = "";
607
+ }
608
+ }
609
+ }
610
+ } catch {
611
+ } finally {
612
+ clearTimeout(timer);
613
+ __privateSet(stream, _done, true);
614
+ (_d = __privateGet(stream, _resolve)) == null ? void 0 : _d.call(stream);
615
+ }
616
+ })();
617
+ } catch {
618
+ clearTimeout(timer);
619
+ __privateSet(stream, _done, true);
620
+ }
621
+ return stream;
622
+ }
623
+ async *[Symbol.asyncIterator]() {
624
+ while (true) {
625
+ if (__privateGet(this, _pendingSteps).length > 0) {
626
+ yield __privateGet(this, _pendingSteps).shift();
627
+ continue;
628
+ }
629
+ if (__privateGet(this, _done)) return;
630
+ await new Promise((resolve) => {
631
+ __privateSet(this, _resolve, resolve);
632
+ });
633
+ __privateSet(this, _resolve, null);
634
+ }
635
+ }
636
+ };
637
+ _steps = new WeakMap();
638
+ _pendingSteps = new WeakMap();
639
+ _done = new WeakMap();
640
+ _doneResult = new WeakMap();
641
+ _resolve = new WeakMap();
642
+ _reader = new WeakMap();
643
+ _abortController = new WeakMap();
644
+ var PipelineStream = _PipelineStream;
453
645
  async function send(config, payload, options) {
454
646
  const client = new HostWebhook(config);
455
647
  return client.send(payload, options);
@@ -457,6 +649,7 @@ async function send(config, payload, options) {
457
649
  // Annotate the CommonJS export names for ESM import in node:
458
650
  0 && (module.exports = {
459
651
  HostWebhook,
652
+ PipelineStream,
460
653
  SendError,
461
654
  VerificationError,
462
655
  createNestGuard,