@hookflo/tern 4.3.1-beta → 4.3.3-beta

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 (46) hide show
  1. package/README.md +27 -0
  2. package/dist/adapters/cloudflare.d.ts +1 -2
  3. package/dist/adapters/cloudflare.js +1 -1
  4. package/dist/adapters/express.d.ts +1 -2
  5. package/dist/adapters/express.js +1 -1
  6. package/dist/adapters/hono.d.ts +1 -2
  7. package/dist/adapters/hono.js +1 -1
  8. package/dist/adapters/nextjs.d.ts +1 -2
  9. package/dist/adapters/nextjs.js +1 -1
  10. package/dist/adapters/shared.js +5 -2
  11. package/dist/index.d.ts +3 -4
  12. package/dist/index.js +18 -18
  13. package/dist/types.d.ts +1 -64
  14. package/dist/verifiers/algorithms.d.ts +6 -0
  15. package/dist/verifiers/algorithms.js +56 -12
  16. package/package.json +1 -1
  17. package/dist/normalization/index.d.ts +0 -20
  18. package/dist/normalization/index.js +0 -78
  19. package/dist/normalization/providers/payment/paypal.d.ts +0 -2
  20. package/dist/normalization/providers/payment/paypal.js +0 -12
  21. package/dist/normalization/providers/payment/razorpay.d.ts +0 -2
  22. package/dist/normalization/providers/payment/razorpay.js +0 -13
  23. package/dist/normalization/providers/payment/stripe.d.ts +0 -2
  24. package/dist/normalization/providers/payment/stripe.js +0 -13
  25. package/dist/normalization/providers/registry.d.ts +0 -5
  26. package/dist/normalization/providers/registry.js +0 -21
  27. package/dist/normalization/simple.d.ts +0 -4
  28. package/dist/normalization/simple.js +0 -126
  29. package/dist/normalization/storage/interface.d.ts +0 -13
  30. package/dist/normalization/storage/interface.js +0 -2
  31. package/dist/normalization/storage/memory.d.ts +0 -12
  32. package/dist/normalization/storage/memory.js +0 -39
  33. package/dist/normalization/templates/base/auth.d.ts +0 -2
  34. package/dist/normalization/templates/base/auth.js +0 -22
  35. package/dist/normalization/templates/base/ecommerce.d.ts +0 -2
  36. package/dist/normalization/templates/base/ecommerce.js +0 -25
  37. package/dist/normalization/templates/base/payment.d.ts +0 -2
  38. package/dist/normalization/templates/base/payment.js +0 -25
  39. package/dist/normalization/templates/registry.d.ts +0 -6
  40. package/dist/normalization/templates/registry.js +0 -22
  41. package/dist/normalization/transformer/engine.d.ts +0 -11
  42. package/dist/normalization/transformer/engine.js +0 -86
  43. package/dist/normalization/transformer/validator.d.ts +0 -12
  44. package/dist/normalization/transformer/validator.js +0 -56
  45. package/dist/normalization/types.d.ts +0 -79
  46. 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 |
