@hookflo/tern 2.0.0 → 2.0.2
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 +184 -61
- 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/simple.d.ts +4 -0
- package/dist/normalization/simple.js +138 -0
- package/dist/platforms/algorithms.js +14 -0
- package/dist/test.js +98 -2
- package/dist/types.d.ts +73 -3
- package/dist/types.js +1 -0
- package/dist/verifiers/algorithms.js +4 -0
- package/dist/verifiers/custom-algorithms.js +3 -0
- package/package.json +47 -2
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
A robust, algorithm-agnostic webhook verification framework that supports multiple platforms with accurate signature verification and payload retrieval.
|
|
4
4
|
The same framework that secures webhook verification at [Hookflo](https://hookflo.com).
|
|
5
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
|
+
|
|
6
10
|
```bash
|
|
7
11
|
npm install @hookflo/tern
|
|
8
12
|
```
|
|
@@ -24,6 +28,12 @@ Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
|
|
|
24
28
|
- **Flexible Configuration**: Custom signature configurations for any webhook format
|
|
25
29
|
- **Type Safe**: Full TypeScript support with comprehensive type definitions
|
|
26
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.)
|
|
27
37
|
|
|
28
38
|
## Why Tern?
|
|
29
39
|
|
|
@@ -65,6 +75,78 @@ if (result.isValid) {
|
|
|
65
75
|
}
|
|
66
76
|
```
|
|
67
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
|
+
|
|
68
150
|
### Platform-Specific Usage
|
|
69
151
|
|
|
70
152
|
```typescript
|
|
@@ -132,6 +214,7 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
|
|
|
132
214
|
- **Vercel**: HMAC-SHA256
|
|
133
215
|
- **Polar**: HMAC-SHA256
|
|
134
216
|
- **Supabase**: Token-based authentication
|
|
217
|
+
- **GitLab**: Token-based authentication
|
|
135
218
|
|
|
136
219
|
## Custom Platform Configuration
|
|
137
220
|
|
|
@@ -231,80 +314,59 @@ const timestampedConfig = {
|
|
|
231
314
|
|
|
232
315
|
## Framework Integration
|
|
233
316
|
|
|
234
|
-
### Express.js
|
|
317
|
+
### Express.js middleware (body-parser safe)
|
|
235
318
|
|
|
236
319
|
```typescript
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
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
|
+
);
|
|
252
337
|
```
|
|
253
338
|
|
|
254
|
-
### Next.js
|
|
339
|
+
### Next.js App Router
|
|
255
340
|
|
|
256
341
|
```typescript
|
|
257
|
-
|
|
258
|
-
export default async function handler(req, res) {
|
|
259
|
-
if (req.method !== 'POST') {
|
|
260
|
-
return res.status(405).json({ error: 'Method not allowed' });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const result = await WebhookVerificationService.verifyWithPlatformConfig(
|
|
264
|
-
req,
|
|
265
|
-
'github',
|
|
266
|
-
process.env.GITHUB_WEBHOOK_SECRET
|
|
267
|
-
);
|
|
342
|
+
import { createWebhookHandler } from '@hookflo/tern/nextjs';
|
|
268
343
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const event = req.headers['x-github-event'];
|
|
275
|
-
console.log('GitHub event:', event);
|
|
276
|
-
|
|
277
|
-
res.json({ received: true });
|
|
278
|
-
}
|
|
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
|
+
});
|
|
279
349
|
```
|
|
280
350
|
|
|
281
351
|
### Cloudflare Workers
|
|
282
352
|
|
|
283
353
|
```typescript
|
|
284
|
-
|
|
285
|
-
|
|
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 }),
|
|
286
360
|
});
|
|
287
361
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
request,
|
|
292
|
-
'clerk',
|
|
293
|
-
CLERK_WEBHOOK_SECRET
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
if (!result.isValid) {
|
|
297
|
-
return new Response(JSON.stringify({ error: result.error }), {
|
|
298
|
-
status: 400,
|
|
299
|
-
headers: { 'Content-Type': 'application/json' }
|
|
300
|
-
});
|
|
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);
|
|
301
366
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return new Response(JSON.stringify({ received: true }));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
367
|
+
return new Response('Not Found', { status: 404 });
|
|
368
|
+
},
|
|
369
|
+
};
|
|
308
370
|
```
|
|
309
371
|
|
|
310
372
|
## API Reference
|
|
@@ -315,14 +377,22 @@ async function handleRequest(request) {
|
|
|
315
377
|
|
|
316
378
|
Verifies a webhook using the provided configuration.
|
|
317
379
|
|
|
318
|
-
#### `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>`
|
|
381
|
+
|
|
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>`
|
|
319
385
|
|
|
320
|
-
|
|
386
|
+
Auto-detects platform from headers and verifies against one or more provider secrets.
|
|
321
387
|
|
|
322
388
|
#### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
|
|
323
389
|
|
|
324
390
|
Verifies token-based webhooks (like Supabase).
|
|
325
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
|
+
|
|
326
396
|
### Types
|
|
327
397
|
|
|
328
398
|
#### `WebhookVerificationResult`
|
|
@@ -331,6 +401,7 @@ Verifies token-based webhooks (like Supabase).
|
|
|
331
401
|
interface WebhookVerificationResult {
|
|
332
402
|
isValid: boolean;
|
|
333
403
|
error?: string;
|
|
404
|
+
errorCode?: WebhookErrorCode;
|
|
334
405
|
platform: WebhookPlatform;
|
|
335
406
|
payload?: any;
|
|
336
407
|
metadata?: {
|
|
@@ -349,6 +420,7 @@ interface WebhookConfig {
|
|
|
349
420
|
secret: string;
|
|
350
421
|
toleranceInSeconds?: number;
|
|
351
422
|
signatureConfig?: SignatureConfig;
|
|
423
|
+
normalize?: boolean | NormalizeOptions;
|
|
352
424
|
}
|
|
353
425
|
```
|
|
354
426
|
|
|
@@ -422,4 +494,55 @@ MIT License - see [LICENSE](./LICENSE) for details.
|
|
|
422
494
|
|
|
423
495
|
- [Documentation](./USAGE.md)
|
|
424
496
|
- [Framework Summary](./FRAMEWORK_SUMMARY.md)
|
|
497
|
+
- [Architecture Guide](./ARCHITECTURE.md)
|
|
425
498
|
- [Issues](https://github.com/Hookflo/tern/issues)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
## Troubleshooting
|
|
502
|
+
|
|
503
|
+
### `Module not found: Can't resolve "@hookflo/tern/nextjs"`
|
|
504
|
+
|
|
505
|
+
If this happens in a Next.js project, it usually means one of these:
|
|
506
|
+
|
|
507
|
+
1. You installed an older published package version that does not include subpath exports yet.
|
|
508
|
+
2. Lockfile still points to an old tarball/version.
|
|
509
|
+
3. `node_modules` cache is stale after upgrading.
|
|
510
|
+
|
|
511
|
+
Fix steps:
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
# in your Next.js app
|
|
515
|
+
npm i @hookflo/tern@latest
|
|
516
|
+
rm -rf node_modules package-lock.json .next
|
|
517
|
+
npm i
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Then verify resolution:
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
node -e "console.log(require.resolve('@hookflo/tern/nextjs'))"
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
If you are testing this repo locally before publish:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
# inside /workspace/tern
|
|
530
|
+
npm run build
|
|
531
|
+
npm pack
|
|
532
|
+
|
|
533
|
+
# inside your other project
|
|
534
|
+
npm i /path/to/hookflo-tern-<version>.tgz
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Minimal Next.js App Router usage:
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
import { createWebhookHandler } from '@hookflo/tern/nextjs';
|
|
541
|
+
|
|
542
|
+
export const POST = createWebhookHandler({
|
|
543
|
+
platform: 'stripe',
|
|
544
|
+
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
545
|
+
handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
@@ -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;
|