@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 +62 -1
- package/dist/platforms/algorithms.js +6 -22
- package/dist/test.js +30 -5
- package/dist/verifiers/algorithms.js +1 -2
- package/package.json +1 -1
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/
|
|
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,
|
|
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,
|
|
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;
|
|
165
|
+
return true;
|
|
182
166
|
case "rsa-sha256":
|
|
183
167
|
case "ed25519":
|
|
184
|
-
return !!config.customConfig?.publicKey;
|
|
168
|
+
return !!config.customConfig?.publicKey;
|
|
185
169
|
case "custom":
|
|
186
|
-
return !!config.customConfig;
|
|
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
|
-
|
|
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