@hookflo/tern 2.2.0 → 2.2.3
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 +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/express.js +1 -1
- package/dist/adapters/nextjs.js +1 -1
- package/dist/index.js +18 -2
- package/dist/platforms/algorithms.js +16 -6
- package/dist/test.js +62 -0
- package/dist/utils.js +6 -0
- package/dist/verifiers/algorithms.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -218,7 +218,7 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
|
|
|
218
218
|
- **WooCommerce**: HMAC-SHA256 (base64 signature)
|
|
219
219
|
- **ReplicateAI**: HMAC-SHA256 (Standard Webhooks style)
|
|
220
220
|
- **fal.ai**: ED25519 (`x-fal-webhook-signature`)
|
|
221
|
-
- **Shopify**: HMAC-SHA256
|
|
221
|
+
- **Shopify**: HMAC-SHA256 (base64 signature)
|
|
222
222
|
- **Vercel**: HMAC-SHA256
|
|
223
223
|
- **Polar**: HMAC-SHA256
|
|
224
224
|
- **Supabase**: Token-based authentication
|
|
@@ -12,7 +12,7 @@ function createWebhookHandler(options) {
|
|
|
12
12
|
}
|
|
13
13
|
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, secret, options.toleranceInSeconds, options.normalize);
|
|
14
14
|
if (!result.isValid) {
|
|
15
|
-
return Response.json({ error: result.error, platform: result.platform }, { status: 400 });
|
|
15
|
+
return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
|
|
16
16
|
}
|
|
17
17
|
const data = await options.handler(result.payload, env, result.metadata || {});
|
|
18
18
|
return Response.json(data);
|
package/dist/adapters/express.js
CHANGED
|
@@ -9,7 +9,7 @@ function createWebhookMiddleware(options) {
|
|
|
9
9
|
const webRequest = await (0, shared_1.toWebRequest)(req);
|
|
10
10
|
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds, options.normalize);
|
|
11
11
|
if (!result.isValid) {
|
|
12
|
-
res.status(400).json({ error: result.error, platform: result.platform });
|
|
12
|
+
res.status(400).json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata });
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
req.webhook = result;
|
package/dist/adapters/nextjs.js
CHANGED
|
@@ -7,7 +7,7 @@ function createWebhookHandler(options) {
|
|
|
7
7
|
try {
|
|
8
8
|
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds, options.normalize);
|
|
9
9
|
if (!result.isValid) {
|
|
10
|
-
return Response.json({ error: result.error, platform: result.platform }, { status: 400 });
|
|
10
|
+
return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
|
|
11
11
|
}
|
|
12
12
|
const data = await options.handler(result.payload, result.metadata || {});
|
|
13
13
|
return Response.json(data);
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ class WebhookVerificationService {
|
|
|
79
79
|
if (detectedPlatform !== 'unknown' && secrets[detectedPlatform]) {
|
|
80
80
|
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds, normalize);
|
|
81
81
|
}
|
|
82
|
+
const failedAttempts = [];
|
|
82
83
|
for (const [platform, secret] of Object.entries(secrets)) {
|
|
83
84
|
if (!secret) {
|
|
84
85
|
continue;
|
|
@@ -87,12 +88,25 @@ class WebhookVerificationService {
|
|
|
87
88
|
if (result.isValid) {
|
|
88
89
|
return result;
|
|
89
90
|
}
|
|
91
|
+
failedAttempts.push({
|
|
92
|
+
platform: platform.toLowerCase(),
|
|
93
|
+
error: result.error,
|
|
94
|
+
errorCode: result.errorCode,
|
|
95
|
+
});
|
|
90
96
|
}
|
|
97
|
+
const details = failedAttempts
|
|
98
|
+
.map((attempt) => `${attempt.platform}: ${attempt.error || 'verification failed'}`)
|
|
99
|
+
.join('; ');
|
|
91
100
|
return {
|
|
92
101
|
isValid: false,
|
|
93
|
-
error:
|
|
94
|
-
|
|
102
|
+
error: details
|
|
103
|
+
? `Unable to verify webhook with provided platform secrets. Attempts -> ${details}`
|
|
104
|
+
: 'Unable to verify webhook with provided platform secrets',
|
|
105
|
+
errorCode: failedAttempts.find((attempt) => attempt.errorCode)?.errorCode || 'VERIFICATION_ERROR',
|
|
95
106
|
platform: detectedPlatform,
|
|
107
|
+
metadata: {
|
|
108
|
+
attempts: failedAttempts,
|
|
109
|
+
},
|
|
96
110
|
};
|
|
97
111
|
}
|
|
98
112
|
static detectPlatform(request) {
|
|
@@ -107,6 +121,8 @@ class WebhookVerificationService {
|
|
|
107
121
|
return 'workos';
|
|
108
122
|
if (headers.has('webhook-signature')) {
|
|
109
123
|
const userAgent = headers.get('user-agent')?.toLowerCase() || '';
|
|
124
|
+
if (userAgent.includes('polar'))
|
|
125
|
+
return 'polar';
|
|
110
126
|
if (userAgent.includes('replicate'))
|
|
111
127
|
return 'replicateai';
|
|
112
128
|
return 'dodopayments';
|
|
@@ -76,10 +76,13 @@ exports.platformAlgorithmConfigs = {
|
|
|
76
76
|
algorithm: 'hmac-sha256',
|
|
77
77
|
headerName: 'x-shopify-hmac-sha256',
|
|
78
78
|
headerFormat: 'raw',
|
|
79
|
-
timestampHeader: 'x-shopify-shop-domain',
|
|
80
79
|
payloadFormat: 'raw',
|
|
80
|
+
customConfig: {
|
|
81
|
+
encoding: 'base64',
|
|
82
|
+
secretEncoding: 'utf8',
|
|
83
|
+
},
|
|
81
84
|
},
|
|
82
|
-
description: 'Shopify webhooks use HMAC-SHA256',
|
|
85
|
+
description: 'Shopify webhooks use HMAC-SHA256 with base64 encoded signature',
|
|
83
86
|
},
|
|
84
87
|
vercel: {
|
|
85
88
|
platform: 'vercel',
|
|
@@ -97,13 +100,20 @@ exports.platformAlgorithmConfigs = {
|
|
|
97
100
|
platform: 'polar',
|
|
98
101
|
signatureConfig: {
|
|
99
102
|
algorithm: 'hmac-sha256',
|
|
100
|
-
headerName: '
|
|
103
|
+
headerName: 'webhook-signature',
|
|
101
104
|
headerFormat: 'raw',
|
|
102
|
-
timestampHeader: '
|
|
105
|
+
timestampHeader: 'webhook-timestamp',
|
|
103
106
|
timestampFormat: 'unix',
|
|
104
|
-
payloadFormat: '
|
|
107
|
+
payloadFormat: 'custom',
|
|
108
|
+
customConfig: {
|
|
109
|
+
signatureFormat: 'v1={signature}',
|
|
110
|
+
payloadFormat: '{id}.{timestamp}.{body}',
|
|
111
|
+
encoding: 'base64',
|
|
112
|
+
secretEncoding: 'base64',
|
|
113
|
+
idHeader: 'webhook-id',
|
|
114
|
+
},
|
|
105
115
|
},
|
|
106
|
-
description: 'Polar webhooks use HMAC-SHA256',
|
|
116
|
+
description: 'Polar webhooks use HMAC-SHA256 with Standard Webhooks format',
|
|
107
117
|
},
|
|
108
118
|
supabase: {
|
|
109
119
|
platform: 'supabase',
|
package/dist/test.js
CHANGED
|
@@ -49,6 +49,11 @@ function createPaddleSignature(body, secret, timestamp) {
|
|
|
49
49
|
hmac.update(signedPayload);
|
|
50
50
|
return `ts=${timestamp};h1=${hmac.digest('hex')}`;
|
|
51
51
|
}
|
|
52
|
+
function createShopifySignature(body, secret) {
|
|
53
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
54
|
+
hmac.update(body);
|
|
55
|
+
return hmac.digest('base64');
|
|
56
|
+
}
|
|
52
57
|
function createWooCommerceSignature(body, secret) {
|
|
53
58
|
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
54
59
|
hmac.update(body);
|
|
@@ -277,6 +282,24 @@ async function runTests() {
|
|
|
277
282
|
catch (error) {
|
|
278
283
|
console.log(' ❌ verifyAny test failed:', error);
|
|
279
284
|
}
|
|
285
|
+
// Test 10.5: verifyAny error diagnostics
|
|
286
|
+
console.log('\n10.5. Testing verifyAny error diagnostics...');
|
|
287
|
+
try {
|
|
288
|
+
const unknownRequest = createMockRequest({
|
|
289
|
+
'content-type': 'application/json',
|
|
290
|
+
});
|
|
291
|
+
const invalidVerifyAny = await index_1.WebhookVerificationService.verifyAny(unknownRequest, {
|
|
292
|
+
stripe: testSecret,
|
|
293
|
+
shopify: testSecret,
|
|
294
|
+
});
|
|
295
|
+
const hasDetailedErrors = Boolean(invalidVerifyAny.error
|
|
296
|
+
&& invalidVerifyAny.error.includes('Attempts ->')
|
|
297
|
+
&& invalidVerifyAny.metadata?.attempts?.length === 2);
|
|
298
|
+
console.log(' ✅ verifyAny diagnostics:', hasDetailedErrors ? 'PASSED' : 'FAILED');
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
console.log(' ❌ verifyAny diagnostics test failed:', error);
|
|
302
|
+
}
|
|
280
303
|
// Test 11: Normalization for Stripe
|
|
281
304
|
console.log('\n11. Testing payload normalization...');
|
|
282
305
|
try {
|
|
@@ -396,6 +419,23 @@ async function runTests() {
|
|
|
396
419
|
catch (error) {
|
|
397
420
|
console.log(' ❌ WorkOS test failed:', error);
|
|
398
421
|
}
|
|
422
|
+
// Test 17.5: Shopify
|
|
423
|
+
console.log('\n17.5. Testing Shopify webhook...');
|
|
424
|
+
try {
|
|
425
|
+
const signature = createShopifySignature(testBody, testSecret);
|
|
426
|
+
const request = createMockRequest({
|
|
427
|
+
'x-shopify-hmac-sha256': signature,
|
|
428
|
+
'content-type': 'application/json',
|
|
429
|
+
});
|
|
430
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'shopify', testSecret);
|
|
431
|
+
console.log(' ✅ Shopify:', result.isValid ? 'PASSED' : 'FAILED');
|
|
432
|
+
if (!result.isValid) {
|
|
433
|
+
console.log(' ❌ Error:', result.error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.log(' ❌ Shopify test failed:', error);
|
|
438
|
+
}
|
|
399
439
|
// Test 18: WooCommerce
|
|
400
440
|
console.log('\n18. Testing WooCommerce webhook...');
|
|
401
441
|
try {
|
|
@@ -410,6 +450,28 @@ async function runTests() {
|
|
|
410
450
|
catch (error) {
|
|
411
451
|
console.log(' ❌ WooCommerce test failed:', error);
|
|
412
452
|
}
|
|
453
|
+
// Test 18.5: Polar (Standard Webhooks)
|
|
454
|
+
console.log('\n18.5. Testing Polar webhook...');
|
|
455
|
+
try {
|
|
456
|
+
const secret = `whsec_${Buffer.from(testSecret).toString('base64')}`;
|
|
457
|
+
const webhookId = 'polar-webhook-id-1';
|
|
458
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
459
|
+
const signature = createStandardWebhooksSignature(testBody, secret, webhookId, timestamp);
|
|
460
|
+
const request = createMockRequest({
|
|
461
|
+
'webhook-signature': signature,
|
|
462
|
+
'webhook-id': webhookId,
|
|
463
|
+
'webhook-timestamp': timestamp.toString(),
|
|
464
|
+
'user-agent': 'Polar.sh Webhooks',
|
|
465
|
+
'content-type': 'application/json',
|
|
466
|
+
});
|
|
467
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'polar', secret);
|
|
468
|
+
const detectedPlatform = index_1.WebhookVerificationService.detectPlatform(request);
|
|
469
|
+
console.log(' ✅ Polar verification:', result.isValid ? 'PASSED' : 'FAILED');
|
|
470
|
+
console.log(' ✅ Polar auto-detect:', detectedPlatform === 'polar' ? 'PASSED' : 'FAILED');
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
console.log(' ❌ Polar test failed:', error);
|
|
474
|
+
}
|
|
413
475
|
// Test 19: Replicate
|
|
414
476
|
console.log('\n19. Testing Replicate webhook...');
|
|
415
477
|
try {
|
package/dist/utils.js
CHANGED
|
@@ -152,6 +152,12 @@ function detectPlatformFromHeaders(headers) {
|
|
|
152
152
|
if (headerMap.has('x-polar-signature')) {
|
|
153
153
|
return 'polar';
|
|
154
154
|
}
|
|
155
|
+
if (headerMap.has('webhook-signature')) {
|
|
156
|
+
const userAgent = (headerMap.get('user-agent') || '').toLowerCase();
|
|
157
|
+
if (userAgent.includes('polar')) {
|
|
158
|
+
return 'polar';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
155
161
|
// Supabase
|
|
156
162
|
if (headerMap.has('x-webhook-token')) {
|
|
157
163
|
return 'supabase';
|
|
@@ -135,9 +135,13 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
135
135
|
}
|
|
136
136
|
verifyHMACWithBase64(payload, signature, algorithm = 'sha256') {
|
|
137
137
|
const secretEncoding = this.config.customConfig?.secretEncoding || 'base64';
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
let secretMaterial = this.secret;
|
|
139
|
+
if (secretEncoding === 'base64') {
|
|
140
|
+
const base64Secret = this.secret.includes('_')
|
|
141
|
+
? this.secret.split('_').slice(1).join('_')
|
|
142
|
+
: this.secret;
|
|
143
|
+
secretMaterial = new Uint8Array(Buffer.from(base64Secret, 'base64'));
|
|
144
|
+
}
|
|
141
145
|
const hmac = (0, crypto_1.createHmac)(algorithm, secretMaterial);
|
|
142
146
|
hmac.update(payload);
|
|
143
147
|
const expectedSignature = hmac.digest('base64');
|
package/package.json
CHANGED