@hookflo/tern 1.0.4 → 1.0.6

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
@@ -109,6 +109,67 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
109
109
  - **Polar**: HMAC-SHA256
110
110
  - **Supabase**: Token-based authentication
111
111
 
112
+ ## Custom Platform Configuration
113
+
114
+ This framework is fully configuration-driven. You can verify webhooks from any provider—even if it is not built-in—by supplying a custom configuration object. This allows you to support new or proprietary platforms instantly, without waiting for a library update.
115
+
116
+ ### Example: Standard HMAC-SHA256 Webhook
117
+
118
+ ```typescript
119
+ import { WebhookVerificationService } from '@hookflo/tern';
120
+
121
+ const acmeConfig = {
122
+ platform: 'acmepay',
123
+ secret: 'acme_secret',
124
+ signatureConfig: {
125
+ algorithm: 'hmac-sha256',
126
+ headerName: 'x-acme-signature',
127
+ headerFormat: 'raw',
128
+ timestampHeader: 'x-acme-timestamp',
129
+ timestampFormat: 'unix',
130
+ payloadFormat: 'timestamped', // signs as {timestamp}.{body}
131
+ }
132
+ };
133
+
134
+ const result = await WebhookVerificationService.verify(request, acmeConfig);
135
+ ```
136
+
137
+ ### Example: Svix/Standard Webhooks (Clerk, Dodo Payments, etc.)
138
+
139
+ ```typescript
140
+ const svixConfig = {
141
+ platform: 'my-svix-platform',
142
+ secret: 'whsec_abc123...',
143
+ signatureConfig: {
144
+ algorithm: 'hmac-sha256',
145
+ headerName: 'webhook-signature',
146
+ headerFormat: 'raw',
147
+ timestampHeader: 'webhook-timestamp',
148
+ timestampFormat: 'unix',
149
+ payloadFormat: 'custom',
150
+ customConfig: {
151
+ payloadFormat: '{id}.{timestamp}.{body}',
152
+ idHeader: 'webhook-id',
153
+ // encoding: 'base64' // only if the provider uses base64, otherwise omit
154
+ }
155
+ }
156
+ };
157
+
158
+ const result = await WebhookVerificationService.verify(request, svixConfig);
159
+ ```
160
+
161
+ You can configure any combination of algorithm, header, payload, and encoding. See the `SignatureConfig` type for all options.
162
+
163
+ ## Webhook Verification OK Tested Platforms
164
+ - **Stripe**
165
+ - **Supabase**
166
+ - **Github**
167
+ - **Clerk**
168
+ - **Dodo Payments**
169
+
170
+ - **Other Platforms** : Yet to verify....
171
+
172
+
112
173
  ## Custom Configurations
113
174
 
114
175
  ### Custom HMAC-SHA256
@@ -301,4 +362,4 @@ MIT License - see [LICENSE](./LICENSE) for details.
301
362
 
302
363
  - [Documentation](./USAGE.md)
303
364
  - [Framework Summary](./FRAMEWORK_SUMMARY.md)
