@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 +14 -5
- package/dist/index.d.mts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +17 -2
- package/dist/index.mjs +17 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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,
|
|
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
|
|
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` | `
|
|
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:
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
/**
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|