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