@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/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# Tern - Algorithm Agnostic Webhook Verification Framework
|
|
2
2
|
|
|
3
3
|
A robust, algorithm-agnostic webhook verification framework that supports multiple platforms with accurate signature verification and payload retrieval.
|
|
4
|
+
The same framework that secures webhook verification at [Hookflo](https://hookflo.com).
|
|
5
|
+
|
|
6
|
+
⭐ Star this repo to support the project and help others discover it!
|
|
7
|
+
|
|
8
|
+
💬 Join the discussion & contribute in our Discord: [Hookflo Community](https://discord.com/invite/SNmCjU97nr)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @hookflo/tern
|
|
12
|
+
```
|
|
4
13
|
|
|
5
14
|
[](https://www.npmjs.com/package/@hookflo/tern)
|
|
6
15
|
[](https://www.typescriptlang.org/)
|
|
@@ -9,15 +18,22 @@ A robust, algorithm-agnostic webhook verification framework that supports multip
|
|
|
9
18
|
Tern is a zero-dependency TypeScript framework for robust webhook verification across multiple platforms and algorithms.
|
|
10
19
|
|
|
11
20
|
<img width="1396" height="470" style="border-radius: 10px" alt="tern bird nature" src="https://github.com/user-attachments/assets/5f0da3e6-1aba-4f88-a9d7-9d8698845c39" />
|
|
21
|
+
|
|
12
22
|
## Features
|
|
13
23
|
|
|
14
24
|
- **Algorithm Agnostic**: Decouples platform logic from signature verification — verify based on cryptographic algorithm, not hardcoded platform rules.
|
|
15
25
|
Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
|
|
16
26
|
|
|
17
|
-
- **Platform Specific**: Accurate implementations for Stripe, GitHub,
|
|
27
|
+
- **Platform Specific**: Accurate implementations for **Stripe, GitHub, Supabase, Clerk**, and other platforms
|
|
18
28
|
- **Flexible Configuration**: Custom signature configurations for any webhook format
|
|
19
29
|
- **Type Safe**: Full TypeScript support with comprehensive type definitions
|
|
20
30
|
- **Framework Agnostic**: Works with Express.js, Next.js, Cloudflare Workers, and more
|
|
31
|
+
- **Body-Parser Safe Adapters**: Read raw request bodies correctly to avoid signature mismatch issues
|
|
32
|
+
- **Multi-Provider Verification**: Verify and auto-detect across multiple providers with one API
|
|
33
|
+
- **Payload Normalization**: Opt-in normalized event shape to reduce provider lock-in
|
|
34
|
+
- **Category-aware Migration**: Normalize within provider categories (payment/auth/infrastructure) for safe platform switching
|
|
35
|
+
- **Strong Typed Normalized Schemas**: Category types like `PaymentWebhookNormalized` and `AuthWebhookNormalized` for safe migrations
|
|
36
|
+
- **Foundational Error Taxonomy**: Stable `errorCode` values (`INVALID_SIGNATURE`, `MISSING_SIGNATURE`, etc.)
|
|
21
37
|
|
|
22
38
|
## Why Tern?
|
|
23
39
|
|
|
@@ -40,15 +56,18 @@ npm install @hookflo/tern
|
|
|
40
56
|
### Basic Usage
|
|
41
57
|
|
|
42
58
|
```typescript
|
|
43
|
-
import { WebhookVerificationService } from '@hookflo/tern';
|
|
59
|
+
import { WebhookVerificationService, platformManager } from '@hookflo/tern';
|
|
44
60
|
|
|
45
|
-
//
|
|
61
|
+
// Method 1: Using the service (recommended)
|
|
46
62
|
const result = await WebhookVerificationService.verifyWithPlatformConfig(
|
|
47
63
|
request,
|
|
48
64
|
'stripe',
|
|
49
65
|
'whsec_your_stripe_webhook_secret'
|
|
50
66
|
);
|
|
51
67
|
|
|
68
|
+
// Method 2: Using platform manager (for platform-specific operations)
|
|
69
|
+
const stripeResult = await platformManager.verify(request, 'stripe', 'whsec_your_secret');
|
|
70
|
+
|
|
52
71
|
if (result.isValid) {
|
|
53
72
|
console.log('Webhook verified!', result.payload);
|
|
54
73
|
} else {
|
|
@@ -56,6 +75,93 @@ if (result.isValid) {
|
|
|
56
75
|
}
|
|
57
76
|
```
|
|
58
77
|
|
|
78
|
+
### Universal Verification (auto-detect platform)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { WebhookVerificationService } from '@hookflo/tern';
|
|
82
|
+
|
|
83
|
+
const result = await WebhookVerificationService.verifyAny(request, {
|
|
84
|
+
stripe: process.env.STRIPE_WEBHOOK_SECRET,
|
|
85
|
+
github: process.env.GITHUB_WEBHOOK_SECRET,
|
|
86
|
+
clerk: process.env.CLERK_WEBHOOK_SECRET,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (result.isValid) {
|
|
90
|
+
console.log(`Verified ${result.platform} webhook`);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Category-aware Payload Normalization
|
|
95
|
+
|
|
96
|
+
### Strongly-Typed Normalized Payloads
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import {
|
|
100
|
+
WebhookVerificationService,
|
|
101
|
+
PaymentWebhookNormalized,
|
|
102
|
+
} from '@hookflo/tern';
|
|
103
|
+
|
|
104
|
+
const result = await WebhookVerificationService.verifyWithPlatformConfig<PaymentWebhookNormalized>(
|
|
105
|
+
request,
|
|
106
|
+
'stripe',
|
|
107
|
+
process.env.STRIPE_WEBHOOK_SECRET!,
|
|
108
|
+
300,
|
|
109
|
+
{ enabled: true, category: 'payment' },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (result.isValid && result.payload?.event === 'payment.succeeded') {
|
|
113
|
+
// result.payload is strongly typed
|
|
114
|
+
console.log(result.payload.amount, result.payload.customer_id);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { WebhookVerificationService, getPlatformsByCategory } from '@hookflo/tern';
|
|
120
|
+
|
|
121
|
+
// Discover migration-compatible providers in the same category
|
|
122
|
+
const paymentPlatforms = getPlatformsByCategory('payment');
|
|
123
|
+
// ['stripe', 'polar', ...]
|
|
124
|
+
|
|
125
|
+
const result = await WebhookVerificationService.verifyWithPlatformConfig(
|
|
126
|
+
request,
|
|
127
|
+
'stripe',
|
|
128
|
+
process.env.STRIPE_WEBHOOK_SECRET!,
|
|
129
|
+
300,
|
|
130
|
+
{
|
|
131
|
+
enabled: true,
|
|
132
|
+
category: 'payment',
|
|
133
|
+
includeRaw: true,
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
console.log(result.payload);
|
|
138
|
+
// {
|
|
139
|
+
// event: 'payment.succeeded',
|
|
140
|
+
// amount: 5000,
|
|
141
|
+
// currency: 'USD',
|
|
142
|
+
// customer_id: 'cus_123',
|
|
143
|
+
// transaction_id: 'pi_123',
|
|
144
|
+
// provider: 'stripe',
|
|
145
|
+
// category: 'payment',
|
|
146
|
+
// _raw: {...}
|
|
147
|
+
// }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Platform-Specific Usage
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { platformManager } from '@hookflo/tern';
|
|
154
|
+
|
|
155
|
+
// Run tests for a specific platform
|
|
156
|
+
const testsPassed = await platformManager.runPlatformTests('stripe');
|
|
157
|
+
|
|
158
|
+
// Get platform configuration
|
|
159
|
+
const config = platformManager.getConfig('stripe');
|
|
160
|
+
|
|
161
|
+
// Get platform documentation
|
|
162
|
+
const docs = platformManager.getDocumentation('stripe');
|
|
163
|
+
```
|
|
164
|
+
|
|
59
165
|
### Platform-Specific Configurations
|
|
60
166
|
|
|
61
167
|
```typescript
|
|
@@ -108,6 +214,7 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
|
|
|
108
214
|
- **Vercel**: HMAC-SHA256
|
|
109
215
|
- **Polar**: HMAC-SHA256
|
|
110
216
|
- **Supabase**: Token-based authentication
|
|
217
|
+
- **GitLab**: Token-based authentication
|
|
111
218
|
|
|
112
219
|
## Custom Platform Configuration
|
|
113
220
|
|
|
@@ -207,80 +314,59 @@ const timestampedConfig = {
|
|
|
207
314
|
|
|
208
315
|
## Framework Integration
|
|
209
316
|
|
|
210
|
-
### Express.js
|
|
317
|
+
### Express.js middleware (body-parser safe)
|
|
211
318
|
|
|
212
319
|
```typescript
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
320
|
+
import express from 'express';
|
|
321
|
+
import { createWebhookMiddleware } from '@hookflo/tern/express';
|
|
322
|
+
|
|
323
|
+
const app = express();
|
|
324
|
+
|
|
325
|
+
app.post(
|
|
326
|
+
'/webhooks/stripe',
|
|
327
|
+
createWebhookMiddleware({
|
|
328
|
+
platform: 'stripe',
|
|
329
|
+
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
330
|
+
normalize: true,
|
|
331
|
+
}),
|
|
332
|
+
(req, res) => {
|
|
333
|
+
const event = (req as any).webhook.payload;
|
|
334
|
+
res.json({ received: true, event: event.event });
|
|
335
|
+
},
|
|
336
|
+
);
|
|
228
337
|
```
|
|
229
338
|
|
|
230
|
-
### Next.js
|
|
339
|
+
### Next.js App Router
|
|
231
340
|
|
|
232
341
|
```typescript
|
|
233
|
-
|
|
234
|
-
export default async function handler(req, res) {
|
|
235
|
-
if (req.method !== 'POST') {
|
|
236
|
-
return res.status(405).json({ error: 'Method not allowed' });
|
|
237
|
-
}
|
|
342
|
+
import { createWebhookHandler } from '@hookflo/tern/nextjs';
|
|
238
343
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (!result.isValid) {
|
|
246
|
-
return res.status(400).json({ error: result.error });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Handle GitHub webhook
|
|
250
|
-
const event = req.headers['x-github-event'];
|
|
251
|
-
console.log('GitHub event:', event);
|
|
252
|
-
|
|
253
|
-
res.json({ received: true });
|
|
254
|
-
}
|
|
344
|
+
export const POST = createWebhookHandler({
|
|
345
|
+
platform: 'github',
|
|
346
|
+
secret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
347
|
+
handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
|
|
348
|
+
});
|
|
255
349
|
```
|
|
256
350
|
|
|
257
351
|
### Cloudflare Workers
|
|
258
352
|
|
|
259
353
|
```typescript
|
|
260
|
-
|
|
261
|
-
|
|
354
|
+
import { createWebhookHandler } from '@hookflo/tern/cloudflare';
|
|
355
|
+
|
|
356
|
+
const handleStripe = createWebhookHandler({
|
|
357
|
+
platform: 'stripe',
|
|
358
|
+
secretEnv: 'STRIPE_WEBHOOK_SECRET',
|
|
359
|
+
handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
|
|
262
360
|
});
|
|
263
361
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
request,
|
|
268
|
-
'clerk',
|
|
269
|
-
CLERK_WEBHOOK_SECRET
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
if (!result.isValid) {
|
|
273
|
-
return new Response(JSON.stringify({ error: result.error }), {
|
|
274
|
-
status: 400,
|
|
275
|
-
headers: { 'Content-Type': 'application/json' }
|
|
276
|
-
});
|
|
362
|
+
export default {
|
|
363
|
+
async fetch(request: Request, env: Record<string, string>) {
|
|
364
|
+
if (new URL(request.url).pathname === '/webhooks/stripe') {
|
|
365
|
+
return handleStripe(request, env);
|
|
277
366
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return new Response(JSON.stringify({ received: true }));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
367
|
+
return new Response('Not Found', { status: 404 });
|
|
368
|
+
},
|
|
369
|
+
};
|
|
284
370
|
```
|
|
285
371
|
|
|
286
372
|
## API Reference
|
|
@@ -291,14 +377,22 @@ async function handleRequest(request) {
|
|
|
291
377
|
|
|
292
378
|
Verifies a webhook using the provided configuration.
|
|
293
379
|
|
|
294
|
-
#### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult>`
|
|
380
|
+
#### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult>`
|
|
295
381
|
|
|
296
|
-
Simplified verification using platform-specific configurations.
|
|
382
|
+
Simplified verification using platform-specific configurations with optional payload normalization.
|
|
383
|
+
|
|
384
|
+
#### `verifyAny(request: Request, secrets: Record<string, string>, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult>`
|
|
385
|
+
|
|
386
|
+
Auto-detects platform from headers and verifies against one or more provider secrets.
|
|
297
387
|
|
|
298
388
|
#### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
|
|
299
389
|
|
|
300
390
|
Verifies token-based webhooks (like Supabase).
|
|
301
391
|
|
|
392
|
+
#### `getPlatformsByCategory(category: 'payment' | 'auth' | 'ecommerce' | 'infrastructure'): WebhookPlatform[]`
|
|
393
|
+
|
|
394
|
+
Returns built-in providers that normalize into a shared schema for the given migration category.
|
|
395
|
+
|
|
302
396
|
### Types
|
|
303
397
|
|
|
304
398
|
#### `WebhookVerificationResult`
|
|
@@ -307,6 +401,7 @@ Verifies token-based webhooks (like Supabase).
|
|
|
307
401
|
interface WebhookVerificationResult {
|
|
308
402
|
isValid: boolean;
|
|
309
403
|
error?: string;
|
|
404
|
+
errorCode?: WebhookErrorCode;
|
|
310
405
|
platform: WebhookPlatform;
|
|
311
406
|
payload?: any;
|
|
312
407
|
metadata?: {
|
|
@@ -325,21 +420,39 @@ interface WebhookConfig {
|
|
|
325
420
|
secret: string;
|
|
326
421
|
toleranceInSeconds?: number;
|
|
327
422
|
signatureConfig?: SignatureConfig;
|
|
423
|
+
normalize?: boolean | NormalizeOptions;
|
|
328
424
|
}
|
|
329
425
|
```
|
|
330
426
|
|
|
331
427
|
## Testing
|
|
332
428
|
|
|
333
|
-
Run
|
|
429
|
+
### Run All Tests
|
|
334
430
|
|
|
335
431
|
```bash
|
|
336
432
|
npm test
|
|
337
433
|
```
|
|
338
434
|
|
|
339
|
-
|
|
435
|
+
### Platform-Specific Testing
|
|
340
436
|
|
|
341
437
|
```bash
|
|
342
|
-
|
|
438
|
+
# Test a specific platform
|
|
439
|
+
npm run test:platform stripe
|
|
440
|
+
|
|
441
|
+
# Test all platforms
|
|
442
|
+
npm run test:all
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Documentation and Analysis
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# Fetch platform documentation
|
|
449
|
+
npm run docs:fetch
|
|
450
|
+
|
|
451
|
+
# Generate diffs between versions
|
|
452
|
+
npm run docs:diff
|
|
453
|
+
|
|
454
|
+
# Analyze changes and generate reports
|
|
455
|
+
npm run docs:analyze
|
|
343
456
|
```
|
|
344
457
|
|
|
345
458
|
## Examples
|
|
@@ -348,11 +461,30 @@ See the [examples.ts](./src/examples.ts) file for comprehensive usage examples.
|
|
|
348
461
|
|
|
349
462
|
## Contributing
|
|
350
463
|
|
|
464
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for detailed information on how to:
|
|
465
|
+
|
|
466
|
+
- Set up your development environment
|
|
467
|
+
- Add new platforms
|
|
468
|
+
- Write tests
|
|
469
|
+
- Submit pull requests
|
|
470
|
+
- Follow our code style guidelines
|
|
471
|
+
|
|
472
|
+
### Quick Start for Contributors
|
|
473
|
+
|
|
351
474
|
1. Fork the repository
|
|
352
|
-
2.
|
|
353
|
-
3.
|
|
354
|
-
4.
|
|
355
|
-
5.
|
|
475
|
+
2. Clone your fork: `git clone https://github.com/your-username/tern.git`
|
|
476
|
+
3. Create a feature branch: `git checkout -b feature/your-feature-name`
|
|
477
|
+
4. Make your changes
|
|
478
|
+
5. Run tests: `npm test`
|
|
479
|
+
6. Submit a pull request
|
|
480
|
+
|
|
481
|
+
### Adding a New Platform
|
|
482
|
+
|
|
483
|
+
See our [Platform Development Guide](CONTRIBUTING.md#adding-new-platforms) for step-by-step instructions on adding support for new webhook platforms.
|
|
484
|
+
|
|
485
|
+
## Code of Conduct
|
|
486
|
+
|
|
487
|
+
This project adheres to our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.
|
|
356
488
|
|
|
357
489
|
## 📄 License
|
|
358
490
|
|
|
@@ -362,4 +494,5 @@ MIT License - see [LICENSE](./LICENSE) for details.
|
|
|
362
494
|
|
|
363
495
|
- [Documentation](./USAGE.md)
|
|
364
496
|
- [Framework Summary](./FRAMEWORK_SUMMARY.md)
|
|
497
|
+
- [Architecture Guide](./ARCHITECTURE.md)
|
|
365
498
|
- [Issues](https://github.com/Hookflo/tern/issues)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { WebhookPlatform, NormalizeOptions } from '../types';
|
|
2
|
+
export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>, TResponse = unknown> {
|
|
3
|
+
platform: WebhookPlatform;
|
|
4
|
+
secret?: string;
|
|
5
|
+
secretEnv?: string;
|
|
6
|
+
toleranceInSeconds?: number;
|
|
7
|
+
normalize?: boolean | NormalizeOptions;
|
|
8
|
+
onError?: (error: Error) => void;
|
|
9
|
+
handler: (payload: any, env: TEnv, metadata: Record<string, any>) => Promise<TResponse> | TResponse;
|
|
10
|
+
}
|
|
11
|
+
export declare function createWebhookHandler<TEnv = Record<string, unknown>, TResponse = unknown>(options: CloudflareWebhookHandlerOptions<TEnv, TResponse>): (request: Request, env: TEnv) => Promise<Response>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookHandler = createWebhookHandler;
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
function createWebhookHandler(options) {
|
|
6
|
+
return async (request, env) => {
|
|
7
|
+
try {
|
|
8
|
+
const secret = options.secret
|
|
9
|
+
|| (options.secretEnv ? env[options.secretEnv] : undefined);
|
|
10
|
+
if (!secret) {
|
|
11
|
+
return Response.json({ error: 'Webhook secret is not configured' }, { status: 500 });
|
|
12
|
+
}
|
|
13
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, secret, options.toleranceInSeconds, options.normalize);
|
|
14
|
+
if (!result.isValid) {
|
|
15
|
+
return Response.json({ error: result.error, platform: result.platform }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
const data = await options.handler(result.payload, env, result.metadata || {});
|
|
18
|
+
return Response.json(data);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
options.onError?.(error);
|
|
22
|
+
return Response.json({ error: error.message }, { status: 500 });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WebhookPlatform, WebhookVerificationResult, NormalizeOptions } from '../types';
|
|
2
|
+
import { MinimalNodeRequest } from './shared';
|
|
3
|
+
export interface ExpressLikeResponse {
|
|
4
|
+
status: (code: number) => ExpressLikeResponse;
|
|
5
|
+
json: (payload: unknown) => unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface ExpressLikeRequest extends MinimalNodeRequest {
|
|
8
|
+
webhook?: WebhookVerificationResult;
|
|
9
|
+
}
|
|
10
|
+
export type ExpressLikeNext = () => void;
|
|
11
|
+
export interface ExpressWebhookMiddlewareOptions {
|
|
12
|
+
platform: WebhookPlatform;
|
|
13
|
+
secret: string;
|
|
14
|
+
toleranceInSeconds?: number;
|
|
15
|
+
normalize?: boolean | NormalizeOptions;
|
|
16
|
+
onError?: (error: Error) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function createWebhookMiddleware(options: ExpressWebhookMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: ExpressLikeNext) => Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookMiddleware = createWebhookMiddleware;
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
const shared_1 = require("./shared");
|
|
6
|
+
function createWebhookMiddleware(options) {
|
|
7
|
+
return async (req, res, next) => {
|
|
8
|
+
try {
|
|
9
|
+
const webRequest = await (0, shared_1.toWebRequest)(req);
|
|
10
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds, options.normalize);
|
|
11
|
+
if (!result.isValid) {
|
|
12
|
+
res.status(400).json({ error: result.error, platform: result.platform });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
req.webhook = result;
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
options.onError?.(error);
|
|
20
|
+
res.status(500).json({ error: error.message });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createWebhookMiddleware, ExpressWebhookMiddlewareOptions, ExpressLikeRequest, ExpressLikeResponse, } from './express';
|
|
2
|
+
export { createWebhookHandler as createNextjsWebhookHandler, NextWebhookHandlerOptions, } from './nextjs';
|
|
3
|
+
export { createWebhookHandler as createCloudflareWebhookHandler, CloudflareWebhookHandlerOptions, } from './cloudflare';
|
|
4
|
+
export { toWebRequest, extractRawBody } from './shared';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractRawBody = exports.toWebRequest = exports.createCloudflareWebhookHandler = exports.createNextjsWebhookHandler = exports.createWebhookMiddleware = void 0;
|
|
4
|
+
var express_1 = require("./express");
|
|
5
|
+
Object.defineProperty(exports, "createWebhookMiddleware", { enumerable: true, get: function () { return express_1.createWebhookMiddleware; } });
|
|
6
|
+
var nextjs_1 = require("./nextjs");
|
|
7
|
+
Object.defineProperty(exports, "createNextjsWebhookHandler", { enumerable: true, get: function () { return nextjs_1.createWebhookHandler; } });
|
|
8
|
+
var cloudflare_1 = require("./cloudflare");
|
|
9
|
+
Object.defineProperty(exports, "createCloudflareWebhookHandler", { enumerable: true, get: function () { return cloudflare_1.createWebhookHandler; } });
|
|
10
|
+
var shared_1 = require("./shared");
|
|
11
|
+
Object.defineProperty(exports, "toWebRequest", { enumerable: true, get: function () { return shared_1.toWebRequest; } });
|
|
12
|
+
Object.defineProperty(exports, "extractRawBody", { enumerable: true, get: function () { return shared_1.extractRawBody; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WebhookPlatform, NormalizeOptions } from '../types';
|
|
2
|
+
export interface NextWebhookHandlerOptions<TResponse = unknown> {
|
|
3
|
+
platform: WebhookPlatform;
|
|
4
|
+
secret: string;
|
|
5
|
+
toleranceInSeconds?: number;
|
|
6
|
+
normalize?: boolean | NormalizeOptions;
|
|
7
|
+
onError?: (error: Error) => void;
|
|
8
|
+
handler: (payload: any, metadata: Record<string, any>) => Promise<TResponse> | TResponse;
|
|
9
|
+
}
|
|
10
|
+
export declare function createWebhookHandler<TResponse = unknown>(options: NextWebhookHandlerOptions<TResponse>): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookHandler = createWebhookHandler;
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
function createWebhookHandler(options) {
|
|
6
|
+
return async (request) => {
|
|
7
|
+
try {
|
|
8
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds, options.normalize);
|
|
9
|
+
if (!result.isValid) {
|
|
10
|
+
return Response.json({ error: result.error, platform: result.platform }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
const data = await options.handler(result.payload, result.metadata || {});
|
|
13
|
+
return Response.json(data);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
options.onError?.(error);
|
|
17
|
+
return Response.json({ error: error.message }, { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MinimalNodeRequest {
|
|
2
|
+
method?: string;
|
|
3
|
+
headers: Record<string, string | string[] | undefined>;
|
|
4
|
+
body?: unknown;
|
|
5
|
+
protocol?: string;
|
|
6
|
+
get?: (name: string) => string | undefined;
|
|
7
|
+
originalUrl?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
on?: (event: string, cb: (chunk?: any) => void) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function extractRawBody(request: MinimalNodeRequest): Promise<string>;
|
|
12
|
+
export declare function toHeadersInit(headers: Record<string, string | string[] | undefined>): HeadersInit;
|
|
13
|
+
export declare function toWebRequest(request: MinimalNodeRequest): Promise<Request>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractRawBody = extractRawBody;
|
|
4
|
+
exports.toHeadersInit = toHeadersInit;
|
|
5
|
+
exports.toWebRequest = toWebRequest;
|
|
6
|
+
function getHeaderValue(headers, name) {
|
|
7
|
+
const value = headers[name.toLowerCase()] ?? headers[name];
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.join(',');
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
async function readIncomingMessageBody(request) {
|
|
14
|
+
if (!request.on) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
const chunks = [];
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
request.on?.('data', (chunk) => {
|
|
20
|
+
if (typeof chunk === 'string') {
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(Buffer.from(chunk ?? '').toString('utf8'));
|
|
25
|
+
});
|
|
26
|
+
request.on?.('end', () => resolve(chunks.join('')));
|
|
27
|
+
request.on?.('error', reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function extractRawBody(request) {
|
|
31
|
+
const body = request.body;
|
|
32
|
+
if (typeof body === 'string') {
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
if (Buffer.isBuffer(body)) {
|
|
36
|
+
return body.toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
if (body && typeof body === 'object') {
|
|
39
|
+
return JSON.stringify(body);
|
|
40
|
+
}
|
|
41
|
+
return readIncomingMessageBody(request);
|
|
42
|
+
}
|
|
43
|
+
function toHeadersInit(headers) {
|
|
44
|
+
const normalized = new Headers();
|
|
45
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
46
|
+
if (!value) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
normalized.set(key, value.join(','));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
normalized.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
async function toWebRequest(request) {
|
|
58
|
+
const protocol = request.protocol || 'https';
|
|
59
|
+
const host = request.get?.('host') || getHeaderValue(request.headers, 'host') || 'localhost';
|
|
60
|
+
const path = request.originalUrl || request.url || '/';
|
|
61
|
+
const rawBody = await extractRawBody(request);
|
|
62
|
+
return new Request(`${protocol}://${host}${path}`, {
|
|
63
|
+
method: request.method || 'POST',
|
|
64
|
+
headers: toHeadersInit(request.headers),
|
|
65
|
+
body: rawBody,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookHandler = void 0;
|
|
4
|
+
var cloudflare_1 = require("./adapters/cloudflare");
|
|
5
|
+
Object.defineProperty(exports, "createWebhookHandler", { enumerable: true, get: function () { return cloudflare_1.createWebhookHandler; } });
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookMiddleware = void 0;
|
|
4
|
+
var express_1 = require("./adapters/express");
|
|
5
|
+
Object.defineProperty(exports, "createWebhookMiddleware", { enumerable: true, get: function () { return express_1.createWebhookMiddleware; } });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { WebhookConfig, WebhookVerificationResult, WebhookPlatform, SignatureConfig } from './types';
|
|
1
|
+
import { WebhookConfig, WebhookVerificationResult, WebhookPlatform, SignatureConfig, MultiPlatformSecrets, NormalizeOptions } from './types';
|
|
2
2
|
export declare class WebhookVerificationService {
|
|
3
|
-
static verify(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult
|
|
3
|
+
static verify<TPayload = unknown>(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult<TPayload>>;
|
|
4
4
|
private static getVerifier;
|
|
5
5
|
private static createAlgorithmBasedVerifier;
|
|
6
6
|
private static getLegacyVerifier;
|
|
7
|
-
static verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult
|
|
7
|
+
static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
|
|
8
|
+
static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
|
|
9
|
+
static detectPlatform(request: Request): WebhookPlatform;
|
|
8
10
|
static getPlatformsUsingAlgorithm(algorithm: string): WebhookPlatform[];
|
|
9
11
|
static platformUsesAlgorithm(platform: WebhookPlatform, algorithm: string): boolean;
|
|
10
12
|
static validateSignatureConfig(config: SignatureConfig): boolean;
|
|
11
|
-
static verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult
|
|
13
|
+
static verifyTokenBased<TPayload = unknown>(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult<TPayload>>;
|
|
12
14
|
}
|
|
13
15
|
export * from './types';
|
|
14
16
|
export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, } from './platforms/algorithms';
|
|
15
17
|
export { createAlgorithmVerifier } from './verifiers/algorithms';
|
|
16
18
|
export { createCustomVerifier } from './verifiers/custom-algorithms';
|
|
19
|
+
export { normalizePayload, getPlatformNormalizationCategory, getPlatformsByCategory, } from './normalization/simple';
|
|
20
|
+
export * from './adapters';
|
|
17
21
|
export default WebhookVerificationService;
|