@@ -407,6 +431,9 @@ interface WebhookVerificationResult {
407
431
 
408
432
  ## Troubleshooting
409
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
+
410
437
  **`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
411
438
 
412
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,6 @@ export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>,
6
6
  secret?: string;
7
7
  secretEnv?: string;
8
8
  toleranceInSeconds?: number;
9
- normalize?: boolean | NormalizeOptions;
10
9
  queue?: QueueOption;
11
10
  alerts?: AlertConfig;
12
11
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -39,7 +39,7 @@ 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.verifyWithPlatformConfig(request, options.platform, secret, options.toleranceInSeconds);
43
43
  if (!result.isValid) {
44
44
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
45
45
  }
@@ -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,6 @@ export interface ExpressWebhookMiddlewareOptions {
14
14
  platform: WebhookPlatform;
15
15
  secret: string;
16
16
  toleranceInSeconds?: number;
17
- normalize?: boolean | NormalizeOptions;
18
17
  queue?: QueueOption;
19
18
  alerts?: AlertConfig;
20
19
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -48,7 +48,7 @@ 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.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds);
52
52
  if (!result.isValid) {
53
53
  res.status(400).json({
54
54
  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,6 @@ export interface HonoWebhookHandlerOptions<TContext extends HonoContextLike = Ho
11
11
  platform: WebhookPlatform;
12
12
  secret: string;
13
13
  toleranceInSeconds?: number;
14
- normalize?: boolean | NormalizeOptions;
15
14
  queue?: QueueOption;
16
15
  alerts?: AlertConfig;
17
16
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -35,7 +35,7 @@ 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.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds);
39
39
  if (!result.isValid) {
40
40
  return c.json({
41
41
  error: result.error,
@@ -1,11 +1,10 @@
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;
9
8
  queue?: QueueOption;
10
9
  alerts?: AlertConfig;
11
10
  alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
@@ -34,7 +34,7 @@ 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.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds);
38
38
  if (!result.isValid) {
39
39
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
40
40
  }
@@ -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,
@@ -375,10 +379,6 @@ var algorithms_4 = require("./verifiers/algorithms");
375
379
  Object.defineProperty(exports, "createAlgorithmVerifier", { enumerable: true, get: function () { return algorithms_4.createAlgorithmVerifier; } });
376
380
  var custom_algorithms_2 = require("./verifiers/custom-algorithms");
377
381
  Object.defineProperty(exports, "createCustomVerifier", { enumerable: true, get: function () { return custom_algorithms_2.createCustomVerifier; } });
378
- var simple_2 = require("./normalization/simple");
379
- Object.defineProperty(exports, "normalizePayload", { enumerable: true, get: function () { return simple_2.normalizePayload; } });
380
- Object.defineProperty(exports, "getPlatformNormalizationCategory", { enumerable: true, get: function () { return simple_2.getPlatformNormalizationCategory; } });
381
- Object.defineProperty(exports, "getPlatformsByCategory", { enumerable: true, get: function () { return simple_2.getPlatformsByCategory; } });
382
382
  __exportStar(require("./adapters"), exports);
383
383
  __exportStar(require("./alerts"), exports);
384
384
  exports.default = WebhookVerificationService;
package/dist/types.d.ts CHANGED
@@ -39,69 +39,6 @@ export interface SignatureConfig {
39
39
  customConfig?: Record<string, any>;
40
40
  }
41
41
  export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
42
- export type NormalizationCategory = 'payment' | 'auth' | 'ecommerce' | 'infrastructure';
43
- export interface BaseNormalizedWebhook {
44
- category: NormalizationCategory;
45
- event: string;
46
- _platform: WebhookPlatform | string;
47
- _raw: unknown;
48
- occurred_at?: string;
49
- }
50
- export type PaymentWebhookEvent = 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'subscription.created' | 'subscription.cancelled' | 'payment.unknown';
51
- export interface PaymentWebhookNormalized extends BaseNormalizedWebhook {
52
- category: 'payment';
53
- event: PaymentWebhookEvent;
54
- amount?: number;
55
- currency?: string;
56
- customer_id?: string;
57
- transaction_id?: string;
58
- subscription_id?: string;
59
- refund_amount?: number;
60
- failure_reason?: string;
61
- metadata?: Record<string, string>;
62
- }
63
- export type AuthWebhookEvent = 'user.created' | 'user.updated' | 'user.deleted' | 'session.started' | 'session.ended' | 'auth.unknown';
64
- export interface AuthWebhookNormalized extends BaseNormalizedWebhook {
65
- category: 'auth';
66
- event: AuthWebhookEvent;
67
- user_id?: string;
68
- email?: string;
69
- phone?: string;
70
- metadata?: Record<string, string>;
71
- }
72
- export interface EcommerceWebhookNormalized extends BaseNormalizedWebhook {
73
- category: 'ecommerce';
74
- event: string;
75
- order_id?: string;
76
- customer_id?: string;
77
- amount?: number;
78
- currency?: string;
79
- metadata?: Record<string, string>;
80
- }
81
- export interface InfrastructureWebhookNormalized extends BaseNormalizedWebhook {
82
- category: 'infrastructure';
83
- event: string;
84
- project_id?: string;
85
- deployment_id?: string;
86
- status?: 'queued' | 'building' | 'ready' | 'error' | 'unknown';
87
- metadata?: Record<string, string>;
88
- }
89
- export interface UnknownNormalizedWebhook extends BaseNormalizedWebhook {
90
- event: string;
91
- warning?: string;
92
- }
93
- export type NormalizedPayloadByCategory = {
94
- payment: PaymentWebhookNormalized;
95
- auth: AuthWebhookNormalized;
96
- ecommerce: EcommerceWebhookNormalized;
97
- infrastructure: InfrastructureWebhookNormalized;
98
- };
99
- export type AnyNormalizedWebhook = PaymentWebhookNormalized | AuthWebhookNormalized | EcommerceWebhookNormalized | InfrastructureWebhookNormalized | UnknownNormalizedWebhook;
100
- export interface NormalizeOptions {
101
- enabled?: boolean;
102
- category?: NormalizationCategory;
103
- includeRaw?: boolean;
104
- }
105
42
  export interface WebhookVerificationResult<TPayload = unknown> {
106
43
  isValid: boolean;
107
44
  error?: string;
@@ -120,7 +57,7 @@ export interface WebhookConfig {
120
57
  secret: string;
121
58
  toleranceInSeconds?: number;
122
59
  signatureConfig?: SignatureConfig;
123
- normalize?: boolean | NormalizeOptions;
60
+ twilioBaseUrl?: string;
124
61
  }
125
62
  export interface MultiPlatformSecrets {
126
63
  [platform: string]: string | undefined;
@@ -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;
@@ -13,6 +13,40 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
13
13
  this.config = config;
14
14
  this.platform = platform;
15
15
  }
16
+ getMissingSignatureMessage() {
17
+ return `Missing signature header: ${this.config.headerName}. Ensure your webhook provider sends this header and your adapter forwards it unchanged.`;
18
+ }
19
+ getMissingTimestampMessage() {
20
+ const timestampHeader = this.config.timestampHeader || this.config.customConfig?.timestampHeader || 'timestamp';
21
+ return `Missing required timestamp for webhook verification. Verify header '${timestampHeader}' is present and passed through by your framework/proxy.`;
22
+ }
23
+ getTimestampExpiredMessage() {
24
+ return 'Webhook timestamp expired. Check server clock drift and increase tolerance only if your provider allows it.';
25
+ }
26
+ getInvalidSignatureMessage() {
27
+ const genericHint = `Invalid signature for ${this.platform}. Confirm webhook secret, raw request body handling, and signature header formatting.`;
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
+ case 'stripe':
32
+ return `${genericHint} Stripe signatures require the exact raw body and Stripe-Signature timestamp/value pair.`;
33
+ case 'github':
34
+ return `${genericHint} GitHub signatures must include the sha256= prefix from x-hub-signature-256.`;
35
+ case 'svix':
36
+ case 'clerk':
37
+ case 'dodopayments':
38
+ case 'replicateai':
39
+ case 'polar':
40
+ 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
+ default:
44
+ return genericHint;
45
+ }
46
+ }
47
+ getVerificationErrorMessage(error) {
48
+ return `${this.platform} verification error: ${error.message}. Check webhook secret configuration and ensure your framework preserves raw body + headers.`;
49
+ }
16
50
  parseDelimitedHeader(headerValue) {
17
51
  const parts = headerValue.split(/[;,]/);
18
52
  const values = {};
@@ -152,6 +186,16 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
152
186
  return true;
153
187
  return false;
154
188
  }
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
+ }
155
199
  formatPayload(rawBody, request) {
156
200
  switch (this.config.payloadFormat) {
157
201
  case "timestamped": {
@@ -196,7 +240,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
196
240
  }
197
241
  if (customFormat.includes('{url}')) {
198
242
  return customFormat
199
- .replace('{url}', request.url)
243
+ .replace('{url}', this.platform === 'twilio' ? this.resolveTwilioSignatureUrl(request) : request.url)
200
244
  .replace('{body}', rawBody);
201
245
  }
202
246
  if (customFormat.includes("{timestamp}") &&
@@ -306,7 +350,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
306
350
  if (this.platform !== 'twilio' || !this.config.customConfig?.validateBodySHA256) {
307
351
  return null;
308
352
  }
309
- const url = new URL(request.url);
353
+ const url = new URL(this.resolveTwilioSignatureUrl(request));
310
354
  const bodySha = url.searchParams.get('bodySHA256');
311
355
  if (!bodySha)
312
356
  return null;
@@ -349,7 +393,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
349
393
  if (signatures.length === 0) {
350
394
  return {
351
395
  isValid: false,
352
- error: `Missing signature header: ${this.config.headerName}`,
396
+ error: this.getMissingSignatureMessage(),
353
397
  errorCode: "MISSING_SIGNATURE",
354
398
  platform: this.platform,
355
399
  };
@@ -383,7 +427,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
383
427
  if (this.requiresTimestamp() && !timestamp) {
384
428
  return {
385
429
  isValid: false,
386
- error: 'Missing required timestamp for webhook verification',
430
+ error: this.getMissingTimestampMessage(),
387
431
  errorCode: 'MISSING_SIGNATURE',
388
432
  platform: this.platform,
389
433
  };
@@ -391,7 +435,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
391
435
  if (timestamp && !this.isTimestampValid(timestamp)) {
392
436
  return {
393
437
  isValid: false,
394
- error: "Webhook timestamp expired",
438
+ error: this.getTimestampExpiredMessage(),
395
439
  errorCode: "TIMESTAMP_EXPIRED",
396
440
  platform: this.platform,
397
441
  };
@@ -423,7 +467,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
423
467
  if (!isValid) {
424
468
  return {
425
469
  isValid: false,
426
- error: "Invalid signature",
470
+ error: this.getInvalidSignatureMessage(),
427
471
  errorCode: "INVALID_SIGNATURE",
428
472
  platform: this.platform,
429
473
  };
@@ -452,7 +496,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
452
496
  catch (error) {
453
497
  return {
454
498
  isValid: false,
455
- error: `${this.platform} verification error: ${error.message}`,
499
+ error: this.getVerificationErrorMessage(error),
456
500
  errorCode: "VERIFICATION_ERROR",
457
501
  platform: this.platform,
458
502
  };
@@ -561,7 +605,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
561
605
  if (signatures.length === 0) {
562
606
  return {
563
607
  isValid: false,
564
- error: `Missing signature header: ${this.config.headerName}`,
608
+ error: this.getMissingSignatureMessage(),
565
609
  errorCode: "MISSING_SIGNATURE",
566
610
  platform: this.platform,
567
611
  };
@@ -575,7 +619,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
575
619
  if (!timestampStr) {
576
620
  return {
577
621
  isValid: false,
578
- error: 'Missing required timestamp for webhook verification',
622
+ error: this.getMissingTimestampMessage(),
579
623
  errorCode: 'MISSING_SIGNATURE',
580
624
  platform: this.platform,
581
625
  };
@@ -584,7 +628,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
584
628
  if (!this.isTimestampValid(timestamp)) {
585
629
  return {
586
630
  isValid: false,
587
- error: "Webhook timestamp expired",
631
+ error: this.getTimestampExpiredMessage(),
588
632
  errorCode: "TIMESTAMP_EXPIRED",
589
633
  platform: this.platform,
590
634
  };
@@ -644,7 +688,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
644
688
  if (!isValid) {
645
689
  return {
646
690
  isValid: false,
647
- error: "Invalid signature",
691
+ error: this.getInvalidSignatureMessage(),
648
692
  errorCode: "INVALID_SIGNATURE",
649
693
  platform: this.platform,
650
694
  };
@@ -673,7 +717,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
673
717
  catch (error) {
674
718
  return {
675
719
  isValid: false,
676
- error: `${this.platform} verification error: ${error.message}`,
720
+ error: this.getVerificationErrorMessage(error),
677
721
  errorCode: "VERIFICATION_ERROR",
678
722
  platform: this.platform,
679
723
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "4.3.1-beta",
3
+ "version": "4.3.3-beta",
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",
@@ -1,20 +0,0 @@
1
- import { BaseTemplate, CreateSchemaInput, NormalizedResult, ProviderInfo, TemplateCategory, TransformParams, UpdateSchemaInput, UserSchema } from './types';
2
- import { StorageAdapter } from './storage/interface';
3
- export declare class Normalizer {
4
- private readonly storage;
5
- private engine;
6
- constructor(storage?: StorageAdapter);
7
- getBaseTemplates(): Promise<BaseTemplate[]>;
8
- getProviders(category?: TemplateCategory): Promise<ProviderInfo[]>;
9
- createSchema(input: CreateSchemaInput): Promise<UserSchema>;
10
- updateSchema(schemaId: string, updates: UpdateSchemaInput): Promise<void>;
11
- getSchema(id: string): Promise<UserSchema | null>;
12
- transform(params: TransformParams): Promise<NormalizedResult>;
13
- validateSchema(schema: UserSchema): Promise<{
14
- valid: boolean;
15
- errors: string[];
16
- }>;
17
- }
18
- export * from './types';
19
- export * from './storage/interface';
20
- export { InMemoryStorageAdapter } from './storage/memory';
@@ -1,78 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.InMemoryStorageAdapter = exports.Normalizer = void 0;
18
- const registry_1 = require("./providers/registry");
19
- const registry_2 = require("./templates/registry");
20
- const memory_1 = require("./storage/memory");
21
- const engine_1 = require("./transformer/engine");
22
- const validator_1 = require("./transformer/validator");
23
- class Normalizer {
24
- constructor(storage = new memory_1.InMemoryStorageAdapter()) {
25
- this.storage = storage;
26
- this.engine = new engine_1.NormalizationEngine(storage, new validator_1.SchemaValidator());
27
- }
28
- async getBaseTemplates() {
29
- return this.storage.listBaseTemplates();
30
- }
31
- async getProviders(category) {
32
- return registry_1.providerRegistry.list(category);
33
- }
34
- async createSchema(input) {
35
- const schema = {
36
- id: generateId(),
37
- userId: input.userId,
38
- baseTemplateId: input.baseTemplateId,
39
- category: input.category,
40
- fields: input.fields,
41
- providerMappings: input.providerMappings,
42
- createdAt: new Date(),
43
- updatedAt: new Date(),
44
- };
45
- await this.storage.saveSchema(schema);
46
- return schema;
47
- }
48
- async updateSchema(schemaId, updates) {
49
- await this.storage.updateSchema(schemaId, updates);
50
- }
51
- async getSchema(id) {
52
- return this.storage.getSchema(id);
53
- }
54
- async transform(params) {
55
- return this.engine.transform(params);
56
- }
57
- async validateSchema(schema) {
58
- const base = (await this.storage.getBaseTemplate(schema.baseTemplateId))
59
- ?? registry_2.templateRegistry.getById(schema.baseTemplateId);
60
- if (!base) {
61
- return {
62
- valid: false,
63
- errors: [`Base template not found: ${schema.baseTemplateId}`],
64
- };
65
- }
66
- const validator = new validator_1.SchemaValidator();
67
- return validator.validateSchema(schema, base);
68
- }
69
- }
70
- exports.Normalizer = Normalizer;
71
- function generateId() {
72
- // Simple non-crypto unique ID generator for framework default
73
- return (`sch_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`);
74
- }
75
- __exportStar(require("./types"), exports);
76
- __exportStar(require("./storage/interface"), exports);
77
- var memory_2 = require("./storage/memory");
78
- Object.defineProperty(exports, "InMemoryStorageAdapter", { enumerable: true, get: function () { return memory_2.InMemoryStorageAdapter; } });