@hookflo/tern 2.2.10 โ 3.0.1
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 +34 -37
- package/dist/adapters/cloudflare.d.ts +3 -3
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/express.d.ts +2 -2
- package/dist/adapters/nextjs.d.ts +3 -3
- package/dist/adapters/nextjs.js +1 -1
- package/dist/adapters/shared.js +20 -20
- package/dist/adapters/testExpress.js +24 -25
- package/dist/examples.js +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +26 -6
- package/dist/normalization/providers/registry.js +0 -2
- package/dist/normalization/simple.js +0 -12
- package/dist/platforms/algorithms.d.ts +1 -1
- package/dist/platforms/algorithms.js +217 -184
- package/dist/test.js +124 -29
- package/dist/types.d.ts +8 -4
- package/dist/types.js +4 -2
- package/dist/utils.js +0 -7
- package/dist/verifiers/algorithms.d.ts +1 -0
- package/dist/verifiers/algorithms.js +77 -14
- package/dist/verifiers/base.d.ts +1 -1
- package/dist/verifiers/custom-algorithms.js +1 -1
- package/package.json +1 -1
package/dist/test.js
CHANGED
|
@@ -65,9 +65,35 @@ function createWorkOSSignature(body, secret, timestamp) {
|
|
|
65
65
|
hmac.update(signedPayload);
|
|
66
66
|
return `t=${timestamp},v1=${hmac.digest('hex')}`;
|
|
67
67
|
}
|
|
68
|
+
function createSentrySignature(body, secret) {
|
|
69
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
70
|
+
hmac.update(JSON.stringify(JSON.parse(body)));
|
|
71
|
+
return hmac.digest('hex');
|
|
72
|
+
}
|
|
73
|
+
function createSentryIssueAlertSignature(issueAlertObject, secret) {
|
|
74
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
75
|
+
hmac.update(JSON.stringify(issueAlertObject));
|
|
76
|
+
return hmac.digest('hex');
|
|
77
|
+
}
|
|
78
|
+
function createGrafanaSignature(body, secret, timestamp) {
|
|
79
|
+
const payload = timestamp ? `${timestamp}.${body}` : body;
|
|
80
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
81
|
+
hmac.update(payload);
|
|
82
|
+
return hmac.digest('hex');
|
|
83
|
+
}
|
|
84
|
+
function createDopplerSignature(body, secret) {
|
|
85
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
86
|
+
hmac.update(body);
|
|
87
|
+
return `sha256=${hmac.digest('hex')}`;
|
|
88
|
+
}
|
|
89
|
+
function createSanitySignature(body, secret, timestamp) {
|
|
90
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
91
|
+
hmac.update(`${timestamp}.${body}`);
|
|
92
|
+
return `t=${timestamp},v1=${hmac.digest('hex')}`;
|
|
93
|
+
}
|
|
68
94
|
function createFalPayloadToSign(body, requestId, userId, timestamp) {
|
|
69
95
|
const bodyHash = (0, crypto_1.createHash)('sha256').update(body).digest('hex');
|
|
70
|
-
return `${requestId}
|
|
96
|
+
return `${requestId}\n${userId}\n${timestamp}\n${bodyHash}`;
|
|
71
97
|
}
|
|
72
98
|
async function runTests() {
|
|
73
99
|
console.log('๐งช Running Webhook Verification Tests...\n');
|
|
@@ -81,7 +107,8 @@ async function runTests() {
|
|
|
81
107
|
'content-type': 'application/json',
|
|
82
108
|
});
|
|
83
109
|
const stripeResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(stripeRequest, 'stripe', testSecret);
|
|
84
|
-
|
|
110
|
+
const stripePassed = stripeResult.isValid && Boolean(stripeResult.eventId?.startsWith('stripe:'));
|
|
111
|
+
console.log(' โ
Stripe:', stripePassed ? 'PASSED' : 'FAILED');
|
|
85
112
|
if (!stripeResult.isValid) {
|
|
86
113
|
console.log(' โ Error:', stripeResult.error);
|
|
87
114
|
}
|
|
@@ -181,7 +208,7 @@ async function runTests() {
|
|
|
181
208
|
catch (error) {
|
|
182
209
|
console.log(' โ Dodo Payments test failed:', error);
|
|
183
210
|
}
|
|
184
|
-
// Test 5: Token-based
|
|
211
|
+
// Test 5: Token-based authentication helper
|
|
185
212
|
console.log('\n5. Testing Token-based Authentication...');
|
|
186
213
|
try {
|
|
187
214
|
const webhookId = 'test-webhook-id';
|
|
@@ -191,8 +218,10 @@ async function runTests() {
|
|
|
191
218
|
'x-webhook-token': webhookToken,
|
|
192
219
|
'content-type': 'application/json',
|
|
193
220
|
});
|
|
194
|
-
const tokenResult = await index_1.WebhookVerificationService.
|
|
195
|
-
|
|
221
|
+
const tokenResult = await index_1.WebhookVerificationService.verifyTokenAuth(tokenRequest.clone(), webhookId, webhookToken);
|
|
222
|
+
const tokenAliasResult = await index_1.WebhookVerificationService.verifyTokenBased(tokenRequest.clone(), webhookId, webhookToken);
|
|
223
|
+
const tokenPassed = tokenResult.isValid && tokenAliasResult.isValid;
|
|
224
|
+
console.log(' โ
Token-based:', tokenPassed ? 'PASSED' : 'FAILED');
|
|
196
225
|
if (!tokenResult.isValid) {
|
|
197
226
|
console.log(' โ Error:', tokenResult.error);
|
|
198
227
|
}
|
|
@@ -200,6 +229,88 @@ async function runTests() {
|
|
|
200
229
|
catch (error) {
|
|
201
230
|
console.log(' โ Token-based test failed:', error);
|
|
202
231
|
}
|
|
232
|
+
// Test 5.5: Sentry webhook verification (standard + issue alert fallback)
|
|
233
|
+
console.log('\n5.5. Testing Sentry Webhook...');
|
|
234
|
+
try {
|
|
235
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
236
|
+
const requestId = 'sentry-request-id-123';
|
|
237
|
+
const sentryBody = JSON.stringify({
|
|
238
|
+
action: 'triggered',
|
|
239
|
+
data: {
|
|
240
|
+
issue_alert: {
|
|
241
|
+
id: 'alert-1',
|
|
242
|
+
title: 'CPU high',
|
|
243
|
+
project: 'infra',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const sentryRequest = createMockRequest({
|
|
248
|
+
'sentry-hook-signature': createSentrySignature(sentryBody, testSecret),
|
|
249
|
+
'sentry-hook-timestamp': timestamp.toString(),
|
|
250
|
+
'request-id': requestId,
|
|
251
|
+
'content-type': 'application/json',
|
|
252
|
+
}, sentryBody);
|
|
253
|
+
const sentryResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(sentryRequest, 'sentry', testSecret);
|
|
254
|
+
const sentryIssueAlertRequest = createMockRequest({
|
|
255
|
+
'sentry-hook-signature': createSentryIssueAlertSignature({ id: 'alert-1', title: 'CPU high', project: 'infra' }, testSecret),
|
|
256
|
+
'sentry-hook-timestamp': timestamp.toString(),
|
|
257
|
+
'request-id': `${requestId}-issue-alert`,
|
|
258
|
+
'content-type': 'application/json',
|
|
259
|
+
}, sentryBody);
|
|
260
|
+
const sentryIssueAlertResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(sentryIssueAlertRequest, 'sentry', testSecret);
|
|
261
|
+
const sentryPassed = sentryResult.isValid && sentryIssueAlertResult.isValid && sentryResult.eventId?.startsWith('sentry:');
|
|
262
|
+
console.log(' โ
Sentry:', sentryPassed ? 'PASSED' : 'FAILED');
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
console.log(' โ Sentry test failed:', error);
|
|
266
|
+
}
|
|
267
|
+
// Test 5.6: Grafana webhook verification
|
|
268
|
+
console.log('\n5.6. Testing Grafana Webhook...');
|
|
269
|
+
try {
|
|
270
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
271
|
+
const grafanaSignature = createGrafanaSignature(testBody, testSecret, timestamp);
|
|
272
|
+
const grafanaRequest = createMockRequest({
|
|
273
|
+
'x-grafana-alerting-signature': grafanaSignature,
|
|
274
|
+
'x-grafana-alerting-timestamp': timestamp.toString(),
|
|
275
|
+
'content-type': 'application/json',
|
|
276
|
+
});
|
|
277
|
+
const grafanaResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(grafanaRequest, 'grafana', testSecret);
|
|
278
|
+
console.log(' โ
Grafana:', grafanaResult.isValid ? 'PASSED' : 'FAILED');
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.log(' โ Grafana test failed:', error);
|
|
282
|
+
}
|
|
283
|
+
// Test 5.7: Doppler webhook verification
|
|
284
|
+
console.log('\n5.7. Testing Doppler Webhook...');
|
|
285
|
+
try {
|
|
286
|
+
const dopplerRequest = createMockRequest({
|
|
287
|
+
'x-doppler-signature': createDopplerSignature(testBody, testSecret),
|
|
288
|
+
'content-type': 'application/json',
|
|
289
|
+
});
|
|
290
|
+
const dopplerResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(dopplerRequest, 'doppler', testSecret);
|
|
291
|
+
const dopplerPassed = dopplerResult.isValid && Boolean(dopplerResult.eventId?.startsWith('doppler:'));
|
|
292
|
+
console.log(' โ
Doppler:', dopplerPassed ? 'PASSED' : 'FAILED');
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
console.log(' โ Doppler test failed:', error);
|
|
296
|
+
}
|
|
297
|
+
// Test 5.8: Sanity webhook verification
|
|
298
|
+
console.log('\n5.8. Testing Sanity Webhook...');
|
|
299
|
+
try {
|
|
300
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
301
|
+
const idempotencyKey = 'sanity-idem-123';
|
|
302
|
+
const sanityRequest = createMockRequest({
|
|
303
|
+
'sanity-webhook-signature': createSanitySignature(testBody, testSecret, timestamp),
|
|
304
|
+
'idempotency-key': idempotencyKey,
|
|
305
|
+
'content-type': 'application/json',
|
|
306
|
+
});
|
|
307
|
+
const sanityResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(sanityRequest, 'sanity', testSecret);
|
|
308
|
+
const sanityPassed = sanityResult.isValid && sanityResult.eventId === `sanity:${idempotencyKey}`;
|
|
309
|
+
console.log(' โ
Sanity:', sanityPassed ? 'PASSED' : 'FAILED');
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
console.log(' โ Sanity test failed:', error);
|
|
313
|
+
}
|
|
203
314
|
// Test 6: Invalid signatures
|
|
204
315
|
console.log('\n6. Testing Invalid Signatures...');
|
|
205
316
|
try {
|
|
@@ -388,24 +499,8 @@ async function runTests() {
|
|
|
388
499
|
catch (error) {
|
|
389
500
|
console.log(' โ Paddle test failed:', error);
|
|
390
501
|
}
|
|
391
|
-
// Test 16:
|
|
392
|
-
console.log('\n16. Testing
|
|
393
|
-
try {
|
|
394
|
-
const hmac = (0, crypto_1.createHmac)('sha256', testSecret);
|
|
395
|
-
hmac.update(testBody);
|
|
396
|
-
const signature = hmac.digest('hex');
|
|
397
|
-
const request = createMockRequest({
|
|
398
|
-
'x-auth0-signature': signature,
|
|
399
|
-
'content-type': 'application/json',
|
|
400
|
-
});
|
|
401
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'auth0', testSecret);
|
|
402
|
-
console.log(' โ
Auth0:', result.isValid ? 'PASSED' : 'FAILED');
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
console.log(' โ Auth0 test failed:', error);
|
|
406
|
-
}
|
|
407
|
-
// Test 17: WorkOS
|
|
408
|
-
console.log('\n17. Testing WorkOS webhook...');
|
|
502
|
+
// Test 16: WorkOS
|
|
503
|
+
console.log('\n16. Testing WorkOS webhook...');
|
|
409
504
|
try {
|
|
410
505
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
411
506
|
const signature = createWorkOSSignature(testBody, testSecret, timestamp);
|
|
@@ -420,7 +515,7 @@ async function runTests() {
|
|
|
420
515
|
console.log(' โ WorkOS test failed:', error);
|
|
421
516
|
}
|
|
422
517
|
// Test 17.5: Shopify
|
|
423
|
-
console.log('\n17.
|
|
518
|
+
console.log('\n17. Testing Shopify webhook...');
|
|
424
519
|
try {
|
|
425
520
|
const signature = createShopifySignature(testBody, testSecret);
|
|
426
521
|
const request = createMockRequest({
|
|
@@ -501,11 +596,11 @@ async function runTests() {
|
|
|
501
596
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
502
597
|
const payloadToSign = createFalPayloadToSign(testBody, requestId, userId, timestamp);
|
|
503
598
|
const payloadBytes = new Uint8Array(Buffer.from(payloadToSign));
|
|
504
|
-
const signature = (0, crypto_1.sign)(null, payloadBytes, privateKey).toString('
|
|
599
|
+
const signature = (0, crypto_1.sign)(null, payloadBytes, privateKey).toString('hex');
|
|
505
600
|
const request = createMockRequest({
|
|
506
601
|
'x-fal-webhook-signature': signature,
|
|
507
|
-
'x-fal-request-id': requestId,
|
|
508
|
-
'x-fal-user-id': userId,
|
|
602
|
+
'x-fal-webhook-request-id': requestId,
|
|
603
|
+
'x-fal-webhook-user-id': userId,
|
|
509
604
|
'x-fal-webhook-timestamp': timestamp,
|
|
510
605
|
'content-type': 'application/json',
|
|
511
606
|
});
|
|
@@ -519,8 +614,8 @@ async function runTests() {
|
|
|
519
614
|
payloadFormat: 'custom',
|
|
520
615
|
customConfig: {
|
|
521
616
|
publicKey: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
522
|
-
requestIdHeader: 'x-fal-request-id',
|
|
523
|
-
userIdHeader: 'x-fal-user-id',
|
|
617
|
+
requestIdHeader: 'x-fal-webhook-request-id',
|
|
618
|
+
userIdHeader: 'x-fal-webhook-user-id',
|
|
524
619
|
timestampHeader: 'x-fal-webhook-timestamp',
|
|
525
620
|
},
|
|
526
621
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WebhookPlatform = 'custom' | 'clerk' | '
|
|
1
|
+
export type WebhookPlatform = 'custom' | 'clerk' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'paddle' | 'razorpay' | 'lemonsqueezy' | 'workos' | 'woocommerce' | 'replicateai' | 'falai' | 'sentry' | 'grafana' | 'doppler' | 'sanity' | 'unknown';
|
|
2
2
|
export declare enum WebhookPlatformKeys {
|
|
3
3
|
GitHub = "github",
|
|
4
4
|
Stripe = "stripe",
|
|
@@ -7,16 +7,18 @@ export declare enum WebhookPlatformKeys {
|
|
|
7
7
|
Shopify = "shopify",
|
|
8
8
|
Vercel = "vercel",
|
|
9
9
|
Polar = "polar",
|
|
10
|
-
Supabase = "supabase",
|
|
11
10
|
GitLab = "gitlab",
|
|
12
11
|
Paddle = "paddle",
|
|
13
12
|
Razorpay = "razorpay",
|
|
14
13
|
LemonSqueezy = "lemonsqueezy",
|
|
15
|
-
Auth0 = "auth0",
|
|
16
14
|
WorkOS = "workos",
|
|
17
15
|
WooCommerce = "woocommerce",
|
|
18
16
|
ReplicateAI = "replicateai",
|
|
19
17
|
FalAI = "falai",
|
|
18
|
+
Sentry = "sentry",
|
|
19
|
+
Grafana = "grafana",
|
|
20
|
+
Doppler = "doppler",
|
|
21
|
+
Sanity = "sanity",
|
|
20
22
|
Custom = "custom",
|
|
21
23
|
Unknown = "unknown"
|
|
22
24
|
}
|
|
@@ -28,7 +30,8 @@ export interface SignatureConfig {
|
|
|
28
30
|
prefix?: string;
|
|
29
31
|
timestampHeader?: string;
|
|
30
32
|
timestampFormat?: 'unix' | 'iso' | 'custom';
|
|
31
|
-
payloadFormat?: 'raw' | 'timestamped' | 'custom';
|
|
33
|
+
payloadFormat?: 'raw' | 'timestamped' | 'json-stringified' | 'custom';
|
|
34
|
+
idHeader?: string;
|
|
32
35
|
customConfig?: Record<string, any>;
|
|
33
36
|
}
|
|
34
37
|
export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
|
|
@@ -101,6 +104,7 @@ export interface WebhookVerificationResult<TPayload = unknown> {
|
|
|
101
104
|
errorCode?: WebhookErrorCode;
|
|
102
105
|
platform: WebhookPlatform;
|
|
103
106
|
payload?: TPayload;
|
|
107
|
+
eventId?: string;
|
|
104
108
|
metadata?: {
|
|
105
109
|
timestamp?: string;
|
|
106
110
|
id?: string | null;
|
package/dist/types.js
CHANGED
|
@@ -10,16 +10,18 @@ var WebhookPlatformKeys;
|
|
|
10
10
|
WebhookPlatformKeys["Shopify"] = "shopify";
|
|
11
11
|
WebhookPlatformKeys["Vercel"] = "vercel";
|
|
12
12
|
WebhookPlatformKeys["Polar"] = "polar";
|
|
13
|
-
WebhookPlatformKeys["Supabase"] = "supabase";
|
|
14
13
|
WebhookPlatformKeys["GitLab"] = "gitlab";
|
|
15
14
|
WebhookPlatformKeys["Paddle"] = "paddle";
|
|
16
15
|
WebhookPlatformKeys["Razorpay"] = "razorpay";
|
|
17
16
|
WebhookPlatformKeys["LemonSqueezy"] = "lemonsqueezy";
|
|
18
|
-
WebhookPlatformKeys["Auth0"] = "auth0";
|
|
19
17
|
WebhookPlatformKeys["WorkOS"] = "workos";
|
|
20
18
|
WebhookPlatformKeys["WooCommerce"] = "woocommerce";
|
|
21
19
|
WebhookPlatformKeys["ReplicateAI"] = "replicateai";
|
|
22
20
|
WebhookPlatformKeys["FalAI"] = "falai";
|
|
21
|
+
WebhookPlatformKeys["Sentry"] = "sentry";
|
|
22
|
+
WebhookPlatformKeys["Grafana"] = "grafana";
|
|
23
|
+
WebhookPlatformKeys["Doppler"] = "doppler";
|
|
24
|
+
WebhookPlatformKeys["Sanity"] = "sanity";
|
|
23
25
|
WebhookPlatformKeys["Custom"] = "custom";
|
|
24
26
|
WebhookPlatformKeys["Unknown"] = "unknown";
|
|
25
27
|
})(WebhookPlatformKeys || (exports.WebhookPlatformKeys = WebhookPlatformKeys = {}));
|
package/dist/utils.js
CHANGED
|
@@ -131,9 +131,6 @@ function detectPlatformFromHeaders(headers) {
|
|
|
131
131
|
if (headerMap.has('x-signature')) {
|
|
132
132
|
return 'lemonsqueezy';
|
|
133
133
|
}
|
|
134
|
-
if (headerMap.has('x-auth0-signature')) {
|
|
135
|
-
return 'auth0';
|
|
136
|
-
}
|
|
137
134
|
if (headerMap.has('x-wc-webhook-signature')) {
|
|
138
135
|
return 'woocommerce';
|
|
139
136
|
}
|
|
@@ -158,10 +155,6 @@ function detectPlatformFromHeaders(headers) {
|
|
|
158
155
|
return 'polar';
|
|
159
156
|
}
|
|
160
157
|
}
|
|
161
|
-
// Supabase
|
|
162
|
-
if (headerMap.has('x-webhook-token')) {
|
|
163
|
-
return 'supabase';
|
|
164
|
-
}
|
|
165
158
|
return null;
|
|
166
159
|
}
|
|
167
160
|
/**
|
|
@@ -17,6 +17,7 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
|
|
|
17
17
|
protected extractMetadata(request: Request): Record<string, any>;
|
|
18
18
|
}
|
|
19
19
|
export declare class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
20
|
+
private resolveSentryPayloadCandidates;
|
|
20
21
|
verify(request: Request): Promise<WebhookVerificationResult>;
|
|
21
22
|
}
|
|
22
23
|
export declare class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
@@ -93,6 +93,13 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
93
93
|
this.extractTimestamp(request);
|
|
94
94
|
return timestamp ? `${timestamp}.${rawBody}` : rawBody;
|
|
95
95
|
}
|
|
96
|
+
case "json-stringified":
|
|
97
|
+
try {
|
|
98
|
+
return JSON.stringify(JSON.parse(rawBody));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return rawBody;
|
|
102
|
+
}
|
|
96
103
|
case "custom":
|
|
97
104
|
return this.formatCustomPayload(rawBody, request);
|
|
98
105
|
case "raw":
|
|
@@ -107,10 +114,18 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
107
114
|
const customFormat = this.config.customConfig.payloadFormat;
|
|
108
115
|
if (customFormat.includes("{id}") && customFormat.includes("{timestamp}")) {
|
|
109
116
|
const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id");
|
|
110
|
-
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
117
|
+
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
118
|
+
this.config.customConfig?.timestampHeader ||
|
|
119
|
+
"x-webhook-timestamp");
|
|
120
|
+
// if either is missing payload will be malformed โ fail explicitly
|
|
121
|
+
if (!id || !timestamp) {
|
|
122
|
+
throw new Error(`Missing required headers for payload construction: ${!id ? this.config.customConfig.idHeader || "x-webhook-id" : ""} ${!timestamp
|
|
123
|
+
? this.config.timestampHeader || "x-webhook-timestamp"
|
|
124
|
+
: ""}`.trim());
|
|
125
|
+
}
|
|
111
126
|
return customFormat
|
|
112
|
-
.replace("{id}", id || "")
|
|
113
|
-
.replace("{timestamp}", timestamp || "")
|
|
127
|
+
.replace("{id}", id.trim() || "")
|
|
128
|
+
.replace("{timestamp}", timestamp.trim() || "")
|
|
114
129
|
.replace("{body}", rawBody);
|
|
115
130
|
}
|
|
116
131
|
if (customFormat.includes("{timestamp}") &&
|
|
@@ -178,9 +193,16 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
178
193
|
metadata.id = request.headers.get(this.config.customConfig?.idHeader || "webhook-id");
|
|
179
194
|
break;
|
|
180
195
|
case "workos":
|
|
181
|
-
|
|
196
|
+
case "sentry":
|
|
197
|
+
case "sanity":
|
|
198
|
+
metadata.id = request.headers.get(this.config.idHeader ||
|
|
199
|
+
this.config.customConfig?.idHeader ||
|
|
200
|
+
"webhook-id");
|
|
182
201
|
break;
|
|
183
202
|
default:
|
|
203
|
+
if (this.config.idHeader) {
|
|
204
|
+
metadata.id = request.headers.get(this.config.idHeader);
|
|
205
|
+
}
|
|
184
206
|
break;
|
|
185
207
|
}
|
|
186
208
|
return metadata;
|
|
@@ -188,6 +210,33 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
188
210
|
}
|
|
189
211
|
exports.AlgorithmBasedVerifier = AlgorithmBasedVerifier;
|
|
190
212
|
class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
213
|
+
resolveSentryPayloadCandidates(rawBody, request) {
|
|
214
|
+
const candidates = [
|
|
215
|
+
this.formatPayload(rawBody, request),
|
|
216
|
+
rawBody,
|
|
217
|
+
];
|
|
218
|
+
if (this.config.payloadFormat === "json-stringified") {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(rawBody);
|
|
221
|
+
candidates.push(JSON.stringify(parsed));
|
|
222
|
+
const issueAlertPath = this.config.customConfig?.issueAlertPayloadPath;
|
|
223
|
+
if (issueAlertPath) {
|
|
224
|
+
const segments = `${issueAlertPath}`.split(".").filter(Boolean);
|
|
225
|
+
let current = parsed;
|
|
226
|
+
for (const segment of segments) {
|
|
227
|
+
current = current?.[segment];
|
|
228
|
+
}
|
|
229
|
+
if (current !== undefined) {
|
|
230
|
+
candidates.push(typeof current === "string" ? current : JSON.stringify(current));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// ignore malformed JSON payloads
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
239
|
+
}
|
|
191
240
|
async verify(request) {
|
|
192
241
|
try {
|
|
193
242
|
const signature = this.extractSignature(request);
|
|
@@ -215,17 +264,24 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
215
264
|
platform: this.platform,
|
|
216
265
|
};
|
|
217
266
|
}
|
|
218
|
-
const
|
|
267
|
+
const payloadCandidates = this.platform === "sentry"
|
|
268
|
+
? this.resolveSentryPayloadCandidates(rawBody, request)
|
|
269
|
+
: [this.formatPayload(rawBody, request)];
|
|
219
270
|
let isValid = false;
|
|
220
271
|
const algorithm = this.config.algorithm.replace("hmac-", "");
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
272
|
+
for (const payload of payloadCandidates) {
|
|
273
|
+
if (this.config.customConfig?.encoding === "base64") {
|
|
274
|
+
isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
|
|
275
|
+
}
|
|
276
|
+
else if (this.config.headerFormat === "prefixed") {
|
|
277
|
+
isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
isValid = this.verifyHMAC(payload, signature, algorithm);
|
|
281
|
+
}
|
|
282
|
+
if (isValid) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
229
285
|
}
|
|
230
286
|
if (!isValid) {
|
|
231
287
|
return {
|
|
@@ -242,11 +298,18 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
242
298
|
catch (e) {
|
|
243
299
|
parsedPayload = rawBody;
|
|
244
300
|
}
|
|
301
|
+
const metadata = this.extractMetadata(request);
|
|
302
|
+
if (this.platform === "doppler" && !metadata.id) {
|
|
303
|
+
const timestamp = metadata.timestamp || Math.floor(Date.now() / 1000).toString();
|
|
304
|
+
metadata.id = (0, crypto_1.createHash)("sha256")
|
|
305
|
+
.update(`${timestamp}:${rawBody}`)
|
|
306
|
+
.digest("hex");
|
|
307
|
+
}
|
|
245
308
|
return {
|
|
246
309
|
isValid: true,
|
|
247
310
|
platform: this.platform,
|
|
248
311
|
payload: parsedPayload,
|
|
249
|
-
metadata
|
|
312
|
+
metadata,
|
|
250
313
|
};
|
|
251
314
|
}
|
|
252
315
|
catch (error) {
|
package/dist/verifiers/base.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TokenBasedVerifier = void 0;
|
|
4
4
|
exports.createCustomVerifier = createCustomVerifier;
|
|
5
5
|
const base_1 = require("./base");
|
|
6
|
-
// Token-based verifier for
|
|
6
|
+
// Token-based verifier for providers that use header token authentication
|
|
7
7
|
class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
8
8
|
constructor(secret, config, toleranceInSeconds = 300) {
|
|
9
9
|
super(secret, toleranceInSeconds);
|
package/package.json
CHANGED