@hostwebhook/node 1.6.2 → 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
 
@@ -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`) */
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`) */
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);
@@ -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.2"}`,
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),
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);
@@ -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.2"}`,
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),
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.2",
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",