@hookflo/tern 4.3.0-beta.0 → 4.3.0-beta.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.
Files changed (48) hide show
  1. package/README.md +31 -0
  2. package/dist/adapters/cloudflare.d.ts +2 -2
  3. package/dist/adapters/cloudflare.js +6 -1
  4. package/dist/adapters/express.d.ts +2 -2
  5. package/dist/adapters/express.js +6 -1
  6. package/dist/adapters/hono.d.ts +2 -2
  7. package/dist/adapters/hono.js +6 -1
  8. package/dist/adapters/nextjs.d.ts +2 -2
  9. package/dist/adapters/nextjs.js +6 -1
  10. package/dist/adapters/shared.js +5 -2
  11. package/dist/index.d.ts +3 -4
  12. package/dist/index.js +29 -19
  13. package/dist/platforms/algorithms.js +65 -0
  14. package/dist/types.d.ts +6 -65
  15. package/dist/types.js +4 -0
  16. package/dist/verifiers/algorithms.d.ts +8 -0
  17. package/dist/verifiers/algorithms.js +145 -17
  18. package/package.json +1 -1
  19. package/dist/normalization/index.d.ts +0 -20
  20. package/dist/normalization/index.js +0 -78
  21. package/dist/normalization/providers/payment/paypal.d.ts +0 -2
  22. package/dist/normalization/providers/payment/paypal.js +0 -12
  23. package/dist/normalization/providers/payment/razorpay.d.ts +0 -2
  24. package/dist/normalization/providers/payment/razorpay.js +0 -13
  25. package/dist/normalization/providers/payment/stripe.d.ts +0 -2
  26. package/dist/normalization/providers/payment/stripe.js +0 -13
  27. package/dist/normalization/providers/registry.d.ts +0 -5
  28. package/dist/normalization/providers/registry.js +0 -21
  29. package/dist/normalization/simple.d.ts +0 -4
  30. package/dist/normalization/simple.js +0 -126
  31. package/dist/normalization/storage/interface.d.ts +0 -13
  32. package/dist/normalization/storage/interface.js +0 -2
  33. package/dist/normalization/storage/memory.d.ts +0 -12
  34. package/dist/normalization/storage/memory.js +0 -39
  35. package/dist/normalization/templates/base/auth.d.ts +0 -2
  36. package/dist/normalization/templates/base/auth.js +0 -22
  37. package/dist/normalization/templates/base/ecommerce.d.ts +0 -2
  38. package/dist/normalization/templates/base/ecommerce.js +0 -25
  39. package/dist/normalization/templates/base/payment.d.ts +0 -2
  40. package/dist/normalization/templates/base/payment.js +0 -25
  41. package/dist/normalization/templates/registry.d.ts +0 -6
  42. package/dist/normalization/templates/registry.js +0 -22
  43. package/dist/normalization/transformer/engine.d.ts +0 -11
  44. package/dist/normalization/transformer/engine.js +0 -86
  45. package/dist/normalization/transformer/validator.d.ts +0 -12
  46. package/dist/normalization/transformer/validator.js +0 -56
  47. package/dist/normalization/types.d.ts +0 -79
  48. package/dist/normalization/types.js +0 -2
package/README.md CHANGED
@@ -79,6 +79,27 @@ 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
+
82
103
  ### Core SDK (runtime-agnostic)
83
104
 
84
105
  Use Tern without framework adapters in any runtime that supports the Web `Request` API.
