@hostwebhook/node 1.6.1 → 1.6.3

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
@@ -14,7 +14,7 @@ Requires Node.js 18+.
14
14
 
15
15
  ## Verify Webhook Signatures
16
16
 
17
- HostWebhook signs every delivery with HMAC-SHA256. Use these helpers to verify authenticity and prevent replay attacks.
17
+ HostWebhook signs every delivery with HMAC-SHA256. Use these helpers to verify authenticity and to reject stale signatures (see [What is signed](#what-is-signed)).
18
18
 
19
19
  ### Express
20
20
 
@@ -86,7 +86,9 @@ try {
86
86
  }
87
87
  ```
88
88
 
89
- Options: `verify(payload, headers, secret, { maxAgeSec: 300 })` — default 300s (5 min). Set `0` to disable replay protection.
89
+ Options: `verify(payload, headers, secret, { maxAgeSec: 300 })` — default 300s (5 min). Set `0` to disable the age check.
90
+
91
+ `verify` and the middlewares refuse an empty signing secret (an unset environment variable is the usual cause): with an empty key anyone can compute a valid signature.
90
92
 
91
93
  ## Send Webhooks
92
94
 
@@ -113,7 +115,7 @@ const result = await hw.send(
113
115
  { waitForResult: true },
114
116
  );
115
117
 
116
- console.log(result.syncStatus); // 200 = delivered, 422 = validation failed, 502 = failed, 408 = timeout
118
+ console.log(result.syncStatus); // 200 = delivered, 202 = paused, 422 = validation failed, 500 = failed, 408 = timeout
117
119
  console.log(result.steps); // array of pipeline steps with nodeType, status, durationMs
118
120
  console.log(result.response); // response body from target URL
119
121
  ```
@@ -187,7 +189,7 @@ Your endpoint has a **Response Mode** setting (None or Sync) that affects how `s
187
189
  |----------|--------------|------------|----------|
188
190
  | **Forms / UIs** | None | `onResult` | `send()` returns instantly. Pipeline result arrives in background callback. **UI never blocks.** |
189
191
  | **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. |
192
+ | **API proxy / Stripe** | Sync | *(none)* | `send()` blocks until a Schema Validator or an AI Node in the flow answers (20s at most, 55s with an AI Node; `timeoutMs` is 60s by default, enough for both). A 2xx is the result; a 422, 500 or 408 throws `SendError` with the JSON body in `responseBody`. |
191
193
  | **Testing (Postman)** | Sync | *(none)* | Single request, response includes full pipeline result. |
192
194
 
193
195
  > **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.
@@ -214,7 +216,7 @@ Your endpoint has a **Response Mode** setting (None or Sync) that affects how `s
214
216
  | `signingSecret` | `string` | — | Signs outgoing requests with HMAC-SHA256 |
215
217
  | `signature` | `StaticSignature` | — | Static secret signature (alternative to HMAC) |
216
218
  | `defaultHeaders` | `Record<string, string>` | — | Headers added to every request |
217
- | `timeoutMs` | `number` | `10000` | Request timeout in ms |
219
+ | `timeoutMs` | `number` | `60000` | Request timeout in ms (long enough for a Sync webhook) |
218
220
 
219
221
  ### SendOptions
220
222
 
@@ -259,6 +261,13 @@ X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>
259
261
 
260
262
  HMAC computed as: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`
261
263
 
264
+ ### What is signed
265
+
266
+ The signature covers **only the timestamp and the raw body**. In particular:
267
+
268
+ - The other `X-HostWebhook-*` headers — the event id, the attempt number and every `chain-*` header — are **not signed**. Anyone who can reach your endpoint can add or change them, so use them as hints (for example to deduplicate), never as proof.
269
+ - The timestamp limits how **old** a signature may be (`maxAgeSec`). It does not stop the same signed request from being sent again inside that window: to reject duplicates, remember the event ids you already processed.
270
+
262
271
  ## Support
263
272
 
264
273
  Found a bug or have a feature request? [Report an issue](https://www.hostwebhook.com/#issues) on our website.
package/dist/index.d.mts CHANGED
@@ -16,16 +16,28 @@ interface VerifyOptions {
16
16
  *
17
17
  * The HMAC is computed as: HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
18
18
  *
19
+ * What is signed: ONLY the timestamp and the raw body. The other
20
+ * `X-HostWebhook-*` headers (event id, attempt, chain-*) are NOT covered by the
21
+ * signature: anyone who can reach your endpoint can add or change them. Do not
22
+ * make security decisions from them. And the timestamp only limits how OLD a
23
+ * signature may be (`maxAgeSec`); it does not stop the same signed request from
24
+ * being replayed inside that window. To reject duplicates, remember the event
25
+ * id of what you already processed.
26
+ *
19
27
  * @param payload - The raw request body (string or Buffer)
20
28
  * @param headers - The request headers object
21
29
  * @param secret - Your signing secret (e.g. `whsec_...`)
22
30
  * @param options - Optional configuration
23
- * @throws {VerificationError} If the signature is missing, malformed, expired, or invalid
31
+ * @throws {VerificationError} If the secret is empty, or the signature is missing, malformed, expired, or invalid
24
32
  */
25
33
  declare function verify(payload: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyOptions): void;
26
34
 
27
35
  /**
28
36
  * Typed representation of HostWebhook delivery headers.
37
+ *
38
+ * ⚠️ Only `timestamp` and the body are covered by the signature. The event id,
39
+ * the attempt number and every `chain*` field come from headers that are NOT
40
+ * signed: treat them as hints (for example to deduplicate), never as proof.
29
41
  */
30
42
  interface WebhookHeaders {
31
43
  /** Unique event ID (`X-HostWebhook-Event-Id`) */
@@ -220,7 +232,7 @@ interface HostWebhookConfig {
220
232
  signature?: StaticSignature;
221
233
  /** Custom headers to include in every request. */
222
234
  defaultHeaders?: Record<string, string>;
223
- /** Request timeout in milliseconds. Default: 10000 (10s). */
235
+ /** Request timeout in milliseconds. Default: 60000 (60s), long enough for a Sync webhook with an AI Node (55s at most). */
224
236
  timeoutMs?: number;
225
237
  }
226
238
  /** A single pipeline execution step received via SSE stream. */
@@ -295,7 +307,13 @@ interface SendResult {
295
307
  eventId: string;
296
308
  /** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
297
309
  streamToken?: string;
298
- /** Sync/stream: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
310
+ /**
311
+ * Stream (`waitForResult`, `onResult`, `stream()`): pipeline result status —
312
+ * 200=delivered, 202=paused (approval, merge or a long delay), 422=validation
313
+ * failed, 500=delivery failed, 408=timeout. A webhook in Sync response mode
314
+ * answers with the real HTTP status instead: a non-2xx (422, 500, 408) throws
315
+ * `SendError` with the JSON body in `responseBody`.
316
+ */
299
317
  syncStatus?: number;
300
318
  /** Sync/stream: delivery status ('delivered') */
301
319
  deliveryStatus?: string;
package/dist/index.d.ts CHANGED
@@ -16,16 +16,28 @@ interface VerifyOptions {
16
16
  *
17
17
  * The HMAC is computed as: HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
18
18
  *
19
+ * What is signed: ONLY the timestamp and the raw body. The other
20
+ * `X-HostWebhook-*` headers (event id, attempt, chain-*) are NOT covered by the
21
+ * signature: anyone who can reach your endpoint can add or change them. Do not
22
+ * make security decisions from them. And the timestamp only limits how OLD a
23
+ * signature may be (`maxAgeSec`); it does not stop the same signed request from
24
+ * being replayed inside that window. To reject duplicates, remember the event
25
+ * id of what you already processed.
26
+ *
19
27
  * @param payload - The raw request body (string or Buffer)
20
28
  * @param headers - The request headers object
21
29
  * @param secret - Your signing secret (e.g. `whsec_...`)
22
30
  * @param options - Optional configuration
23
- * @throws {VerificationError} If the signature is missing, malformed, expired, or invalid
31
+ * @throws {VerificationError} If the secret is empty, or the signature is missing, malformed, expired, or invalid
24
32
  */
25
33
  declare function verify(payload: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyOptions): void;
26
34
 
27
35
  /**
28
36
  * Typed representation of HostWebhook delivery headers.
37
+ *
38
+ * ⚠️ Only `timestamp` and the body are covered by the signature. The event id,
39
+ * the attempt number and every `chain*` field come from headers that are NOT
40
+ * signed: treat them as hints (for example to deduplicate), never as proof.
29
41
  */
30
42
  interface WebhookHeaders {
31
43
  /** Unique event ID (`X-HostWebhook-Event-Id`) */
@@ -220,7 +232,7 @@ interface HostWebhookConfig {
220
232
  signature?: StaticSignature;
221
233
  /** Custom headers to include in every request. */
222
234
  defaultHeaders?: Record<string, string>;
223
- /** Request timeout in milliseconds. Default: 10000 (10s). */
235
+ /** Request timeout in milliseconds. Default: 60000 (60s), long enough for a Sync webhook with an AI Node (55s at most). */
224
236
  timeoutMs?: number;
225
237
  }
226
238
  /** A single pipeline execution step received via SSE stream. */
@@ -295,7 +307,13 @@ interface SendResult {
295
307
  eventId: string;
296
308
  /** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
297
309
  streamToken?: string;
298
- /** Sync/stream: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
310
+ /**
311
+ * Stream (`waitForResult`, `onResult`, `stream()`): pipeline result status —
312
+ * 200=delivered, 202=paused (approval, merge or a long delay), 422=validation
313
+ * failed, 500=delivery failed, 408=timeout. A webhook in Sync response mode
314
+ * answers with the real HTTP status instead: a non-2xx (422, 500, 408) throws
315
+ * `SendError` with the JSON body in `responseBody`.
316
+ */
299
317
  syncStatus?: number;
300
318
  /** Sync/stream: delivery status ('delivered') */
301
319
  deliveryStatus?: string;
package/dist/index.js CHANGED
@@ -49,7 +49,15 @@ var VerificationError = class extends Error {
49
49
  this.name = "VerificationError";
50
50
  }
51
51
  };
52
+ function exigirUnSecreto(secret) {
53
+ if (typeof secret !== "string" || secret.trim() === "") {
54
+ throw new VerificationError(
55
+ "The signing secret is empty. Set the signing secret of your endpoint (whsec_\u2026) before verifying"
56
+ );
57
+ }
58
+ }
52
59
  function verify(payload, headers, secret, options) {
60
+ exigirUnSecreto(secret);
53
61
  const sigHeader = getHeader(headers, "x-hostwebhook-signature");
54
62
  if (!sigHeader) {
55
63
  throw new VerificationError("Missing X-HostWebhook-Signature header");
@@ -159,6 +167,7 @@ function parseHeaders(headers) {
159
167
 
160
168
  // src/middleware.ts
161
169
  function expressMiddleware(secret, options) {
170
+ exigirUnSecreto(secret);
162
171
  return (req, res, next) => {
163
172
  try {
164
173
  verify(req.body, req.headers, secret, options);
@@ -173,6 +182,7 @@ function expressMiddleware(secret, options) {
173
182
  };
174
183
  }
175
184
  function createNestGuard(secret, options) {
185
+ exigirUnSecreto(secret);
176
186
  return class HostWebhookGuard {
177
187
  canActivate(context) {
178
188
  const req = context.switchToHttp().getRequest();
@@ -182,6 +192,7 @@ function createNestGuard(secret, options) {
182
192
  };
183
193
  }
184
194
  function fastifyHook(secret, options) {
195
+ exigirUnSecreto(secret);
185
196
  return (req, _reply, done) => {
186
197
  try {
187
198
  verify(req.body, req.headers, secret, options);
@@ -213,7 +224,7 @@ var SendError = class extends Error {
213
224
 
214
225
  // src/client.ts
215
226
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
216
- var DEFAULT_TIMEOUT = 1e4;
227
+ var DEFAULT_TIMEOUT = 6e4;
217
228
  var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
218
229
  var HostWebhook = class {
219
230
  constructor(config) {
@@ -410,7 +421,7 @@ doSend_fn = async function(token, payload, options) {
410
421
  }
411
422
  const headers = {
412
423
  "Content-Type": options?.contentType ?? "application/json",
413
- "User-Agent": `HostWebhook-SDK/${"1.6.1"}`,
424
+ "User-Agent": `HostWebhook-SDK/${"1.6.3"}`,
414
425
  "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
415
426
  ...tz ? { "X-HostWebhook-Timezone": tz } : {},
416
427
  ...__privateGet(this, _defaultHeaders),
@@ -457,6 +468,10 @@ doSend_fn = async function(token, payload, options) {
457
468
  } catch {
458
469
  ingressResult = { eventId: text };
459
470
  }
471
+ if (!ingressResult.eventId) {
472
+ const deCabecera = res.headers.get("x-hostwebhook-event-id");
473
+ if (deCabecera) ingressResult = { ...ingressResult, eventId: deCabecera };
474
+ }
460
475
  } catch (err) {
461
476
  if (err instanceof SendError) throw err;
462
477
  if (err instanceof DOMException && err.name === "AbortError") {
package/dist/index.mjs CHANGED
@@ -15,7 +15,15 @@ var VerificationError = class extends Error {
15
15
  this.name = "VerificationError";
16
16
  }
17
17
  };
18
+ function exigirUnSecreto(secret) {
19
+ if (typeof secret !== "string" || secret.trim() === "") {
20
+ throw new VerificationError(
21
+ "The signing secret is empty. Set the signing secret of your endpoint (whsec_\u2026) before verifying"
22
+ );
23
+ }
24
+ }
18
25
  function verify(payload, headers, secret, options) {
26
+ exigirUnSecreto(secret);
19
27
  const sigHeader = getHeader(headers, "x-hostwebhook-signature");
20
28
  if (!sigHeader) {
21
29
  throw new VerificationError("Missing X-HostWebhook-Signature header");
@@ -125,6 +133,7 @@ function parseHeaders(headers) {
125
133
 
126
134
  // src/middleware.ts
127
135
  function expressMiddleware(secret, options) {
136
+ exigirUnSecreto(secret);
128
137
  return (req, res, next) => {
129
138
  try {
130
139
  verify(req.body, req.headers, secret, options);
@@ -139,6 +148,7 @@ function expressMiddleware(secret, options) {
139
148
  };
140
149
  }
141
150
  function createNestGuard(secret, options) {
151
+ exigirUnSecreto(secret);
142
152
  return class HostWebhookGuard {
143
153
  canActivate(context) {
144
154
  const req = context.switchToHttp().getRequest();
@@ -148,6 +158,7 @@ function createNestGuard(secret, options) {
148
158
  };
149
159
  }
150
160
  function fastifyHook(secret, options) {
161
+ exigirUnSecreto(secret);
151
162
  return (req, _reply, done) => {
152
163
  try {
153
164
  verify(req.body, req.headers, secret, options);
@@ -179,7 +190,7 @@ var SendError = class extends Error {
179
190
 
180
191
  // src/client.ts
181
192
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
182
- var DEFAULT_TIMEOUT = 1e4;
193
+ var DEFAULT_TIMEOUT = 6e4;
183
194
  var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
184
195
  var HostWebhook = class {
185
196
  constructor(config) {
@@ -376,7 +387,7 @@ doSend_fn = async function(token, payload, options) {
376
387
  }
377
388
  const headers = {
378
389
  "Content-Type": options?.contentType ?? "application/json",
379
- "User-Agent": `HostWebhook-SDK/${"1.6.1"}`,
390
+ "User-Agent": `HostWebhook-SDK/${"1.6.3"}`,
380
391
  "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
381
392
  ...tz ? { "X-HostWebhook-Timezone": tz } : {},
382
393
  ...__privateGet(this, _defaultHeaders),
@@ -423,6 +434,10 @@ doSend_fn = async function(token, payload, options) {
423
434
  } catch {
424
435
  ingressResult = { eventId: text };
425
436
  }
437
+ if (!ingressResult.eventId) {
438
+ const deCabecera = res.headers.get("x-hostwebhook-event-id");
439
+ if (deCabecera) ingressResult = { ...ingressResult, eventId: deCabecera };
440
+ }
426
441
  } catch (err) {
427
442
  if (err instanceof SendError) throw err;
428
443
  if (err instanceof DOMException && err.name === "AbortError") {
package/llms.txt CHANGED
@@ -80,7 +80,7 @@ try {
80
80
  ```
81
81
 
82
82
  verify(payload, headers, secret, options?) — throws VerificationError if invalid.
83
- VerifyOptions: { maxAgeSec?: number } — default 300 (5 min). Set 0 to disable replay protection.
83
+ VerifyOptions: { maxAgeSec?: number } — default 300 (5 min). Set 0 to disable the age check. The signature covers only the timestamp and the raw body: the event-id, attempt and chain-* headers are NOT signed, and the timestamp does not stop a replay inside the window (deduplicate by event id). An empty signing secret is refused.
84
84
 
85
85
  ## Send Webhooks (sending through HostWebhook)
86
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
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",