304
- - [Issues](https://github.com/your-repo/tern/issues)
365
+ - [Issues](https://github.com/Hookflo/tern/issues)
@@ -5,9 +5,7 @@ exports.getPlatformAlgorithmConfig = getPlatformAlgorithmConfig;
5
5
  exports.platformUsesAlgorithm = platformUsesAlgorithm;
6
6
  exports.getPlatformsUsingAlgorithm = getPlatformsUsingAlgorithm;
7
7
  exports.validateSignatureConfig = validateSignatureConfig;
8
- // Platform to algorithm mapping configuration
9
8
  exports.platformAlgorithmConfigs = {
10
- // GitHub uses HMAC-SHA256 with prefixed signature
11
9
  github: {
12
10
  platform: "github",
13
11
  signatureConfig: {
@@ -15,19 +13,18 @@ exports.platformAlgorithmConfigs = {
15
13
  headerName: "x-hub-signature-256",
16
14
  headerFormat: "prefixed",
17
15
  prefix: "sha256=",
18
- timestampHeader: undefined, // GitHub doesn't use timestamp validation
16
+ timestampHeader: undefined,
19
17
  payloadFormat: "raw",
20
18
  },
21
19
  description: "GitHub webhooks use HMAC-SHA256 with sha256= prefix",
22
20
  },
23
- // Stripe uses HMAC-SHA256 with comma-separated format
24
21
  stripe: {
25
22
  platform: "stripe",
26
23
  signatureConfig: {
27
24
  algorithm: "hmac-sha256",
28
25
  headerName: "stripe-signature",
29
26
  headerFormat: "comma-separated",
30
- timestampHeader: undefined, // Timestamp is embedded in signature
27
+ timestampHeader: undefined,
31
28
  payloadFormat: "timestamped",
32
29
  customConfig: {
33
30
  signatureFormat: "t={timestamp},v1={signature}",
@@ -35,7 +32,6 @@ exports.platformAlgorithmConfigs = {
35
32
  },
36
33
  description: "Stripe webhooks use HMAC-SHA256 with comma-separated format",
37
34
  },
38
- // Clerk uses HMAC-SHA256 with custom base64 encoding
39
35
  clerk: {
40
36
  platform: "clerk",
41
37
  signatureConfig: {
@@ -54,7 +50,6 @@ exports.platformAlgorithmConfigs = {
54
50
  },
55
51
  description: "Clerk webhooks use HMAC-SHA256 with base64 encoding",
56
52
  },
57
- // Dodo Payments uses HMAC-SHA256
58
53
  dodopayments: {
59
54
  platform: "dodopayments",
60
55
  signatureConfig: {
@@ -71,9 +66,8 @@ exports.platformAlgorithmConfigs = {
71
66
  idHeader: "webhook-id",
72
67
  },
73
68
  },
74
- description: "Dodo Payments webhooks use HMAC-SHA256",
69
+ description: "Dodo Payments webhooks use HMAC-SHA256 with svix-style format (Standard Webhooks)",
75
70
  },
76
- // Shopify uses HMAC-SHA256
77
71
  shopify: {
78
72
  platform: "shopify",
79
73
  signatureConfig: {
@@ -85,7 +79,6 @@ exports.platformAlgorithmConfigs = {
85
79
  },
86
80
  description: "Shopify webhooks use HMAC-SHA256",
87
81
  },
88
- // Vercel uses HMAC-SHA256
89
82
  vercel: {
90
83
  platform: "vercel",
91
84
  signatureConfig: {
@@ -98,7 +91,6 @@ exports.platformAlgorithmConfigs = {
98
91
  },
99
92
  description: "Vercel webhooks use HMAC-SHA256",
100
93
  },
101
- // Polar uses HMAC-SHA256
102
94
  polar: {
103
95
  platform: "polar",
104
96
  signatureConfig: {
@@ -111,7 +103,6 @@ exports.platformAlgorithmConfigs = {
111
103
  },
112
104
  description: "Polar webhooks use HMAC-SHA256",
113
105
  },
114
- // Supabase uses simple token-based authentication
115
106
  supabase: {
116
107
  platform: "supabase",
117
108
  signatureConfig: {
@@ -126,7 +117,6 @@ exports.platformAlgorithmConfigs = {
126
117
  },
127
118
  description: "Supabase webhooks use token-based authentication",
128
119
  },
129
- // Custom platform - can be configured per instance
130
120
  custom: {
131
121
  platform: "custom",
132
122
  signatureConfig: {
@@ -141,7 +131,6 @@ exports.platformAlgorithmConfigs = {
141
131
  },
142
132
  description: "Custom webhook configuration",
143
133
  },
144
- // Unknown platform - fallback
145
134
  unknown: {
146
135
  platform: "unknown",
147
136
  signatureConfig: {
@@ -153,37 +142,32 @@ exports.platformAlgorithmConfigs = {
153
142
  description: "Unknown platform - using default HMAC-SHA256",
154
143
  },
155
144
  };
156
- // Helper function to get algorithm config for a platform
157
145
  function getPlatformAlgorithmConfig(platform) {
158
146
  return exports.platformAlgorithmConfigs[platform] || exports.platformAlgorithmConfigs.unknown;
159
147
  }
160
- // Helper function to check if a platform uses a specific algorithm
161
148
  function platformUsesAlgorithm(platform, algorithm) {
162
149
  const config = getPlatformAlgorithmConfig(platform);
163
150
  return config.signatureConfig.algorithm === algorithm;
164
151
  }
165
- // Helper function to get all platforms using a specific algorithm
166
152
  function getPlatformsUsingAlgorithm(algorithm) {
167
153
  return Object.entries(exports.platformAlgorithmConfigs)
168
154
  .filter(([_, config]) => config.signatureConfig.algorithm === algorithm)
169
155
  .map(([platform, _]) => platform);
170
156
  }
171
- // Helper function to validate signature config
172
157
  function validateSignatureConfig(config) {
173
158
  if (!config.algorithm || !config.headerName) {
174
159
  return false;
175
160
  }
176
- // Validate algorithm-specific requirements
177
161
  switch (config.algorithm) {
178
162
  case "hmac-sha256":
179
163
  case "hmac-sha1":
180
164
  case "hmac-sha512":
181
- return true; // These algorithms only need headerName
165
+ return true;
182
166
  case "rsa-sha256":
183
167
  case "ed25519":
184
- return !!config.customConfig?.publicKey; // These need public key
168
+ return !!config.customConfig?.publicKey;
185
169
  case "custom":
186
- return !!config.customConfig; // Custom needs custom config
170
+ return !!config.customConfig;
187
171
  default:
188
172
  return false;
189
173
  }
package/dist/test.js CHANGED
@@ -3,10 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runTests = runTests;
4
4
  const crypto_1 = require("crypto");
5
5
  const index_1 = require("./index");
6
- // Test data
7
6
  const testSecret = 'whsec_test_secret_key_12345';
8
7
  const testBody = JSON.stringify({ event: 'test', data: { id: '123' } });
9
- // Helper function to create a mock request
10
8
  function createMockRequest(headers, body = testBody) {
11
9
  return new Request('https://example.com/webhook', {
12
10
  method: 'POST',
@@ -14,7 +12,6 @@ function createMockRequest(headers, body = testBody) {
14
12
  body,
15
13
  });
16
14
  }
17
- // Helper function to create Stripe signature
18
15
  function createStripeSignature(body, secret, timestamp) {
19
16
  const signedPayload = `${timestamp}.${body}`;
20
17
  const hmac = (0, crypto_1.createHmac)('sha256', secret);
@@ -22,13 +19,11 @@ function createStripeSignature(body, secret, timestamp) {
22
19
  const signature = hmac.digest('hex');
23
20
  return `t=${timestamp},v1=${signature}`;
24
21
  }
25
- // Helper function to create GitHub signature
26
22
  function createGitHubSignature(body, secret) {
27
23
  const hmac = (0, crypto_1.createHmac)('sha256', secret);
28
24
  hmac.update(body);
29
25
  return `sha256=${hmac.digest('hex')}`;
30
26
  }
31
- // Helper function to create Clerk signature
32
27
  function createClerkSignature(body, secret, id, timestamp) {
33
28
  const signedContent = `${id}.${timestamp}.${body}`;
34
29
  const secretBytes = new Uint8Array(Buffer.from(secret.split('_')[1], 'base64'));
@@ -118,6 +113,36 @@ async function runTests() {
118
113
  catch (error) {
119
114
  console.log(' ❌ Generic test failed:', error);
120
115
  }
116
+ // Test 4.5: Dodo Payments (Standard Webhooks / svix-style)
117
+ console.log('\n4.5. Testing Dodo Payments...');
118
+ try {
119
+ const webhookId = 'test-webhook-id-123';
120
+ const timestamp = Math.floor(Date.now() / 1000);
121
+ // Create a proper secret format for Standard Webhooks (whsec_ + base64 encoded secret)
122
+ const base64Secret = Buffer.from(testSecret).toString('base64');
123
+ const dodoSecret = `whsec_${base64Secret}`;
124
+ // Create svix-style signature: {webhook-id}.{webhook-timestamp}.{payload}
125
+ const signedContent = `${webhookId}.${timestamp}.${testBody}`;
126
+ // Use the base64-decoded secret for HMAC (like the Standard Webhooks library)
127
+ const secretBytes = new Uint8Array(Buffer.from(base64Secret, 'base64'));
128
+ const hmac = (0, crypto_1.createHmac)('sha256', secretBytes);
129
+ hmac.update(signedContent);
130
+ const signature = `v1,${hmac.digest('base64')}`;
131
+ const dodoRequest = createMockRequest({
132
+ 'webhook-signature': signature,
133
+ 'webhook-id': webhookId,
134
+ 'webhook-timestamp': timestamp.toString(),
135
+ 'content-type': 'application/json',
136
+ });
137
+ const dodoResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(dodoRequest, 'dodopayments', dodoSecret);
138
+ console.log(' ✅ Dodo Payments:', dodoResult.isValid ? 'PASSED' : 'FAILED');
139
+ if (!dodoResult.isValid) {
140
+ console.log(' ❌ Error:', dodoResult.error);
141
+ }
142
+ }
143
+ catch (error) {
144
+ console.log(' ❌ Dodo Payments test failed:', error);
145
+ }
121
146
  // Test 5: Token-based (Supabase)
122
147
  console.log('\n5. Testing Token-based Authentication...');
123
148
  try {
@@ -31,8 +31,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
31
31
  return sigMap.v1 || sigMap.signature || null;
32
32
  case "raw":
33
33
  default:
34
- // For Clerk, handle space-separated signatures
35
- if (this.platform === "clerk") {
34
+ if (this.platform === "clerk" || this.platform === "dodopayments") {
36
35
  const signatures = headerValue.split(" ");
37
36
  for (const sig of signatures) {
38
37
  const [version, signature] = sig.split(",");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",