@hookflo/tern 1.0.6 → 2.0.2-experimental.0
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 +204 -71
- package/dist/adapters/cloudflare.d.ts +11 -0
- package/dist/adapters/cloudflare.js +25 -0
- package/dist/adapters/express.d.ts +18 -0
- package/dist/adapters/express.js +23 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/nextjs.d.ts +10 -0
- package/dist/adapters/nextjs.js +20 -0
- package/dist/adapters/shared.d.ts +13 -0
- package/dist/adapters/shared.js +67 -0
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.js +5 -0
- package/dist/express.d.ts +2 -0
- package/dist/express.js +5 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +66 -14
- package/dist/nextjs.d.ts +2 -0
- package/dist/nextjs.js +5 -0
- package/dist/normalization/index.d.ts +20 -0
- package/dist/normalization/index.js +78 -0
- package/dist/normalization/providers/payment/paypal.d.ts +2 -0
- package/dist/normalization/providers/payment/paypal.js +12 -0
- package/dist/normalization/providers/payment/razorpay.d.ts +2 -0
- package/dist/normalization/providers/payment/razorpay.js +13 -0
- package/dist/normalization/providers/payment/stripe.d.ts +2 -0
- package/dist/normalization/providers/payment/stripe.js +13 -0
- package/dist/normalization/providers/registry.d.ts +5 -0
- package/dist/normalization/providers/registry.js +23 -0
- package/dist/normalization/simple.d.ts +4 -0
- package/dist/normalization/simple.js +138 -0
- package/dist/normalization/storage/interface.d.ts +13 -0
- package/dist/normalization/storage/interface.js +2 -0
- package/dist/normalization/storage/memory.d.ts +12 -0
- package/dist/normalization/storage/memory.js +39 -0
- package/dist/normalization/templates/base/auth.d.ts +2 -0
- package/dist/normalization/templates/base/auth.js +22 -0
- package/dist/normalization/templates/base/ecommerce.d.ts +2 -0
- package/dist/normalization/templates/base/ecommerce.js +25 -0
- package/dist/normalization/templates/base/payment.d.ts +2 -0
- package/dist/normalization/templates/base/payment.js +25 -0
- package/dist/normalization/templates/registry.d.ts +6 -0
- package/dist/normalization/templates/registry.js +22 -0
- package/dist/normalization/transformer/engine.d.ts +11 -0
- package/dist/normalization/transformer/engine.js +86 -0
- package/dist/normalization/transformer/validator.d.ts +12 -0
- package/dist/normalization/transformer/validator.js +56 -0
- package/dist/normalization/types.d.ts +79 -0
- package/dist/normalization/types.js +2 -0
- package/dist/platforms/algorithms.d.ts +1 -1
- package/dist/platforms/algorithms.js +103 -89
- package/dist/test.js +98 -2
- package/dist/types.d.ts +73 -3
- package/dist/types.js +1 -0
- package/dist/verifiers/algorithms.d.ts +2 -2
- package/dist/verifiers/algorithms.js +66 -62
- package/dist/verifiers/base.d.ts +1 -1
- package/dist/verifiers/custom-algorithms.d.ts +2 -2
- package/dist/verifiers/custom-algorithms.js +11 -8
- package/package.json +22 -2
package/dist/test.js
CHANGED
|
@@ -24,6 +24,10 @@ function createGitHubSignature(body, secret) {
|
|
|
24
24
|
hmac.update(body);
|
|
25
25
|
return `sha256=${hmac.digest('hex')}`;
|
|
26
26
|
}
|
|
27
|
+
function createGitLabSignature(body, secret) {
|
|
28
|
+
// GitLab just compares the token in X-Gitlab-Token header
|
|
29
|
+
return secret;
|
|
30
|
+
}
|
|
27
31
|
function createClerkSignature(body, secret, id, timestamp) {
|
|
28
32
|
const signedContent = `${id}.${timestamp}.${body}`;
|
|
29
33
|
const secretBytes = new Uint8Array(Buffer.from(secret.split('_')[1], 'base64'));
|
|
@@ -170,7 +174,9 @@ async function runTests() {
|
|
|
170
174
|
'content-type': 'application/json',
|
|
171
175
|
});
|
|
172
176
|
const invalidResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(invalidRequest, 'stripe', testSecret);
|
|
173
|
-
|
|
177
|
+
const invalidSigPassed = !invalidResult.isValid && (invalidResult.errorCode === 'INVALID_SIGNATURE'
|
|
178
|
+
|| invalidResult.errorCode === 'TIMESTAMP_EXPIRED');
|
|
179
|
+
console.log(' ✅ Invalid signature correctly rejected:', invalidSigPassed ? 'PASSED' : 'FAILED');
|
|
174
180
|
if (invalidResult.isValid) {
|
|
175
181
|
console.log(' ❌ Should have been rejected');
|
|
176
182
|
}
|
|
@@ -185,7 +191,8 @@ async function runTests() {
|
|
|
185
191
|
'content-type': 'application/json',
|
|
186
192
|
});
|
|
187
193
|
const missingHeaderResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(missingHeaderRequest, 'stripe', testSecret);
|
|
188
|
-
|
|
194
|
+
const missingHeaderPassed = !missingHeaderResult.isValid && missingHeaderResult.errorCode === 'MISSING_SIGNATURE';
|
|
195
|
+
console.log(' ✅ Missing headers correctly rejected:', missingHeaderPassed ? 'PASSED' : 'FAILED');
|
|
189
196
|
if (missingHeaderResult.isValid) {
|
|
190
197
|
console.log(' ❌ Should have been rejected');
|
|
191
198
|
}
|
|
@@ -193,6 +200,95 @@ async function runTests() {
|
|
|
193
200
|
catch (error) {
|
|
194
201
|
console.log(' ❌ Missing headers test failed:', error);
|
|
195
202
|
}
|
|
203
|
+
// Test 8: GitLab Webhook
|
|
204
|
+
console.log('\n8. Testing GitLab Webhook...');
|
|
205
|
+
try {
|
|
206
|
+
const gitlabSecret = testSecret;
|
|
207
|
+
const gitlabRequest = createMockRequest({
|
|
208
|
+
'X-Gitlab-Token': gitlabSecret,
|
|
209
|
+
'content-type': 'application/json',
|
|
210
|
+
});
|
|
211
|
+
const gitlabResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(gitlabRequest, 'gitlab', gitlabSecret);
|
|
212
|
+
console.log(' ✅ GitLab:', gitlabResult.isValid ? 'PASSED' : 'FAILED');
|
|
213
|
+
if (!gitlabResult.isValid) {
|
|
214
|
+
console.log(' ❌ Error:', gitlabResult.error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.log(' ❌ GitLab test failed:', error);
|
|
219
|
+
}
|
|
220
|
+
// Test 9: GitLab Invalid Token
|
|
221
|
+
console.log('\n9. Testing GitLab Invalid Token...');
|
|
222
|
+
try {
|
|
223
|
+
const gitlabRequest = createMockRequest({
|
|
224
|
+
'X-Gitlab-Token': 'wrong_secret',
|
|
225
|
+
'content-type': 'application/json',
|
|
226
|
+
});
|
|
227
|
+
const gitlabResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(gitlabRequest, 'gitlab', testSecret);
|
|
228
|
+
console.log(' ✅ Invalid token correctly rejected:', !gitlabResult.isValid ? 'PASSED' : 'FAILED');
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.log(' ❌ GitLab invalid token test failed:', error);
|
|
232
|
+
}
|
|
233
|
+
// Test 10: verifyAny should auto-detect Stripe
|
|
234
|
+
console.log('\n10. Testing verifyAny auto-detection...');
|
|
235
|
+
try {
|
|
236
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
237
|
+
const stripeSignature = createStripeSignature(testBody, testSecret, timestamp);
|
|
238
|
+
const request = createMockRequest({
|
|
239
|
+
'stripe-signature': stripeSignature,
|
|
240
|
+
'content-type': 'application/json',
|
|
241
|
+
});
|
|
242
|
+
const result = await index_1.WebhookVerificationService.verifyAny(request, {
|
|
243
|
+
stripe: testSecret,
|
|
244
|
+
github: 'wrong-secret',
|
|
245
|
+
});
|
|
246
|
+
console.log(' ✅ verifyAny:', result.isValid && result.platform === 'stripe' ? 'PASSED' : 'FAILED');
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.log(' ❌ verifyAny test failed:', error);
|
|
250
|
+
}
|
|
251
|
+
// Test 11: Normalization for Stripe
|
|
252
|
+
console.log('\n11. Testing payload normalization...');
|
|
253
|
+
try {
|
|
254
|
+
const normalizedStripeBody = JSON.stringify({
|
|
255
|
+
type: 'payment_intent.succeeded',
|
|
256
|
+
data: {
|
|
257
|
+
object: {
|
|
258
|
+
id: 'pi_123',
|
|
259
|
+
amount: 5000,
|
|
260
|
+
currency: 'usd',
|
|
261
|
+
customer: 'cus_456',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
266
|
+
const stripeSignature = createStripeSignature(normalizedStripeBody, testSecret, timestamp);
|
|
267
|
+
const request = createMockRequest({
|
|
268
|
+
'stripe-signature': stripeSignature,
|
|
269
|
+
'content-type': 'application/json',
|
|
270
|
+
}, normalizedStripeBody);
|
|
271
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'stripe', testSecret, 300, true);
|
|
272
|
+
const payload = result.payload;
|
|
273
|
+
const passed = result.isValid
|
|
274
|
+
&& payload.event === 'payment.succeeded'
|
|
275
|
+
&& payload.currency === 'USD'
|
|
276
|
+
&& payload.transaction_id === 'pi_123';
|
|
277
|
+
console.log(' ✅ Normalization:', passed ? 'PASSED' : 'FAILED');
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.log(' ❌ Normalization test failed:', error);
|
|
281
|
+
}
|
|
282
|
+
// Test 12: Category-aware normalization registry
|
|
283
|
+
console.log('\n12. Testing category-based platform registry...');
|
|
284
|
+
try {
|
|
285
|
+
const paymentPlatforms = (0, index_1.getPlatformsByCategory)('payment');
|
|
286
|
+
const hasStripeAndPolar = paymentPlatforms.includes('stripe') && paymentPlatforms.includes('polar');
|
|
287
|
+
console.log(' ✅ Category registry:', hasStripeAndPolar ? 'PASSED' : 'FAILED');
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
console.log(' ❌ Category registry test failed:', error);
|
|
291
|
+
}
|
|
196
292
|
console.log('\n🎉 All tests completed!');
|
|
197
293
|
}
|
|
198
294
|
// Run tests if this file is executed directly
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WebhookPlatform = 'custom' | 'clerk' | 'supabase' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'unknown';
|
|
1
|
+
export type WebhookPlatform = 'custom' | 'clerk' | 'supabase' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'unknown';
|
|
2
2
|
export declare enum WebhookPlatformKeys {
|
|
3
3
|
GitHub = "github",
|
|
4
4
|
Stripe = "stripe",
|
|
@@ -8,6 +8,7 @@ export declare enum WebhookPlatformKeys {
|
|
|
8
8
|
Vercel = "vercel",
|
|
9
9
|
Polar = "polar",
|
|
10
10
|
Supabase = "supabase",
|
|
11
|
+
GitLab = "gitlab",
|
|
11
12
|
Custom = "custom",
|
|
12
13
|
Unknown = "unknown"
|
|
13
14
|
}
|
|
@@ -22,11 +23,76 @@ export interface SignatureConfig {
|
|
|
22
23
|
payloadFormat?: 'raw' | 'timestamped' | 'custom';
|
|
23
24
|
customConfig?: Record<string, any>;
|
|
24
25
|
}
|
|
25
|
-
export
|
|
26
|
+
export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
|
|
27
|
+
export type NormalizationCategory = 'payment' | 'auth' | 'ecommerce' | 'infrastructure';
|
|
28
|
+
export interface BaseNormalizedWebhook {
|
|
29
|
+
category: NormalizationCategory;
|
|
30
|
+
event: string;
|
|
31
|
+
_platform: WebhookPlatform | string;
|
|
32
|
+
_raw: unknown;
|
|
33
|
+
occurred_at?: string;
|
|
34
|
+
}
|
|
35
|
+
export type PaymentWebhookEvent = 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'subscription.created' | 'subscription.cancelled' | 'payment.unknown';
|
|
36
|
+
export interface PaymentWebhookNormalized extends BaseNormalizedWebhook {
|
|
37
|
+
category: 'payment';
|
|
38
|
+
event: PaymentWebhookEvent;
|
|
39
|
+
amount?: number;
|
|
40
|
+
currency?: string;
|
|
41
|
+
customer_id?: string;
|
|
42
|
+
transaction_id?: string;
|
|
43
|
+
subscription_id?: string;
|
|
44
|
+
refund_amount?: number;
|
|
45
|
+
failure_reason?: string;
|
|
46
|
+
metadata?: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
export type AuthWebhookEvent = 'user.created' | 'user.updated' | 'user.deleted' | 'session.started' | 'session.ended' | 'auth.unknown';
|
|
49
|
+
export interface AuthWebhookNormalized extends BaseNormalizedWebhook {
|
|
50
|
+
category: 'auth';
|
|
51
|
+
event: AuthWebhookEvent;
|
|
52
|
+
user_id?: string;
|
|
53
|
+
email?: string;
|
|
54
|
+
phone?: string;
|
|
55
|
+
metadata?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
export interface EcommerceWebhookNormalized extends BaseNormalizedWebhook {
|
|
58
|
+
category: 'ecommerce';
|
|
59
|
+
event: string;
|
|
60
|
+
order_id?: string;
|
|
61
|
+
customer_id?: string;
|
|
62
|
+
amount?: number;
|
|
63
|
+
currency?: string;
|
|
64
|
+
metadata?: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
export interface InfrastructureWebhookNormalized extends BaseNormalizedWebhook {
|
|
67
|
+
category: 'infrastructure';
|
|
68
|
+
event: string;
|
|
69
|
+
project_id?: string;
|
|
70
|
+
deployment_id?: string;
|
|
71
|
+
status?: 'queued' | 'building' | 'ready' | 'error' | 'unknown';
|
|
72
|
+
metadata?: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
export interface UnknownNormalizedWebhook extends BaseNormalizedWebhook {
|
|
75
|
+
event: string;
|
|
76
|
+
warning?: string;
|
|
77
|
+
}
|
|
78
|
+
export type NormalizedPayloadByCategory = {
|
|
79
|
+
payment: PaymentWebhookNormalized;
|
|
80
|
+
auth: AuthWebhookNormalized;
|
|
81
|
+
ecommerce: EcommerceWebhookNormalized;
|
|
82
|
+
infrastructure: InfrastructureWebhookNormalized;
|
|
83
|
+
};
|
|
84
|
+
export type AnyNormalizedWebhook = PaymentWebhookNormalized | AuthWebhookNormalized | EcommerceWebhookNormalized | InfrastructureWebhookNormalized | UnknownNormalizedWebhook;
|
|
85
|
+
export interface NormalizeOptions {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
category?: NormalizationCategory;
|
|
88
|
+
includeRaw?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface WebhookVerificationResult<TPayload = unknown> {
|
|
26
91
|
isValid: boolean;
|
|
27
92
|
error?: string;
|
|
93
|
+
errorCode?: WebhookErrorCode;
|
|
28
94
|
platform: WebhookPlatform;
|
|
29
|
-
payload?:
|
|
95
|
+
payload?: TPayload;
|
|
30
96
|
metadata?: {
|
|
31
97
|
timestamp?: string;
|
|
32
98
|
id?: string | null;
|
|
@@ -38,6 +104,10 @@ export interface WebhookConfig {
|
|
|
38
104
|
secret: string;
|
|
39
105
|
toleranceInSeconds?: number;
|
|
40
106
|
signatureConfig?: SignatureConfig;
|
|
107
|
+
normalize?: boolean | NormalizeOptions;
|
|
108
|
+
}
|
|
109
|
+
export interface MultiPlatformSecrets {
|
|
110
|
+
[platform: string]: string | undefined;
|
|
41
111
|
}
|
|
42
112
|
export interface PlatformAlgorithmConfig {
|
|
43
113
|
platform: WebhookPlatform;
|
package/dist/types.js
CHANGED
|
@@ -11,6 +11,7 @@ var WebhookPlatformKeys;
|
|
|
11
11
|
WebhookPlatformKeys["Vercel"] = "vercel";
|
|
12
12
|
WebhookPlatformKeys["Polar"] = "polar";
|
|
13
13
|
WebhookPlatformKeys["Supabase"] = "supabase";
|
|
14
|
+
WebhookPlatformKeys["GitLab"] = "gitlab";
|
|
14
15
|
WebhookPlatformKeys["Custom"] = "custom";
|
|
15
16
|
WebhookPlatformKeys["Unknown"] = "unknown";
|
|
16
17
|
})(WebhookPlatformKeys || (exports.WebhookPlatformKeys = WebhookPlatformKeys = {}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WebhookVerifier } from
|
|
2
|
-
import { WebhookVerificationResult, SignatureConfig, WebhookPlatform } from
|
|
1
|
+
import { WebhookVerifier } from './base';
|
|
2
|
+
import { WebhookVerificationResult, SignatureConfig, WebhookPlatform } from '../types';
|
|
3
3
|
export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
|
|
4
4
|
protected config: SignatureConfig;
|
|
5
5
|
protected platform: WebhookPlatform;
|
|
@@ -15,27 +15,27 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
15
15
|
if (!headerValue)
|
|
16
16
|
return null;
|
|
17
17
|
switch (this.config.headerFormat) {
|
|
18
|
-
case
|
|
18
|
+
case 'prefixed':
|
|
19
19
|
// For GitHub, return the full signature including prefix for comparison
|
|
20
20
|
return headerValue;
|
|
21
|
-
case
|
|
21
|
+
case 'comma-separated':
|
|
22
22
|
// Handle comma-separated format like Stripe: "t=1234567890,v1=abc123"
|
|
23
|
-
const parts = headerValue.split(
|
|
23
|
+
const parts = headerValue.split(',');
|
|
24
24
|
const sigMap = {};
|
|
25
25
|
for (const part of parts) {
|
|
26
|
-
const [key, value] = part.split(
|
|
26
|
+
const [key, value] = part.split('=');
|
|
27
27
|
if (key && value) {
|
|
28
28
|
sigMap[key] = value;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
return sigMap.v1 || sigMap.signature || null;
|
|
32
|
-
case
|
|
32
|
+
case 'raw':
|
|
33
33
|
default:
|
|
34
|
-
if (this.platform ===
|
|
35
|
-
const signatures = headerValue.split(
|
|
34
|
+
if (this.platform === 'clerk' || this.platform === 'dodopayments') {
|
|
35
|
+
const signatures = headerValue.split(' ');
|
|
36
36
|
for (const sig of signatures) {
|
|
37
|
-
const [version, signature] = sig.split(
|
|
38
|
-
if (version ===
|
|
37
|
+
const [version, signature] = sig.split(',');
|
|
38
|
+
if (version === 'v1') {
|
|
39
39
|
return signature;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -51,11 +51,11 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
51
51
|
if (!timestampHeader)
|
|
52
52
|
return null;
|
|
53
53
|
switch (this.config.timestampFormat) {
|
|
54
|
-
case
|
|
54
|
+
case 'unix':
|
|
55
55
|
return parseInt(timestampHeader, 10);
|
|
56
|
-
case
|
|
56
|
+
case 'iso':
|
|
57
57
|
return Math.floor(new Date(timestampHeader).getTime() / 1000);
|
|
58
|
-
case
|
|
58
|
+
case 'custom':
|
|
59
59
|
// Custom timestamp parsing logic can be added here
|
|
60
60
|
return parseInt(timestampHeader, 10);
|
|
61
61
|
default:
|
|
@@ -64,14 +64,14 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
64
64
|
}
|
|
65
65
|
extractTimestampFromSignature(request) {
|
|
66
66
|
// For platforms like Stripe where timestamp is embedded in signature
|
|
67
|
-
if (this.config.headerFormat ===
|
|
67
|
+
if (this.config.headerFormat === 'comma-separated') {
|
|
68
68
|
const headerValue = request.headers.get(this.config.headerName);
|
|
69
69
|
if (!headerValue)
|
|
70
70
|
return null;
|
|
71
|
-
const parts = headerValue.split(
|
|
71
|
+
const parts = headerValue.split(',');
|
|
72
72
|
const sigMap = {};
|
|
73
73
|
for (const part of parts) {
|
|
74
|
-
const [key, value] = part.split(
|
|
74
|
+
const [key, value] = part.split('=');
|
|
75
75
|
if (key && value) {
|
|
76
76
|
sigMap[key] = value;
|
|
77
77
|
}
|
|
@@ -82,14 +82,14 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
82
82
|
}
|
|
83
83
|
formatPayload(rawBody, request) {
|
|
84
84
|
switch (this.config.payloadFormat) {
|
|
85
|
-
case
|
|
85
|
+
case 'timestamped':
|
|
86
86
|
// For Stripe, timestamp is embedded in signature
|
|
87
|
-
const timestamp = this.extractTimestampFromSignature(request)
|
|
88
|
-
this.extractTimestamp(request);
|
|
87
|
+
const timestamp = this.extractTimestampFromSignature(request)
|
|
88
|
+
|| this.extractTimestamp(request);
|
|
89
89
|
return timestamp ? `${timestamp}.${rawBody}` : rawBody;
|
|
90
|
-
case
|
|
90
|
+
case 'custom':
|
|
91
91
|
return this.formatCustomPayload(rawBody, request);
|
|
92
|
-
case
|
|
92
|
+
case 'raw':
|
|
93
93
|
default:
|
|
94
94
|
return rawBody;
|
|
95
95
|
}
|
|
@@ -100,42 +100,42 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
100
100
|
}
|
|
101
101
|
const customFormat = this.config.customConfig.payloadFormat;
|
|
102
102
|
// Handle Clerk-style format: {id}.{timestamp}.{body}
|
|
103
|
-
if (customFormat.includes(
|
|
104
|
-
const id = request.headers.get(this.config.customConfig.idHeader ||
|
|
105
|
-
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
103
|
+
if (customFormat.includes('{id}') && customFormat.includes('{timestamp}')) {
|
|
104
|
+
const id = request.headers.get(this.config.customConfig.idHeader || 'x-webhook-id');
|
|
105
|
+
const timestamp = request.headers.get(this.config.timestampHeader || 'x-webhook-timestamp');
|
|
106
106
|
return customFormat
|
|
107
|
-
.replace(
|
|
108
|
-
.replace(
|
|
109
|
-
.replace(
|
|
107
|
+
.replace('{id}', id || '')
|
|
108
|
+
.replace('{timestamp}', timestamp || '')
|
|
109
|
+
.replace('{body}', rawBody);
|
|
110
110
|
}
|
|
111
111
|
// Handle Stripe-style format: {timestamp}.{body}
|
|
112
|
-
if (customFormat.includes(
|
|
113
|
-
customFormat.includes(
|
|
112
|
+
if (customFormat.includes('{timestamp}')
|
|
113
|
+
&& customFormat.includes('{body}')) {
|
|
114
114
|
const timestamp = this.extractTimestamp(request);
|
|
115
115
|
return customFormat
|
|
116
|
-
.replace(
|
|
117
|
-
.replace(
|
|
116
|
+
.replace('{timestamp}', timestamp?.toString() || '')
|
|
117
|
+
.replace('{body}', rawBody);
|
|
118
118
|
}
|
|
119
119
|
return rawBody;
|
|
120
120
|
}
|
|
121
|
-
verifyHMAC(payload, signature, algorithm =
|
|
121
|
+
verifyHMAC(payload, signature, algorithm = 'sha256') {
|
|
122
122
|
const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
|
|
123
123
|
hmac.update(payload);
|
|
124
|
-
const expectedSignature = hmac.digest(
|
|
124
|
+
const expectedSignature = hmac.digest('hex');
|
|
125
125
|
return this.safeCompare(signature, expectedSignature);
|
|
126
126
|
}
|
|
127
|
-
verifyHMACWithPrefix(payload, signature, algorithm =
|
|
127
|
+
verifyHMACWithPrefix(payload, signature, algorithm = 'sha256') {
|
|
128
128
|
const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
|
|
129
129
|
hmac.update(payload);
|
|
130
|
-
const expectedSignature = `${this.config.prefix ||
|
|
130
|
+
const expectedSignature = `${this.config.prefix || ''}${hmac.digest('hex')}`;
|
|
131
131
|
return this.safeCompare(signature, expectedSignature);
|
|
132
132
|
}
|
|
133
|
-
verifyHMACWithBase64(payload, signature, algorithm =
|
|
133
|
+
verifyHMACWithBase64(payload, signature, algorithm = 'sha256') {
|
|
134
134
|
// For platforms like Clerk that use base64 encoding
|
|
135
|
-
const secretBytes = new Uint8Array(Buffer.from(this.secret.split(
|
|
135
|
+
const secretBytes = new Uint8Array(Buffer.from(this.secret.split('_')[1], 'base64'));
|
|
136
136
|
const hmac = (0, crypto_1.createHmac)(algorithm, secretBytes);
|
|
137
137
|
hmac.update(payload);
|
|
138
|
-
const expectedSignature = hmac.digest(
|
|
138
|
+
const expectedSignature = hmac.digest('base64');
|
|
139
139
|
return this.safeCompare(signature, expectedSignature);
|
|
140
140
|
}
|
|
141
141
|
extractMetadata(request) {
|
|
@@ -149,18 +149,18 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
149
149
|
}
|
|
150
150
|
// Add platform-specific metadata
|
|
151
151
|
switch (this.platform) {
|
|
152
|
-
case
|
|
153
|
-
metadata.event = request.headers.get(
|
|
154
|
-
metadata.delivery = request.headers.get(
|
|
152
|
+
case 'github':
|
|
153
|
+
metadata.event = request.headers.get('x-github-event');
|
|
154
|
+
metadata.delivery = request.headers.get('x-github-delivery');
|
|
155
155
|
break;
|
|
156
|
-
case
|
|
156
|
+
case 'stripe':
|
|
157
157
|
// Extract Stripe-specific metadata from signature
|
|
158
158
|
const headerValue = request.headers.get(this.config.headerName);
|
|
159
|
-
if (headerValue && this.config.headerFormat ===
|
|
160
|
-
const parts = headerValue.split(
|
|
159
|
+
if (headerValue && this.config.headerFormat === 'comma-separated') {
|
|
160
|
+
const parts = headerValue.split(',');
|
|
161
161
|
const sigMap = {};
|
|
162
162
|
for (const part of parts) {
|
|
163
|
-
const [key, value] = part.split(
|
|
163
|
+
const [key, value] = part.split('=');
|
|
164
164
|
if (key && value) {
|
|
165
165
|
sigMap[key] = value;
|
|
166
166
|
}
|
|
@@ -168,8 +168,8 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
168
168
|
metadata.id = sigMap.id;
|
|
169
169
|
}
|
|
170
170
|
break;
|
|
171
|
-
case
|
|
172
|
-
metadata.id = request.headers.get(
|
|
171
|
+
case 'clerk':
|
|
172
|
+
metadata.id = request.headers.get('svix-id');
|
|
173
173
|
break;
|
|
174
174
|
}
|
|
175
175
|
return metadata;
|
|
@@ -185,13 +185,14 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
185
185
|
return {
|
|
186
186
|
isValid: false,
|
|
187
187
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
188
|
+
errorCode: 'MISSING_SIGNATURE',
|
|
188
189
|
platform: this.platform,
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
192
|
const rawBody = await request.text();
|
|
192
193
|
// Extract timestamp based on platform configuration
|
|
193
194
|
let timestamp = null;
|
|
194
|
-
if (this.config.headerFormat ===
|
|
195
|
+
if (this.config.headerFormat === 'comma-separated') {
|
|
195
196
|
// For platforms like Stripe where timestamp is embedded in signature
|
|
196
197
|
timestamp = this.extractTimestampFromSignature(request);
|
|
197
198
|
}
|
|
@@ -203,7 +204,8 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
203
204
|
if (timestamp && !this.isTimestampValid(timestamp)) {
|
|
204
205
|
return {
|
|
205
206
|
isValid: false,
|
|
206
|
-
error:
|
|
207
|
+
error: 'Webhook timestamp expired',
|
|
208
|
+
errorCode: 'TIMESTAMP_EXPIRED',
|
|
207
209
|
platform: this.platform,
|
|
208
210
|
};
|
|
209
211
|
}
|
|
@@ -211,12 +213,12 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
211
213
|
const payload = this.formatPayload(rawBody, request);
|
|
212
214
|
// Verify signature based on platform configuration
|
|
213
215
|
let isValid = false;
|
|
214
|
-
const algorithm = this.config.algorithm.replace(
|
|
215
|
-
if (this.config.customConfig?.encoding ===
|
|
216
|
+
const algorithm = this.config.algorithm.replace('hmac-', '');
|
|
217
|
+
if (this.config.customConfig?.encoding === 'base64') {
|
|
216
218
|
// For platforms like Clerk that use base64 encoding
|
|
217
219
|
isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
|
|
218
220
|
}
|
|
219
|
-
else if (this.config.headerFormat ===
|
|
221
|
+
else if (this.config.headerFormat === 'prefixed') {
|
|
220
222
|
// For platforms like GitHub that use prefixed signatures
|
|
221
223
|
isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
|
|
222
224
|
}
|
|
@@ -227,7 +229,8 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
227
229
|
if (!isValid) {
|
|
228
230
|
return {
|
|
229
231
|
isValid: false,
|
|
230
|
-
error:
|
|
232
|
+
error: 'Invalid signature',
|
|
233
|
+
errorCode: 'INVALID_SIGNATURE',
|
|
231
234
|
platform: this.platform,
|
|
232
235
|
};
|
|
233
236
|
}
|
|
@@ -252,6 +255,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
252
255
|
return {
|
|
253
256
|
isValid: false,
|
|
254
257
|
error: `${this.platform} verification error: ${error.message}`,
|
|
258
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
255
259
|
platform: this.platform,
|
|
256
260
|
};
|
|
257
261
|
}
|
|
@@ -260,33 +264,33 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
260
264
|
exports.GenericHMACVerifier = GenericHMACVerifier;
|
|
261
265
|
// Legacy verifiers for backward compatibility
|
|
262
266
|
class HMACSHA256Verifier extends GenericHMACVerifier {
|
|
263
|
-
constructor(secret, config, platform =
|
|
267
|
+
constructor(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
|
|
264
268
|
super(secret, config, platform, toleranceInSeconds);
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
exports.HMACSHA256Verifier = HMACSHA256Verifier;
|
|
268
272
|
class HMACSHA1Verifier extends GenericHMACVerifier {
|
|
269
|
-
constructor(secret, config, platform =
|
|
273
|
+
constructor(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
|
|
270
274
|
super(secret, config, platform, toleranceInSeconds);
|
|
271
275
|
}
|
|
272
276
|
}
|
|
273
277
|
exports.HMACSHA1Verifier = HMACSHA1Verifier;
|
|
274
278
|
class HMACSHA512Verifier extends GenericHMACVerifier {
|
|
275
|
-
constructor(secret, config, platform =
|
|
279
|
+
constructor(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
|
|
276
280
|
super(secret, config, platform, toleranceInSeconds);
|
|
277
281
|
}
|
|
278
282
|
}
|
|
279
283
|
exports.HMACSHA512Verifier = HMACSHA512Verifier;
|
|
280
284
|
// Factory function to create verifiers based on algorithm
|
|
281
|
-
function createAlgorithmVerifier(secret, config, platform =
|
|
285
|
+
function createAlgorithmVerifier(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
|
|
282
286
|
switch (config.algorithm) {
|
|
283
|
-
case
|
|
284
|
-
case
|
|
285
|
-
case
|
|
287
|
+
case 'hmac-sha256':
|
|
288
|
+
case 'hmac-sha1':
|
|
289
|
+
case 'hmac-sha512':
|
|
286
290
|
return new GenericHMACVerifier(secret, config, platform, toleranceInSeconds);
|
|
287
|
-
case
|
|
288
|
-
case
|
|
289
|
-
case
|
|
291
|
+
case 'rsa-sha256':
|
|
292
|
+
case 'ed25519':
|
|
293
|
+
case 'custom':
|
|
290
294
|
// These can be implemented as needed
|
|
291
295
|
throw new Error(`Algorithm ${config.algorithm} not yet implemented`);
|
|
292
296
|
default:
|
package/dist/verifiers/base.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WebhookVerifier } from
|
|
2
|
-
import { WebhookVerificationResult, SignatureConfig } from
|
|
1
|
+
import { WebhookVerifier } from './base';
|
|
2
|
+
import { WebhookVerificationResult, SignatureConfig } from '../types';
|
|
3
3
|
export declare class TokenBasedVerifier extends WebhookVerifier {
|
|
4
4
|
private config;
|
|
5
5
|
constructor(secret: string, config: SignatureConfig, toleranceInSeconds?: number);
|
|
@@ -12,12 +12,13 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
12
12
|
async verify(request) {
|
|
13
13
|
try {
|
|
14
14
|
const token = request.headers.get(this.config.headerName);
|
|
15
|
-
const id = request.headers.get(this.config.customConfig?.idHeader ||
|
|
15
|
+
const id = request.headers.get(this.config.customConfig?.idHeader || 'x-webhook-id');
|
|
16
16
|
if (!token) {
|
|
17
17
|
return {
|
|
18
18
|
isValid: false,
|
|
19
19
|
error: `Missing token header: ${this.config.headerName}`,
|
|
20
|
-
|
|
20
|
+
errorCode: 'MISSING_TOKEN',
|
|
21
|
+
platform: 'custom',
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
// Simple token comparison
|
|
@@ -25,8 +26,9 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
25
26
|
if (!isValid) {
|
|
26
27
|
return {
|
|
27
28
|
isValid: false,
|
|
28
|
-
error:
|
|
29
|
-
|
|
29
|
+
error: 'Invalid token',
|
|
30
|
+
errorCode: 'INVALID_TOKEN',
|
|
31
|
+
platform: 'custom',
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
const rawBody = await request.text();
|
|
@@ -39,11 +41,11 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
39
41
|
}
|
|
40
42
|
return {
|
|
41
43
|
isValid: true,
|
|
42
|
-
platform:
|
|
44
|
+
platform: 'custom',
|
|
43
45
|
payload,
|
|
44
46
|
metadata: {
|
|
45
47
|
id,
|
|
46
|
-
algorithm:
|
|
48
|
+
algorithm: 'token-based',
|
|
47
49
|
},
|
|
48
50
|
};
|
|
49
51
|
}
|
|
@@ -51,7 +53,8 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
51
53
|
return {
|
|
52
54
|
isValid: false,
|
|
53
55
|
error: `Token-based verification error: ${error.message}`,
|
|
54
|
-
|
|
56
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
57
|
+
platform: 'custom',
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
}
|
|
@@ -61,7 +64,7 @@ exports.TokenBasedVerifier = TokenBasedVerifier;
|
|
|
61
64
|
function createCustomVerifier(secret, config, toleranceInSeconds = 300) {
|
|
62
65
|
const customType = config.customConfig?.type;
|
|
63
66
|
switch (customType) {
|
|
64
|
-
case
|
|
67
|
+
case 'token-based':
|
|
65
68
|
return new TokenBasedVerifier(secret, config, toleranceInSeconds);
|
|
66
69
|
default:
|
|
67
70
|
// Fallback to token-based for unknown custom types
|