@hookflo/tern 4.3.3-beta → 4.4.0

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
@@ -7,7 +7,7 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
8
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
9
9
 
10
- Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API.
10
+ Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API. It also verifies **Standard Webhooks** (including Svix-style `svix-*` and canonical `webhook-*` headers) through a single `standardwebhooks` platform config.
11
11
 
12
12
  > Need reliable delivery too? Tern supports inbound webhook delivery via Upstash QStash — automatic retries, DLQ management, replay controls, and Slack/Discord alerting. Bring your own Upstash account (BYOK).
13
13
 
@@ -79,27 +79,6 @@ const result = await WebhookVerificationService.verifyAny(request, {
79
79
  console.log(`Verified ${result.platform} webhook`);
80
80
  ```
81
81
 
82
- ### Twilio example
83
-
84
- ```typescript
85
- import { WebhookVerificationService } from '@hookflo/tern';
86
-
87
- export async function POST(request: Request) {
88
- const result = await WebhookVerificationService.verify(request, {
89
- platform: 'twilio',
90
- secret: process.env.TWILIO_AUTH_TOKEN!,
91
- // Optional when behind proxies/CDNs if request.url differs from the public Twilio URL:
92
- twilioBaseUrl: 'https://yourdomain.com/api/webhooks/twilio',
93
- });
94
-
95
- if (!result.isValid) {
96
- return Response.json({ error: result.error }, { status: 400 });
97
- }
98
-
99
- return Response.json({ ok: true });
100
- }
101
- ```
102
-
103
82
  ### Core SDK (runtime-agnostic)
104
83
 
105
84
  Use Tern without framework adapters in any runtime that supports the Web `Request` API.
@@ -214,9 +193,8 @@ app.post('/webhooks/stripe', createWebhookHandler({
214
193
  | **Doppler** | HMAC-SHA256 | ✅ Tested |
215
194
  | **Sanity** | HMAC-SHA256 | ✅ Tested |
216
195
  | **Svix** | HMAC-SHA256 | ⚠️ Untested for now |
196
+ | **Standard Webhooks** (`standardwebhooks`) | HMAC-SHA256 | ✅ Tested |
217
197
  | **Linear** | HMAC-SHA256 | ⚠️ Untested for now |
218
- | **PagerDuty** | HMAC-SHA256 | ⚠️ Untested for now |
219
- | **Twilio** | HMAC-SHA1 | ⚠️ Untested for now |
220
198
  | **Razorpay** | HMAC-SHA256 | 🔄 Pending |
221
199
  | **Vercel** | HMAC-SHA256 | 🔄 Pending |
222
200
 
@@ -224,7 +202,7 @@ app.post('/webhooks/stripe', createWebhookHandler({
224
202
 
225
203
  ### Platform signature notes
226
204
 
227
- - **Standard Webhooks style** platforms (Clerk, Dodo Payments, Polar, ReplicateAI) commonly use a secret that starts with `whsec_...`.
205
+ - **Standard Webhooks style** providers are supported via the canonical `standardwebhooks` platform (with aliases for both `webhook-*` and `svix-*` headers). Clerk, Dodo Payments, Polar, and ReplicateAI all follow this pattern and commonly use a secret that starts with `whsec_...`.
228
206
  - **ReplicateAI**: copy the webhook signing secret from your Replicate webhook settings and pass it directly as `secret`.
229
207
  - **fal.ai**: supports JWKS key resolution out of the box — use `secret: ''` for auto key resolution, or pass a PEM public key explicitly.
230
208
 
@@ -365,25 +343,31 @@ const result = await WebhookVerificationService.verify(request, {
365
343
  });
366
344
  ```
367
345
 
368
- ### Svix / Standard Webhooks format (Clerk, Dodo Payments, ReplicateAI, etc.)
346
+ ### Standard Webhooks config helpers (Svix-style and webhook-* headers)
369
347
 
370
348
  ```typescript
371
- const svixConfig = {
372
- platform: 'my-svix-platform',
373
- secret: 'whsec_abc123...',
349
+ import {
350
+ createStandardWebhooksConfig,
351
+ STANDARD_WEBHOOKS_BASE,
352
+ } from '@hookflo/tern';
353
+
354
+ const signatureConfig = createStandardWebhooksConfig({
355
+ id: 'webhook-id',
356
+ timestamp: 'webhook-timestamp',
357
+ signature: 'webhook-signature',
358
+ idAliases: ['svix-id'],
359
+ timestampAliases: ['svix-timestamp'],
360
+ signatureAliases: ['svix-signature'],
361
+ });
362
+
363
+ const result = await WebhookVerificationService.verify(request, {
364
+ platform: 'standardwebhooks',
365
+ secret: process.env.STANDARD_WEBHOOKS_SECRET!,
374
366
  signatureConfig: {
375
- algorithm: 'hmac-sha256',
376
- headerName: 'webhook-signature',
377
- headerFormat: 'raw',
378
- timestampHeader: 'webhook-timestamp',
379
- timestampFormat: 'unix',
380
- payloadFormat: 'custom',
381
- customConfig: {
382
- payloadFormat: '{id}.{timestamp}.{body}',
383
- idHeader: 'webhook-id',
384
- },
367
+ ...STANDARD_WEBHOOKS_BASE,
368
+ ...signatureConfig,
385
369
  },
386
- };
370
+ });
387
371
  ```
388
372
 
389
373
  See the [SignatureConfig type](https://tern.hookflo.com) for all options.
@@ -431,8 +415,6 @@ interface WebhookVerificationResult {
431
415
 
432
416
  ## Troubleshooting
433
417
 
434
- - **Twilio invalid signature behind proxies/CDNs**: if your runtime `request.url` differs from the public Twilio webhook URL, pass `twilioBaseUrl` in `WebhookVerificationService.verify(...)` for platform `twilio`.
435
-
436
418
 
437
419
  **`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
438
420
 
@@ -39,7 +39,11 @@ function createWebhookHandler(options) {
39
39
  }
40
40
  return response;
41
41
  }
42
- const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, secret, options.toleranceInSeconds);
42
+ const result = await index_1.WebhookVerificationService.verify(request, {
43
+ platform: options.platform,
44
+ secret,
45
+ toleranceInSeconds: options.toleranceInSeconds,
46
+ });
43
47
  if (!result.isValid) {
44
48
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
45
49
  }
@@ -48,7 +48,11 @@ function createWebhookMiddleware(options) {
48
48
  }
49
49
  return;
50
50
  }
51
- const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds);
51
+ const result = await index_1.WebhookVerificationService.verify(webRequest, {
52
+ platform: options.platform,
53
+ secret: options.secret,
54
+ toleranceInSeconds: options.toleranceInSeconds,
55
+ });
52
56
  if (!result.isValid) {
53
57
  res.status(400).json({
54
58
  error: result.error,
@@ -35,7 +35,11 @@ function createWebhookHandler(options) {
35
35
  }
36
36
  return response;
37
37
  }
38
- const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds);
38
+ const result = await index_1.WebhookVerificationService.verify(request, {
39
+ platform: options.platform,
40
+ secret: options.secret,
41
+ toleranceInSeconds: options.toleranceInSeconds,
42
+ });
39
43
  if (!result.isValid) {
40
44
  return c.json({
41
45
  error: result.error,
@@ -34,7 +34,11 @@ function createWebhookHandler(options) {
34
34
  }
35
35
  return response;
36
36
  }
37
- const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds);
37
+ const result = await index_1.WebhookVerificationService.verify(request, {
38
+ platform: options.platform,
39
+ secret: options.secret,
40
+ toleranceInSeconds: options.toleranceInSeconds,
41
+ });
38
42
  if (!result.isValid) {
39
43
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
40
44
  }
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export declare class WebhookVerificationService {
29
29
  static verifyTokenBased<TPayload = unknown>(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult<TPayload>>;
30
30
  }
31
31
  export * from './types';
32
- export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, } from './platforms/algorithms';
32
+ export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, STANDARD_WEBHOOKS_BASE, createStandardWebhooksConfig, } from './platforms/algorithms';
33
33
  export { createAlgorithmVerifier } from './verifiers/algorithms';
34
34
  export { createCustomVerifier } from './verifiers/custom-algorithms';
35
35
  export * from './adapters';
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
39
+ exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.createStandardWebhooksConfig = exports.STANDARD_WEBHOOKS_BASE = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
40
40
  const crypto_1 = require("crypto");
41
41
  const algorithms_1 = require("./verifiers/algorithms");
42
42
  const custom_algorithms_1 = require("./verifiers/custom-algorithms");
@@ -70,9 +70,6 @@ class WebhookVerificationService {
70
70
  ...signatureConfig,
71
71
  customConfig: {
72
72
  ...(signatureConfig.customConfig || {}),
73
- ...(config.platform === 'twilio' && config.twilioBaseUrl
74
- ? { twilioBaseUrl: config.twilioBaseUrl }
75
- : {}),
76
73
  },
77
74
  };
78
75
  // Use custom verifiers for special cases (token-based, etc.)
@@ -194,9 +191,8 @@ class WebhookVerificationService {
194
191
  case 'sentry':
195
192
  case 'vercel':
196
193
  case 'linear':
197
- case 'pagerduty':
198
- case 'twilio':
199
194
  case 'svix':
195
+ case 'standardwebhooks':
200
196
  return this.pickString(payload?.id) || null;
201
197
  case 'doppler':
202
198
  return this.pickString(payload?.event?.id, metadata?.id) || null;
@@ -231,10 +227,6 @@ class WebhookVerificationService {
231
227
  return headers.has('svix-id') ? 'svix' : 'clerk';
232
228
  if (headers.has('linear-signature'))
233
229
  return 'linear';
234
- if (headers.has('x-pagerduty-signature'))
235
- return 'pagerduty';
236
- if (headers.has('x-twilio-signature'))
237
- return 'twilio';
238
230
  if (headers.has('workos-signature'))
239
231
  return 'workos';
240
232
  if (headers.has('webhook-signature')) {
@@ -375,6 +367,8 @@ Object.defineProperty(exports, "getPlatformAlgorithmConfig", { enumerable: true,
375
367
  Object.defineProperty(exports, "platformUsesAlgorithm", { enumerable: true, get: function () { return algorithms_3.platformUsesAlgorithm; } });
376
368
  Object.defineProperty(exports, "getPlatformsUsingAlgorithm", { enumerable: true, get: function () { return algorithms_3.getPlatformsUsingAlgorithm; } });
377
369
  Object.defineProperty(exports, "validateSignatureConfig", { enumerable: true, get: function () { return algorithms_3.validateSignatureConfig; } });
370
+ Object.defineProperty(exports, "STANDARD_WEBHOOKS_BASE", { enumerable: true, get: function () { return algorithms_3.STANDARD_WEBHOOKS_BASE; } });
371
+ Object.defineProperty(exports, "createStandardWebhooksConfig", { enumerable: true, get: function () { return algorithms_3.createStandardWebhooksConfig; } });
378
372
  var algorithms_4 = require("./verifiers/algorithms");
379
373
  Object.defineProperty(exports, "createAlgorithmVerifier", { enumerable: true, get: function () { return algorithms_4.createAlgorithmVerifier; } });
380
374
  var custom_algorithms_2 = require("./verifiers/custom-algorithms");
@@ -1,4 +1,24 @@
1
1
  import { PlatformAlgorithmConfig, WebhookPlatform, SignatureConfig } from "../types";
2
+ export declare const STANDARD_WEBHOOKS_BASE: {
3
+ algorithm: "hmac-sha256";
4
+ headerFormat: "raw";
5
+ timestampFormat: "unix";
6
+ payloadFormat: "custom";
7
+ customConfig: {
8
+ signatureFormat: string;
9
+ payloadFormat: string;
10
+ encoding: string;
11
+ secretEncoding: string;
12
+ };
13
+ };
14
+ export declare function createStandardWebhooksConfig(headers: {
15
+ id: string;
16
+ timestamp: string;
17
+ signature: string;
18
+ idAliases?: string[];
19
+ timestampAliases?: string[];
20
+ signatureAliases?: string[];
21
+ }): SignatureConfig;
2
22
  export declare const platformAlgorithmConfigs: Record<WebhookPlatform, PlatformAlgorithmConfig>;
3
23
  export declare function getPlatformAlgorithmConfig(platform: WebhookPlatform): PlatformAlgorithmConfig;
4
24
  export declare function platformUsesAlgorithm(platform: WebhookPlatform, algorithm: string): boolean;
@@ -1,10 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.platformAlgorithmConfigs = void 0;
3
+ exports.platformAlgorithmConfigs = exports.STANDARD_WEBHOOKS_BASE = void 0;
4
+ exports.createStandardWebhooksConfig = createStandardWebhooksConfig;
4
5
  exports.getPlatformAlgorithmConfig = getPlatformAlgorithmConfig;
5
6
  exports.platformUsesAlgorithm = platformUsesAlgorithm;
6
7
  exports.getPlatformsUsingAlgorithm = getPlatformsUsingAlgorithm;
7
8
  exports.validateSignatureConfig = validateSignatureConfig;
9
+ exports.STANDARD_WEBHOOKS_BASE = {
10
+ algorithm: "hmac-sha256",
11
+ headerFormat: "raw",
12
+ timestampFormat: "unix",
13
+ payloadFormat: "custom",
14
+ customConfig: {
15
+ signatureFormat: "v1={signature}",
16
+ payloadFormat: "{id}.{timestamp}.{body}",
17
+ encoding: "base64",
18
+ secretEncoding: "base64",
19
+ },
20
+ };
21
+ function createStandardWebhooksConfig(headers) {
22
+ return {
23
+ ...exports.STANDARD_WEBHOOKS_BASE,
24
+ headerName: headers.signature,
25
+ timestampHeader: headers.timestamp,
26
+ customConfig: {
27
+ ...exports.STANDARD_WEBHOOKS_BASE.customConfig,
28
+ idHeader: headers.id,
29
+ ...(headers.idAliases && { idHeaderAliases: headers.idAliases }),
30
+ ...(headers.timestampAliases && {
31
+ timestampHeaderAliases: headers.timestampAliases,
32
+ }),
33
+ ...(headers.signatureAliases && {
34
+ signatureHeaderAliases: headers.signatureAliases,
35
+ }),
36
+ },
37
+ };
38
+ }
8
39
  exports.platformAlgorithmConfigs = {
9
40
  github: {
10
41
  platform: "github",
@@ -51,27 +82,6 @@ exports.platformAlgorithmConfigs = {
51
82
  },
52
83
  description: "Clerk webhooks use HMAC-SHA256 with base64 encoding",
53
84
  },
54
- svix: {
55
- platform: 'svix',
56
- signatureConfig: {
57
- algorithm: 'hmac-sha256',
58
- headerName: 'svix-signature',
59
- headerFormat: 'raw',
60
- timestampHeader: 'svix-timestamp',
61
- timestampFormat: 'unix',
62
- payloadFormat: 'custom',
63
- customConfig: {
64
- signatureFormat: 'v1={signature}',
65
- payloadFormat: '{id}.{timestamp}.{body}',
66
- encoding: 'base64',
67
- secretEncoding: 'base64',
68
- idHeader: 'svix-id',
69
- idHeaderAliases: ['webhook-id'],
70
- timestampHeaderAliases: ['webhook-timestamp'],
71
- },
72
- },
73
- description: 'Svix webhooks use HMAC-SHA256 with Standard Webhooks format',
74
- },
75
85
  dodopayments: {
76
86
  platform: "dodopayments",
77
87
  signatureConfig: {
@@ -309,48 +319,52 @@ exports.platformAlgorithmConfigs = {
309
319
  description: "Sanity webhooks use Stripe-compatible HMAC-SHA256 with base64 encoded signature and plain UTF-8 secret",
310
320
  },
311
321
  linear: {
312
- platform: 'linear',
322
+ platform: "linear",
313
323
  signatureConfig: {
314
- algorithm: 'hmac-sha256',
315
- headerName: 'linear-signature',
316
- headerFormat: 'raw',
317
- payloadFormat: 'raw',
324
+ algorithm: "hmac-sha256",
325
+ headerName: "linear-signature",
326
+ headerFormat: "raw",
327
+ payloadFormat: "raw",
318
328
  customConfig: {
319
329
  replayToleranceMs: 60000,
320
330
  },
321
331
  },
322
- description: 'Linear webhooks use HMAC-SHA256 on the raw body with a 60s timestamp replay window',
332
+ description: "Linear webhooks use HMAC-SHA256 on the raw body with a 60s timestamp replay window",
323
333
  },
324
- pagerduty: {
325
- platform: 'pagerduty',
334
+ svix: {
335
+ platform: "svix",
326
336
  signatureConfig: {
327
- algorithm: 'hmac-sha256',
328
- headerName: 'x-pagerduty-signature',
329
- headerFormat: 'raw',
330
- payloadFormat: 'raw',
331
- prefix: 'v1=',
337
+ algorithm: "hmac-sha256",
338
+ headerName: "svix-signature",
339
+ headerFormat: "raw",
340
+ timestampHeader: "svix-timestamp",
341
+ timestampFormat: "unix",
342
+ payloadFormat: "custom",
332
343
  customConfig: {
333
- signatureFormat: 'v1={signature}',
334
- comparePrefixed: true,
344
+ signatureFormat: "v1={signature}",
345
+ payloadFormat: "{id}.{timestamp}.{body}",
346
+ encoding: "base64",
347
+ secretEncoding: "base64",
348
+ idHeader: "svix-id",
349
+ idHeaderAliases: ["webhook-id"],
350
+ timestampHeaderAliases: ["webhook-timestamp"],
335
351
  },
336
352
  },
337
- description: 'PagerDuty webhooks use HMAC-SHA256 with v1=<hex> signatures',
353
+ description: "Svix webhooks use HMAC-SHA256 with Standard Webhooks format",
338
354
  },
339
- twilio: {
340
- platform: 'twilio',
355
+ standardwebhooks: {
356
+ platform: "standardwebhooks",
341
357
  signatureConfig: {
342
- algorithm: 'hmac-sha1',
343
- headerName: 'x-twilio-signature',
344
- headerFormat: 'raw',
345
- payloadFormat: 'custom',
346
- customConfig: {
347
- payloadFormat: '{url}',
348
- encoding: 'base64',
349
- secretEncoding: 'utf8',
350
- validateBodySHA256: true,
351
- },
358
+ ...createStandardWebhooksConfig({
359
+ id: "webhook-id",
360
+ timestamp: "webhook-timestamp",
361
+ signature: "webhook-signature",
362
+ idAliases: ["svix-id"],
363
+ timestampAliases: ["svix-timestamp"],
364
+ signatureAliases: ["svix-signature"],
365
+ }),
352
366
  },
353
- description: 'Twilio webhooks use HMAC-SHA1 with base64 signatures (URL canonicalization required)',
367
+ description: "Canonical Standard Webhooks implementation. Works for any platform using v1= HMAC-SHA256 signing regardless of header names.",
354
368
  },
355
369
  custom: {
356
370
  platform: "custom",
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type WebhookPlatform = 'custom' | 'clerk' | 'svix' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'paddle' | 'razorpay' | 'lemonsqueezy' | 'workos' | 'woocommerce' | 'replicateai' | 'falai' | 'sentry' | 'grafana' | 'doppler' | 'sanity' | 'linear' | 'pagerduty' | 'twilio' | 'unknown';
1
+ export type WebhookPlatform = "custom" | "clerk" | "svix" | "github" | "stripe" | "shopify" | "vercel" | "polar" | "dodopayments" | "gitlab" | "paddle" | "razorpay" | "lemonsqueezy" | "workos" | "woocommerce" | "replicateai" | "falai" | "sentry" | "grafana" | "doppler" | "sanity" | "linear" | "standardwebhooks" | "unknown";
2
2
  export declare enum WebhookPlatformKeys {
3
3
  GitHub = "github",
4
4
  Stripe = "stripe",
@@ -21,24 +21,23 @@ export declare enum WebhookPlatformKeys {
21
21
  Doppler = "doppler",
22
22
  Sanity = "sanity",
23
23
  Linear = "linear",
24
- PagerDuty = "pagerduty",
25
- Twilio = "twilio",
24
+ StandardWebhooks = "standardwebhooks",
26
25
  Custom = "custom",
27
26
  Unknown = "unknown"
28
27
  }
29
- export type SignatureAlgorithm = 'hmac-sha256' | 'hmac-sha1' | 'hmac-sha512' | 'rsa-sha256' | 'ed25519' | 'custom';
28
+ export type SignatureAlgorithm = "hmac-sha256" | "hmac-sha1" | "hmac-sha512" | "rsa-sha256" | "ed25519" | "custom";
30
29
  export interface SignatureConfig {
31
30
  algorithm: SignatureAlgorithm;
32
31
  headerName: string;
33
- headerFormat?: 'raw' | 'prefixed' | 'comma-separated';
32
+ headerFormat?: "raw" | "prefixed" | "comma-separated";
34
33
  prefix?: string;
35
34
  timestampHeader?: string;
36
- timestampFormat?: 'unix' | 'iso' | 'custom';
37
- payloadFormat?: 'raw' | 'timestamped' | 'json-stringified' | 'custom';
35
+ timestampFormat?: "unix" | "iso" | "custom";
36
+ payloadFormat?: "raw" | "timestamped" | "json-stringified" | "custom";
38
37
  idHeader?: string;
39
38
  customConfig?: Record<string, any>;
40
39
  }
41
- export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
40
+ export type WebhookErrorCode = "MISSING_SIGNATURE" | "INVALID_SIGNATURE" | "TIMESTAMP_EXPIRED" | "MISSING_TOKEN" | "INVALID_TOKEN" | "PLATFORM_NOT_SUPPORTED" | "NORMALIZATION_ERROR" | "VERIFICATION_ERROR";
42
41
  export interface WebhookVerificationResult<TPayload = unknown> {
43
42
  isValid: boolean;
44
43
  error?: string;
@@ -57,7 +56,6 @@ export interface WebhookConfig {
57
56
  secret: string;
58
57
  toleranceInSeconds?: number;
59
58
  signatureConfig?: SignatureConfig;
60
- twilioBaseUrl?: string;
61
59
  }
62
60
  export interface MultiPlatformSecrets {
63
61
  [platform: string]: string | undefined;
package/dist/types.js CHANGED
@@ -24,8 +24,7 @@ var WebhookPlatformKeys;
24
24
  WebhookPlatformKeys["Doppler"] = "doppler";
25
25
  WebhookPlatformKeys["Sanity"] = "sanity";
26
26
  WebhookPlatformKeys["Linear"] = "linear";
27
- WebhookPlatformKeys["PagerDuty"] = "pagerduty";
28
- WebhookPlatformKeys["Twilio"] = "twilio";
27
+ WebhookPlatformKeys["StandardWebhooks"] = "standardwebhooks";
29
28
  WebhookPlatformKeys["Custom"] = "custom";
30
29
  WebhookPlatformKeys["Unknown"] = "unknown";
31
30
  })(WebhookPlatformKeys || (exports.WebhookPlatformKeys = WebhookPlatformKeys = {}));
@@ -15,7 +15,6 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
15
15
  protected extractTimestamp(request: Request): number | null;
16
16
  protected extractTimestampFromSignature(request: Request): number | null;
17
17
  protected requiresTimestamp(): boolean;
18
- protected resolveTwilioSignatureUrl(request: Request): string;
19
18
  protected formatPayload(rawBody: string, request: Request): string;
20
19
  protected formatCustomPayload(rawBody: string, request: Request): string;
21
20
  protected verifyHMAC(payload: string, signature: string, algorithm?: string): boolean;
@@ -25,7 +24,6 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
25
24
  }
26
25
  export declare class GenericHMACVerifier extends AlgorithmBasedVerifier {
27
26
  private validateLinearReplayWindow;
28
- private validateTwilioBodyHash;
29
27
  private resolveSentryPayloadCandidates;
30
28
  verify(request: Request): Promise<WebhookVerificationResult>;
31
29
  }
@@ -26,20 +26,17 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
26
26
  getInvalidSignatureMessage() {
27
27
  const genericHint = `Invalid signature for ${this.platform}. Confirm webhook secret, raw request body handling, and signature header formatting.`;
28
28
  switch (this.platform) {
29
- case 'twilio':
30
- return `${genericHint} Twilio also requires the exact public URL used for signing (including query params like bodySHA256). Use twilioBaseUrl if your runtime URL is rewritten behind a proxy.`;
31
29
  case 'stripe':
32
30
  return `${genericHint} Stripe signatures require the exact raw body and Stripe-Signature timestamp/value pair.`;
33
31
  case 'github':
34
32
  return `${genericHint} GitHub signatures must include the sha256= prefix from x-hub-signature-256.`;
35
33
  case 'svix':
34
+ case 'standardwebhooks':
36
35
  case 'clerk':
37
36
  case 'dodopayments':
38
37
  case 'replicateai':
39
38
  case 'polar':
40
39
  return `${genericHint} Standard Webhooks payload must be signed as id.timestamp.body and secrets may need whsec_ base64 decoding.`;
41
- case 'pagerduty':
42
- return `${genericHint} PagerDuty expects v1=<hex> signature values from x-pagerduty-signature.`;
43
40
  default:
44
41
  return genericHint;
45
42
  }
@@ -66,7 +63,9 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
66
63
  return values;
67
64
  }
68
65
  extractSignatures(request) {
69
- const headerValue = request.headers.get(this.config.headerName);
66
+ const headerValue = request.headers.get(this.config.headerName)
67
+ || this.config.customConfig?.signatureHeaderAliases?.map((alias) => request.headers.get(alias)).find(Boolean)
68
+ || null;
70
69
  if (!headerValue)
71
70
  return [];
72
71
  switch (this.config.headerFormat) {
@@ -169,7 +168,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
169
168
  return true;
170
169
  // These platforms have timestampHeader in config but timestamp
171
170
  // is optional in their spec — validate only if present, never mandate
172
- const optionalTimestampPlatforms = ['vercel', 'sentry', 'grafana', 'twilio'];
171
+ const optionalTimestampPlatforms = ['vercel', 'sentry', 'grafana'];
173
172
  if (optionalTimestampPlatforms.includes(this.platform))
174
173
  return false;
175
174
  // For all other platforms: infer from config
@@ -186,16 +185,6 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
186
185
  return true;
187
186
  return false;
188
187
  }
189
- resolveTwilioSignatureUrl(request) {
190
- const overrideBaseUrl = this.config.customConfig?.twilioBaseUrl;
191
- if (!overrideBaseUrl) {
192
- return request.url;
193
- }
194
- const requestUrl = new URL(request.url);
195
- const baseUrl = new URL(overrideBaseUrl);
196
- baseUrl.search = requestUrl.search;
197
- return baseUrl.toString();
198
- }
199
188
  formatPayload(rawBody, request) {
200
189
  switch (this.config.payloadFormat) {
201
190
  case "timestamped": {
@@ -226,7 +215,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
226
215
  const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id") || this.config.customConfig?.idHeaderAliases?.map((alias) => request.headers.get(alias)).find(Boolean);
227
216
  const timestamp = request.headers.get(this.config.timestampHeader ||
228
217
  this.config.customConfig?.timestampHeader ||
229
- "x-webhook-timestamp");
218
+ "x-webhook-timestamp") || this.config.customConfig?.timestampHeaderAliases?.map((alias) => request.headers.get(alias)).find(Boolean);
230
219
  // if either is missing payload will be malformed — fail explicitly
231
220
  if (!id || !timestamp) {
232
221
  throw new Error(`Missing required headers for payload construction: ${!id ? this.config.customConfig.idHeader || "x-webhook-id" : ""} ${!timestamp
@@ -240,7 +229,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
240
229
  }
241
230
  if (customFormat.includes('{url}')) {
242
231
  return customFormat
243
- .replace('{url}', this.platform === 'twilio' ? this.resolveTwilioSignatureUrl(request) : request.url)
232
+ .replace('{url}', request.url)
244
233
  .replace('{body}', rawBody);
245
234
  }
246
235
  if (customFormat.includes("{timestamp}") &&
@@ -346,20 +335,6 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
346
335
  }
347
336
  return null;
348
337
  }
349
- validateTwilioBodyHash(rawBody, request) {
350
- if (this.platform !== 'twilio' || !this.config.customConfig?.validateBodySHA256) {
351
- return null;
352
- }
353
- const url = new URL(this.resolveTwilioSignatureUrl(request));
354
- const bodySha = url.searchParams.get('bodySHA256');
355
- if (!bodySha)
356
- return null;
357
- const computed = (0, crypto_1.createHash)('sha256').update(rawBody).digest('hex');
358
- if (!this.safeCompare(computed, bodySha)) {
359
- return 'Twilio bodySHA256 query param does not match payload hash';
360
- }
361
- return null;
362
- }
363
338
  resolveSentryPayloadCandidates(rawBody, request) {
364
339
  const candidates = [
365
340
  this.formatPayload(rawBody, request),
@@ -408,15 +383,6 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
408
383
  platform: this.platform,
409
384
  };
410
385
  }
411
- const twilioBodyHashError = this.validateTwilioBodyHash(rawBody, request);
412
- if (twilioBodyHashError) {
413
- return {
414
- isValid: false,
415
- error: twilioBodyHashError,
416
- errorCode: 'INVALID_SIGNATURE',
417
- platform: this.platform,
418
- };
419
- }
420
386
  let timestamp = null;
421
387
  if (this.config.headerFormat === "comma-separated") {
422
388
  timestamp = this.extractTimestampFromSignature(request);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "4.3.3-beta",
3
+ "version": "4.4.0",
4
4
  "description": "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",