@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 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);
@@ -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;
@@ -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: 'Unable to verify webhook with provided platform secrets',
94
- errorCode: 'VERIFICATION_ERROR',
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: 'x-polar-signature',
103
+ headerName: 'webhook-signature',
101
104
  headerFormat: 'raw',
102
- timestampHeader: 'x-polar-timestamp',
105
+ timestampHeader: 'webhook-timestamp',
103
106
  timestampFormat: 'unix',
104
- payloadFormat: 'raw',
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
- const secretMaterial = secretEncoding === 'base64'
139
- ? new Uint8Array(Buffer.from(this.secret.includes('_') ? this.secret.split('_')[1] : this.secret, 'base64'))
140
- : this.secret;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "2.2.0",
3
+ "version": "2.2.3",
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",