@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.
Files changed (60) hide show
  1. package/README.md +204 -71
  2. package/dist/adapters/cloudflare.d.ts +11 -0
  3. package/dist/adapters/cloudflare.js +25 -0
  4. package/dist/adapters/express.d.ts +18 -0
  5. package/dist/adapters/express.js +23 -0
  6. package/dist/adapters/index.d.ts +4 -0
  7. package/dist/adapters/index.js +12 -0
  8. package/dist/adapters/nextjs.d.ts +10 -0
  9. package/dist/adapters/nextjs.js +20 -0
  10. package/dist/adapters/shared.d.ts +13 -0
  11. package/dist/adapters/shared.js +67 -0
  12. package/dist/cloudflare.d.ts +2 -0
  13. package/dist/cloudflare.js +5 -0
  14. package/dist/express.d.ts +2 -0
  15. package/dist/express.js +5 -0
  16. package/dist/index.d.ts +8 -4
  17. package/dist/index.js +66 -14
  18. package/dist/nextjs.d.ts +2 -0
  19. package/dist/nextjs.js +5 -0
  20. package/dist/normalization/index.d.ts +20 -0
  21. package/dist/normalization/index.js +78 -0
  22. package/dist/normalization/providers/payment/paypal.d.ts +2 -0
  23. package/dist/normalization/providers/payment/paypal.js +12 -0
  24. package/dist/normalization/providers/payment/razorpay.d.ts +2 -0
  25. package/dist/normalization/providers/payment/razorpay.js +13 -0
  26. package/dist/normalization/providers/payment/stripe.d.ts +2 -0
  27. package/dist/normalization/providers/payment/stripe.js +13 -0
  28. package/dist/normalization/providers/registry.d.ts +5 -0
  29. package/dist/normalization/providers/registry.js +23 -0
  30. package/dist/normalization/simple.d.ts +4 -0
  31. package/dist/normalization/simple.js +138 -0
  32. package/dist/normalization/storage/interface.d.ts +13 -0
  33. package/dist/normalization/storage/interface.js +2 -0
  34. package/dist/normalization/storage/memory.d.ts +12 -0
  35. package/dist/normalization/storage/memory.js +39 -0
  36. package/dist/normalization/templates/base/auth.d.ts +2 -0
  37. package/dist/normalization/templates/base/auth.js +22 -0
  38. package/dist/normalization/templates/base/ecommerce.d.ts +2 -0
  39. package/dist/normalization/templates/base/ecommerce.js +25 -0
  40. package/dist/normalization/templates/base/payment.d.ts +2 -0
  41. package/dist/normalization/templates/base/payment.js +25 -0
  42. package/dist/normalization/templates/registry.d.ts +6 -0
  43. package/dist/normalization/templates/registry.js +22 -0
  44. package/dist/normalization/transformer/engine.d.ts +11 -0
  45. package/dist/normalization/transformer/engine.js +86 -0
  46. package/dist/normalization/transformer/validator.d.ts +12 -0
  47. package/dist/normalization/transformer/validator.js +56 -0
  48. package/dist/normalization/types.d.ts +79 -0
  49. package/dist/normalization/types.js +2 -0
  50. package/dist/platforms/algorithms.d.ts +1 -1
  51. package/dist/platforms/algorithms.js +103 -89
  52. package/dist/test.js +98 -2
  53. package/dist/types.d.ts +73 -3
  54. package/dist/types.js +1 -0
  55. package/dist/verifiers/algorithms.d.ts +2 -2
  56. package/dist/verifiers/algorithms.js +66 -62
  57. package/dist/verifiers/base.d.ts +1 -1
  58. package/dist/verifiers/custom-algorithms.d.ts +2 -2
  59. package/dist/verifiers/custom-algorithms.js +11 -8
  60. 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
  [![npm version](https://img.shields.io/npm/v/@hookflo/tern)](https://www.npmjs.com/package/@hookflo/tern)
6
15
  [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue)](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, Clerk, and other platforms
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
- // Verify a Stripe webhook
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
- app.post('/webhooks/stripe', async (req, res) => {
214
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
215
- req,
216
- 'stripe',
217
- process.env.STRIPE_WEBHOOK_SECRET
218
- );
219
-
220
- if (!result.isValid) {
221
- return res.status(400).json({ error: result.error });
222
- }
223
-
224
- // Process the webhook
225
- console.log('Stripe event:', result.payload.type);
226
- res.json({ received: true });
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 API Route
339
+ ### Next.js App Router
231
340
 
232
341
  ```typescript
233
- // pages/api/webhooks/github.js
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
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
240
- req,
241
- 'github',
242
- process.env.GITHUB_WEBHOOK_SECRET
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
- addEventListener('fetch', event => {
261
- event.respondWith(handleRequest(event.request));
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
- async function handleRequest(request) {
265
- if (request.url.includes('/webhooks/clerk')) {
266
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
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
- // Process Clerk webhook
280
- console.log('Clerk event:', result.payload.type);
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 the test suite:
429
+ ### Run All Tests
334
430
 
335
431
  ```bash
336
432
  npm test
337
433
  ```
338
434
 
339
- Run examples:
435
+ ### Platform-Specific Testing
340
436
 
341
437
  ```bash
342
- npm run examples
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. Create a feature branch
353
- 3. Make your changes
354
- 4. Add tests for new functionality
355
- 5. Submit a pull request
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,2 @@
1
+ export { createWebhookHandler } from './adapters/cloudflare';
2
+ export type { CloudflareWebhookHandlerOptions } from './adapters/cloudflare';
@@ -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; } });
@@ -0,0 +1,2 @@
1
+ export { createWebhookMiddleware } from './adapters/express';
2
+ export type { ExpressWebhookMiddlewareOptions, ExpressLikeRequest, ExpressLikeResponse, } from './adapters/express';
@@ -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;