@@ -171,6 +192,9 @@ app.post('/webhooks/stripe', createWebhookHandler({
171
192
 
172
193
  ## Supported Platforms
173
194
 
195
+ > ⚠️ Normalization is no longer supported in Tern and has been removed from the public verification APIs.
196
+
197
+
174
198
  | Platform | Algorithm | Status |
175
199
  |---|---|---|
176
200
  | **Stripe** | HMAC-SHA256 | ✅ Tested |
@@ -189,6 +213,10 @@ app.post('/webhooks/stripe', createWebhookHandler({
189
213
  | **Grafana** | HMAC-SHA256 | ✅ Tested |
190
214
  | **Doppler** | HMAC-SHA256 | ✅ Tested |
191
215
  | **Sanity** | HMAC-SHA256 | ✅ Tested |
216
+ | **Svix** | HMAC-SHA256 | ⚠️ Untested for now |
217
+ | **Linear** | HMAC-SHA256 | ⚠️ Untested for now |
218
+ | **PagerDuty** | HMAC-SHA256 | ⚠️ Untested for now |
219
+ | **Twilio** | HMAC-SHA1 | ⚠️ Untested for now |
192
220
  | **Razorpay** | HMAC-SHA256 | 🔄 Pending |
193
221
  | **Vercel** | HMAC-SHA256 | 🔄 Pending |
194
222
 
@@ -403,6 +431,9 @@ interface WebhookVerificationResult {
403
431
 
404
432
  ## Troubleshooting
405
433
 
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
+
406
437
  **`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
407
438
 
408
439
  ```bash
@@ -1,4 +1,4 @@
1
- import { WebhookPlatform, NormalizeOptions } from '../types';
1
+ import { WebhookPlatform } from '../types';
2
2
  import { QueueOption } from '../upstash/types';
3
3
  import type { AlertConfig, SendAlertOptions } from '../notifications/types';
4
4
  export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>, TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
@@ -6,7 +6,7 @@ export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>,
6
6
  secret?: string;
7
7
  secretEnv?: string;
8
8
  toleranceInSeconds?: number;
9
- normalize?: boolean | NormalizeOptions;
9
+ twilioBaseUrl?: string;
10
10
  queue?: QueueOption;
11
11
  alerts?: AlertConfig;
12
12
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -39,7 +39,12 @@ 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, options.normalize);
42
+ const result = await index_1.WebhookVerificationService.verify(request, {
43
+ platform: options.platform,
44
+ secret,
45
+ toleranceInSeconds: options.toleranceInSeconds,
46
+ twilioBaseUrl: options.twilioBaseUrl,
47
+ });
43
48
  if (!result.isValid) {
44
49
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
45
50
  }
@@ -1,4 +1,4 @@
1
- import { WebhookPlatform, WebhookVerificationResult, NormalizeOptions } from '../types';
1
+ import { WebhookPlatform, WebhookVerificationResult } from '../types';
2
2
  import { QueueOption } from '../upstash/types';
3
3
  import { MinimalNodeRequest } from './shared';
4
4
  import type { AlertConfig, SendAlertOptions } from '../notifications/types';
@@ -14,7 +14,7 @@ export interface ExpressWebhookMiddlewareOptions {
14
14
  platform: WebhookPlatform;
15
15
  secret: string;
16
16
  toleranceInSeconds?: number;
17
- normalize?: boolean | NormalizeOptions;
17
+ twilioBaseUrl?: string;
18
18
  queue?: QueueOption;
19
19
  alerts?: AlertConfig;
20
20
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -48,7 +48,12 @@ 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, options.normalize);
51
+ const result = await index_1.WebhookVerificationService.verify(webRequest, {
52
+ platform: options.platform,
53
+ secret: options.secret,
54
+ toleranceInSeconds: options.toleranceInSeconds,
55
+ twilioBaseUrl: options.twilioBaseUrl,
56
+ });
52
57
  if (!result.isValid) {
53
58
  res.status(400).json({
54
59
  error: result.error,
@@ -1,4 +1,4 @@
1
- import { WebhookPlatform, NormalizeOptions } from '../types';
1
+ import { WebhookPlatform } from '../types';
2
2
  import { QueueOption } from '../upstash/types';
3
3
  import type { AlertConfig, SendAlertOptions } from '../notifications/types';
4
4
  export interface HonoContextLike {
@@ -11,7 +11,7 @@ export interface HonoWebhookHandlerOptions<TContext extends HonoContextLike = Ho
11
11
  platform: WebhookPlatform;
12
12
  secret: string;
13
13
  toleranceInSeconds?: number;
14
- normalize?: boolean | NormalizeOptions;
14
+ twilioBaseUrl?: string;
15
15
  queue?: QueueOption;
16
16
  alerts?: AlertConfig;
17
17
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -35,7 +35,12 @@ 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, options.normalize);
38
+ const result = await index_1.WebhookVerificationService.verify(request, {
39
+ platform: options.platform,
40
+ secret: options.secret,
41
+ toleranceInSeconds: options.toleranceInSeconds,
42
+ twilioBaseUrl: options.twilioBaseUrl,
43
+ });
39
44
  if (!result.isValid) {
40
45
  return c.json({
41
46
  error: result.error,
@@ -1,11 +1,11 @@
1
- import { WebhookPlatform, NormalizeOptions } from '../types';
1
+ import { WebhookPlatform } from '../types';
2
2
  import { QueueOption } from '../upstash/types';
3
3
  import type { AlertConfig, SendAlertOptions } from '../notifications/types';
4
4
  export interface NextWebhookHandlerOptions<TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
5
5
  platform: WebhookPlatform;
6
6
  secret: string;
7
7
  toleranceInSeconds?: number;
8
- normalize?: boolean | NormalizeOptions;
8
+ twilioBaseUrl?: string;
9
9
  queue?: QueueOption;
10
10
  alerts?: AlertConfig;
11
11
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -34,7 +34,12 @@ 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, options.normalize);
37
+ const result = await index_1.WebhookVerificationService.verify(request, {
38
+ platform: options.platform,
39
+ secret: options.secret,
40
+ toleranceInSeconds: options.toleranceInSeconds,
41
+ twilioBaseUrl: options.twilioBaseUrl,
42
+ });
38
43
  if (!result.isValid) {
39
44
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
40
45
  }
@@ -90,8 +90,11 @@ function toHeadersInit(headers) {
90
90
  return normalized;
91
91
  }
92
92
  async function toWebRequest(request) {
93
- const protocol = request.protocol || 'https';
94
- const host = request.get?.('host')
93
+ const forwardedProto = getHeaderValue(request.headers, 'x-forwarded-proto')?.split(',')[0]?.trim();
94
+ const protocol = forwardedProto || request.protocol || 'https';
95
+ const forwardedHost = getHeaderValue(request.headers, 'x-forwarded-host')?.split(',')[0]?.trim();
96
+ const host = forwardedHost
97
+ || request.get?.('host')
95
98
  || getHeaderValue(request.headers, 'host')
96
99
  || 'localhost';
97
100
  const path = request.originalUrl || request.url || '/';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { WebhookConfig, WebhookVerificationResult, WebhookPlatform, SignatureConfig, MultiPlatformSecrets, NormalizeOptions } from './types';
1
+ import { WebhookConfig, WebhookVerificationResult, WebhookPlatform, SignatureConfig, MultiPlatformSecrets } from './types';
2
2
  import type { QueueOption } from './upstash/types';
3
3
  import type { AlertConfig, SendAlertOptions } from './notifications/types';
4
4
  export declare class WebhookVerificationService {
@@ -6,8 +6,8 @@ export declare class WebhookVerificationService {
6
6
  private static getVerifier;
7
7
  private static createAlgorithmBasedVerifier;
8
8
  private static getLegacyVerifier;
9
- static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
10
- static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
9
+ static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult<TPayload>>;
10
+ static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number): Promise<WebhookVerificationResult<TPayload>>;
11
11
  private static resolveCanonicalEventId;
12
12
  private static safeCompare;
13
13
  private static pickString;
@@ -32,7 +32,6 @@ export * from './types';
32
32
  export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, } from './platforms/algorithms';
33
33
  export { createAlgorithmVerifier } from './verifiers/algorithms';
34
34
  export { createCustomVerifier } from './verifiers/custom-algorithms';
35
- export { normalizePayload, getPlatformNormalizationCategory, getPlatformsByCategory, } from './normalization/simple';
36
35
  export * from './adapters';
37
36
  export * from './alerts';
38
37
  export default WebhookVerificationService;
package/dist/index.js CHANGED
@@ -36,12 +36,11 @@ 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.getPlatformsByCategory = exports.getPlatformNormalizationCategory = exports.normalizePayload = exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
39
+ exports.createCustomVerifier = exports.createAlgorithmVerifier = 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");
43
43
  const algorithms_2 = require("./platforms/algorithms");
44
- const simple_1 = require("./normalization/simple");
45
44
  const dispatch_1 = require("./notifications/dispatch");
46
45
  class WebhookVerificationService {
47
46
  static async verify(request, config) {
@@ -51,9 +50,6 @@ class WebhookVerificationService {
51
50
  if (result.isValid) {
52
51
  result.platform = config.platform;
53
52
  result.eventId = this.resolveCanonicalEventId(config.platform, result.metadata, result.payload) ?? undefined;
54
- if (config.normalize) {
55
- result.payload = (0, simple_1.normalizePayload)(config.platform, result.payload, config.normalize);
56
- }
57
53
  }
58
54
  return result;
59
55
  }
@@ -70,12 +66,21 @@ class WebhookVerificationService {
70
66
  if (!signatureConfig) {
71
67
  throw new Error('Signature config is required for algorithm-based verification');
72
68
  }
69
+ const effectiveSignatureConfig = {
70
+ ...signatureConfig,
71
+ customConfig: {
72
+ ...(signatureConfig.customConfig || {}),
73
+ ...(config.platform === 'twilio' && config.twilioBaseUrl
74
+ ? { twilioBaseUrl: config.twilioBaseUrl }
75
+ : {}),
76
+ },
77
+ };
73
78
  // Use custom verifiers for special cases (token-based, etc.)
74
- if (signatureConfig.algorithm === 'custom') {
75
- return (0, custom_algorithms_1.createCustomVerifier)(secret, signatureConfig, toleranceInSeconds);
79
+ if (effectiveSignatureConfig.algorithm === 'custom') {
80
+ return (0, custom_algorithms_1.createCustomVerifier)(secret, effectiveSignatureConfig, toleranceInSeconds);
76
81
  }
77
82
  // Use algorithm-based verifiers for standard algorithms
78
- return (0, algorithms_1.createAlgorithmVerifier)(secret, signatureConfig, config.platform, toleranceInSeconds);
83
+ return (0, algorithms_1.createAlgorithmVerifier)(secret, effectiveSignatureConfig, config.platform, toleranceInSeconds);
79
84
  }
80
85
  static getLegacyVerifier(config) {
81
86
  // For legacy support, we'll use the algorithm-based approach
@@ -87,29 +92,28 @@ class WebhookVerificationService {
87
92
  return this.createAlgorithmBasedVerifier(configWithSignature);
88
93
  }
89
94
  // New method to create verifier using platform algorithm config
90
- static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300, normalize = false) {
95
+ static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300) {
91
96
  const platformConfig = (0, algorithms_2.getPlatformAlgorithmConfig)(platform);
92
97
  const config = {
93
98
  platform,
94
99
  secret,
95
100
  toleranceInSeconds,
96
- signatureConfig: platformConfig.signatureConfig,
97
- normalize,
101
+ signatureConfig: platformConfig.signatureConfig
98
102
  };
99
103
  return this.verify(request, config);
100
104
  }
101
- static async verifyAny(request, secrets, toleranceInSeconds = 300, normalize = false) {
105
+ static async verifyAny(request, secrets, toleranceInSeconds = 300) {
102
106
  const requestClone = request.clone();
103
107
  const detectedPlatform = this.detectPlatform(requestClone);
104
108
  if (detectedPlatform !== 'unknown' && secrets[detectedPlatform]) {
105
- return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds, normalize);
109
+ return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds);
106
110
  }
107
111
  const failedAttempts = [];
108
112
  const verificationResults = await Promise.all(Object.entries(secrets)
109
113
  .filter(([, secret]) => Boolean(secret))
110
114
  .map(async ([platform, secret]) => {
111
115
  const normalizedPlatform = platform.toLowerCase();
112
- const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds, normalize);
116
+ const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds);
113
117
  return {
114
118
  platform: normalizedPlatform,
115
119
  result,
@@ -189,6 +193,10 @@ class WebhookVerificationService {
189
193
  case 'workos':
190
194
  case 'sentry':
191
195
  case 'vercel':
196
+ case 'linear':
197
+ case 'pagerduty':
198
+ case 'twilio':
199
+ case 'svix':
192
200
  return this.pickString(payload?.id) || null;
193
201
  case 'doppler':
194
202
  return this.pickString(payload?.event?.id, metadata?.id) || null;
@@ -220,7 +228,13 @@ class WebhookVerificationService {
220
228
  if (headers.has('x-hub-signature-256'))
221
229
  return 'github';
222
230
  if (headers.has('svix-signature'))
223
- return 'clerk';
231
+ return headers.has('svix-id') ? 'svix' : 'clerk';
232
+ if (headers.has('linear-signature'))
233
+ return 'linear';
234
+ if (headers.has('x-pagerduty-signature'))
235
+ return 'pagerduty';
236
+ if (headers.has('x-twilio-signature'))
237
+ return 'twilio';
224
238
  if (headers.has('workos-signature'))
225
239
  return 'workos';
226
240
  if (headers.has('webhook-signature')) {
@@ -365,10 +379,6 @@ var algorithms_4 = require("./verifiers/algorithms");
365
379
  Object.defineProperty(exports, "createAlgorithmVerifier", { enumerable: true, get: function () { return algorithms_4.createAlgorithmVerifier; } });
366
380
  var custom_algorithms_2 = require("./verifiers/custom-algorithms");
367
381
  Object.defineProperty(exports, "createCustomVerifier", { enumerable: true, get: function () { return custom_algorithms_2.createCustomVerifier; } });
368
- var simple_2 = require("./normalization/simple");
369
- Object.defineProperty(exports, "normalizePayload", { enumerable: true, get: function () { return simple_2.normalizePayload; } });
370
- Object.defineProperty(exports, "getPlatformNormalizationCategory", { enumerable: true, get: function () { return simple_2.getPlatformNormalizationCategory; } });
371
- Object.defineProperty(exports, "getPlatformsByCategory", { enumerable: true, get: function () { return simple_2.getPlatformsByCategory; } });
372
382
  __exportStar(require("./adapters"), exports);
373
383
  __exportStar(require("./alerts"), exports);
374
384
  exports.default = WebhookVerificationService;
@@ -51,6 +51,27 @@ exports.platformAlgorithmConfigs = {
51
51
  },
52
52
  description: "Clerk webhooks use HMAC-SHA256 with base64 encoding",
53
53
  },
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
+ },
54
75
  dodopayments: {
55
76
  platform: "dodopayments",
56
77
  signatureConfig: {
@@ -287,6 +308,50 @@ exports.platformAlgorithmConfigs = {
287
308
  },
288
309
  description: "Sanity webhooks use Stripe-compatible HMAC-SHA256 with base64 encoded signature and plain UTF-8 secret",
289
310
  },
311
+ linear: {
312
+ platform: 'linear',
313
+ signatureConfig: {
314
+ algorithm: 'hmac-sha256',
315
+ headerName: 'linear-signature',
316
+ headerFormat: 'raw',
317
+ payloadFormat: 'raw',
318
+ customConfig: {
319
+ replayToleranceMs: 60000,
320
+ },
321
+ },
322
+ description: 'Linear webhooks use HMAC-SHA256 on the raw body with a 60s timestamp replay window',
323
+ },
324
+ pagerduty: {
325
+ platform: 'pagerduty',
326
+ signatureConfig: {
327
+ algorithm: 'hmac-sha256',
328
+ headerName: 'x-pagerduty-signature',
329
+ headerFormat: 'raw',
330
+ payloadFormat: 'raw',
331
+ prefix: 'v1=',
332
+ customConfig: {
333
+ signatureFormat: 'v1={signature}',
334
+ comparePrefixed: true,
335
+ },
336
+ },
337
+ description: 'PagerDuty webhooks use HMAC-SHA256 with v1=<hex> signatures',
338
+ },
339
+ twilio: {
340
+ platform: 'twilio',
341
+ 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
+ },
352
+ },
353
+ description: 'Twilio webhooks use HMAC-SHA1 with base64 signatures (URL canonicalization required)',
354
+ },
290
355
  custom: {
291
356
  platform: "custom",
292
357
  signatureConfig: {
package/dist/types.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- export type WebhookPlatform = 'custom' | 'clerk' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'paddle' | 'razorpay' | 'lemonsqueezy' | 'workos' | 'woocommerce' | 'replicateai' | 'falai' | 'sentry' | 'grafana' | 'doppler' | 'sanity' | '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' | 'pagerduty' | 'twilio' | 'unknown';
2
2
  export declare enum WebhookPlatformKeys {
3
3
  GitHub = "github",
4
4
  Stripe = "stripe",
5
5
  Clerk = "clerk",
6
+ Svix = "svix",
6
7
  DodoPayments = "dodopayments",
7
8
  Shopify = "shopify",
8
9
  Vercel = "vercel",
@@ -19,6 +20,9 @@ export declare enum WebhookPlatformKeys {
19
20
  Grafana = "grafana",
20
21
  Doppler = "doppler",
21
22
  Sanity = "sanity",
23
+ Linear = "linear",
24
+ PagerDuty = "pagerduty",
25
+ Twilio = "twilio",
22
26
  Custom = "custom",
23
27
  Unknown = "unknown"
24
28
  }
@@ -35,69 +39,6 @@ export interface SignatureConfig {
35
39
  customConfig?: Record<string, any>;
36
40
  }
37
41
  export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
38
- export type NormalizationCategory = 'payment' | 'auth' | 'ecommerce' | 'infrastructure';
39
- export interface BaseNormalizedWebhook {
40
- category: NormalizationCategory;
41
- event: string;
42
- _platform: WebhookPlatform | string;
43
- _raw: unknown;
44
- occurred_at?: string;
45
- }
46
- export type PaymentWebhookEvent = 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'subscription.created' | 'subscription.cancelled' | 'payment.unknown';
47
- export interface PaymentWebhookNormalized extends BaseNormalizedWebhook {
48
- category: 'payment';
49
- event: PaymentWebhookEvent;
50
- amount?: number;
51
- currency?: string;
52
- customer_id?: string;
53
- transaction_id?: string;
54
- subscription_id?: string;
55
- refund_amount?: number;
56
- failure_reason?: string;
57
- metadata?: Record<string, string>;
58
- }
59
- export type AuthWebhookEvent = 'user.created' | 'user.updated' | 'user.deleted' | 'session.started' | 'session.ended' | 'auth.unknown';
60
- export interface AuthWebhookNormalized extends BaseNormalizedWebhook {
61
- category: 'auth';
62
- event: AuthWebhookEvent;
63
- user_id?: string;
64
- email?: string;
65
- phone?: string;
66
- metadata?: Record<string, string>;
67
- }
68
- export interface EcommerceWebhookNormalized extends BaseNormalizedWebhook {
69
- category: 'ecommerce';
70
- event: string;
71
- order_id?: string;
72
- customer_id?: string;
73
- amount?: number;
74
- currency?: string;
75
- metadata?: Record<string, string>;
76
- }
77
- export interface InfrastructureWebhookNormalized extends BaseNormalizedWebhook {
78
- category: 'infrastructure';
79
- event: string;
80
- project_id?: string;
81
- deployment_id?: string;
82
- status?: 'queued' | 'building' | 'ready' | 'error' | 'unknown';
83
- metadata?: Record<string, string>;
84
- }
85
- export interface UnknownNormalizedWebhook extends BaseNormalizedWebhook {
86
- event: string;
87
- warning?: string;
88
- }
89
- export type NormalizedPayloadByCategory = {
90
- payment: PaymentWebhookNormalized;
91
- auth: AuthWebhookNormalized;
92
- ecommerce: EcommerceWebhookNormalized;
93
- infrastructure: InfrastructureWebhookNormalized;
94
- };
95
- export type AnyNormalizedWebhook = PaymentWebhookNormalized | AuthWebhookNormalized | EcommerceWebhookNormalized | InfrastructureWebhookNormalized | UnknownNormalizedWebhook;
96
- export interface NormalizeOptions {
97
- enabled?: boolean;
98
- category?: NormalizationCategory;
99
- includeRaw?: boolean;
100
- }
101
42
  export interface WebhookVerificationResult<TPayload = unknown> {
102
43
  isValid: boolean;
103
44
  error?: string;
@@ -116,7 +57,7 @@ export interface WebhookConfig {
116
57
  secret: string;
117
58
  toleranceInSeconds?: number;
118
59
  signatureConfig?: SignatureConfig;
119
- normalize?: boolean | NormalizeOptions;
60
+ twilioBaseUrl?: string;
120
61
  }
121
62
  export interface MultiPlatformSecrets {
122
63
  [platform: string]: string | undefined;
package/dist/types.js CHANGED
@@ -6,6 +6,7 @@ var WebhookPlatformKeys;
6
6
  WebhookPlatformKeys["GitHub"] = "github";
7
7
  WebhookPlatformKeys["Stripe"] = "stripe";
8
8
  WebhookPlatformKeys["Clerk"] = "clerk";
9
+ WebhookPlatformKeys["Svix"] = "svix";
9
10
  WebhookPlatformKeys["DodoPayments"] = "dodopayments";
10
11
  WebhookPlatformKeys["Shopify"] = "shopify";
11
12
  WebhookPlatformKeys["Vercel"] = "vercel";
@@ -22,6 +23,9 @@ var WebhookPlatformKeys;
22
23
  WebhookPlatformKeys["Grafana"] = "grafana";
23
24
  WebhookPlatformKeys["Doppler"] = "doppler";
24
25
  WebhookPlatformKeys["Sanity"] = "sanity";
26
+ WebhookPlatformKeys["Linear"] = "linear";
27
+ WebhookPlatformKeys["PagerDuty"] = "pagerduty";
28
+ WebhookPlatformKeys["Twilio"] = "twilio";
25
29
  WebhookPlatformKeys["Custom"] = "custom";
26
30
  WebhookPlatformKeys["Unknown"] = "unknown";
27
31
  })(WebhookPlatformKeys || (exports.WebhookPlatformKeys = WebhookPlatformKeys = {}));
@@ -5,11 +5,17 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
5
5
  protected platform: WebhookPlatform;
6
6
  constructor(secret: string, config: SignatureConfig, platform: WebhookPlatform, toleranceInSeconds?: number);
7
7
  abstract verify(request: Request): Promise<WebhookVerificationResult>;
8
+ protected getMissingSignatureMessage(): string;
9
+ protected getMissingTimestampMessage(): string;
10
+ protected getTimestampExpiredMessage(): string;
11
+ protected getInvalidSignatureMessage(): string;
12
+ protected getVerificationErrorMessage(error: Error): string;
8
13
  protected parseDelimitedHeader(headerValue: string): Record<string, string>;
9
14
  protected extractSignatures(request: Request): string[];
10
15
  protected extractTimestamp(request: Request): number | null;
11
16
  protected extractTimestampFromSignature(request: Request): number | null;
12
17
  protected requiresTimestamp(): boolean;
18
+ protected resolveTwilioSignatureUrl(request: Request): string;
13
19
  protected formatPayload(rawBody: string, request: Request): string;
14
20
  protected formatCustomPayload(rawBody: string, request: Request): string;
15
21
  protected verifyHMAC(payload: string, signature: string, algorithm?: string): boolean;
@@ -18,6 +24,8 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
18
24
  protected extractMetadata(request: Request): Record<string, any>;
19
25
  }
20
26
  export declare class GenericHMACVerifier extends AlgorithmBasedVerifier {
27
+ private validateLinearReplayWindow;
28
+ private validateTwilioBodyHash;
21
29
  private resolveSentryPayloadCandidates;
22
30
  verify(request: Request): Promise<WebhookVerificationResult>;
23
31
  }