@hookflo/tern 2.0.0 → 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 +134 -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 +22 -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
|
-
);
|
|
268
|
-
|
|
269
|
-
if (!result.isValid) {
|
|
270
|
-
return res.status(400).json({ error: result.error });
|
|
271
|
-
}
|
|
342
|
+
import { createWebhookHandler } from '@hookflo/tern/nextjs';
|
|
272
343
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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.
|
|
319
383
|
|
|
320
|
-
|
|
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.
|
|
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,5 @@ 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)
|
|
@@ -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;
|
package/dist/index.js
CHANGED
|
@@ -14,22 +14,25 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
|
|
17
|
+
exports.getPlatformsByCategory = exports.getPlatformNormalizationCategory = exports.normalizePayload = exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
|
|
18
18
|
const algorithms_1 = require("./verifiers/algorithms");
|
|
19
19
|
const custom_algorithms_1 = require("./verifiers/custom-algorithms");
|
|
20
20
|
const algorithms_2 = require("./platforms/algorithms");
|
|
21
|
+
const simple_1 = require("./normalization/simple");
|
|
21
22
|
class WebhookVerificationService {
|
|
22
23
|
static async verify(request, config) {
|
|
23
24
|
const verifier = this.getVerifier(config);
|
|
24
|
-
const result = await verifier.verify(request);
|
|
25
|
+
const result = await verifier.verify(request.clone());
|
|
25
26
|
// Ensure the platform is set correctly in the result
|
|
26
27
|
if (result.isValid) {
|
|
27
28
|
result.platform = config.platform;
|
|
29
|
+
if (config.normalize) {
|
|
30
|
+
result.payload = (0, simple_1.normalizePayload)(config.platform, result.payload, config.normalize);
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
return result;
|
|
30
34
|
}
|
|
31
35
|
static getVerifier(config) {
|
|
32
|
-
const platform = config.platform.toLowerCase();
|
|
33
36
|
// If a custom signature config is provided, use the new algorithm-based framework
|
|
34
37
|
if (config.signatureConfig) {
|
|
35
38
|
return this.createAlgorithmBasedVerifier(config);
|
|
@@ -50,9 +53,8 @@ class WebhookVerificationService {
|
|
|
50
53
|
return (0, algorithms_1.createAlgorithmVerifier)(secret, signatureConfig, config.platform, toleranceInSeconds);
|
|
51
54
|
}
|
|
52
55
|
static getLegacyVerifier(config) {
|
|
53
|
-
const platform = config.platform.toLowerCase();
|
|
54
56
|
// For legacy support, we'll use the algorithm-based approach
|
|
55
|
-
const platformConfig = (0, algorithms_2.getPlatformAlgorithmConfig)(platform);
|
|
57
|
+
const platformConfig = (0, algorithms_2.getPlatformAlgorithmConfig)(config.platform);
|
|
56
58
|
const configWithSignature = {
|
|
57
59
|
...config,
|
|
58
60
|
signatureConfig: platformConfig.signatureConfig,
|
|
@@ -60,30 +62,72 @@ class WebhookVerificationService {
|
|
|
60
62
|
return this.createAlgorithmBasedVerifier(configWithSignature);
|
|
61
63
|
}
|
|
62
64
|
// New method to create verifier using platform algorithm config
|
|
63
|
-
static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300) {
|
|
65
|
+
static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300, normalize = false) {
|
|
64
66
|
const platformConfig = (0, algorithms_2.getPlatformAlgorithmConfig)(platform);
|
|
65
67
|
const config = {
|
|
66
68
|
platform,
|
|
67
69
|
secret,
|
|
68
70
|
toleranceInSeconds,
|
|
69
71
|
signatureConfig: platformConfig.signatureConfig,
|
|
72
|
+
normalize,
|
|
70
73
|
};
|
|
71
|
-
return
|
|
74
|
+
return this.verify(request, config);
|
|
75
|
+
}
|
|
76
|
+
static async verifyAny(request, secrets, toleranceInSeconds = 300, normalize = false) {
|
|
77
|
+
const requestClone = request.clone();
|
|
78
|
+
const detectedPlatform = this.detectPlatform(requestClone);
|
|
79
|
+
if (detectedPlatform !== 'unknown' && secrets[detectedPlatform]) {
|
|
80
|
+
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds, normalize);
|
|
81
|
+
}
|
|
82
|
+
for (const [platform, secret] of Object.entries(secrets)) {
|
|
83
|
+
if (!secret) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const result = await this.verifyWithPlatformConfig(requestClone, platform.toLowerCase(), secret, toleranceInSeconds, normalize);
|
|
87
|
+
if (result.isValid) {
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
isValid: false,
|
|
93
|
+
error: 'Unable to verify webhook with provided platform secrets',
|
|
94
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
95
|
+
platform: detectedPlatform,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
static detectPlatform(request) {
|
|
99
|
+
const headers = request.headers;
|
|
100
|
+
if (headers.has('stripe-signature'))
|
|
101
|
+
return 'stripe';
|
|
102
|
+
if (headers.has('x-hub-signature-256'))
|
|
103
|
+
return 'github';
|
|
104
|
+
if (headers.has('svix-signature'))
|
|
105
|
+
return 'clerk';
|
|
106
|
+
if (headers.has('webhook-signature'))
|
|
107
|
+
return 'dodopayments';
|
|
108
|
+
if (headers.has('x-gitlab-token'))
|
|
109
|
+
return 'gitlab';
|
|
110
|
+
if (headers.has('x-polar-signature'))
|
|
111
|
+
return 'polar';
|
|
112
|
+
if (headers.has('x-shopify-hmac-sha256'))
|
|
113
|
+
return 'shopify';
|
|
114
|
+
if (headers.has('x-vercel-signature'))
|
|
115
|
+
return 'vercel';
|
|
116
|
+
if (headers.has('x-webhook-token') && headers.has('x-webhook-id'))
|
|
117
|
+
return 'supabase';
|
|
118
|
+
return 'unknown';
|
|
72
119
|
}
|
|
73
120
|
// Helper method to get all platforms using a specific algorithm
|
|
74
121
|
static getPlatformsUsingAlgorithm(algorithm) {
|
|
75
|
-
|
|
76
|
-
return getPlatformsUsingAlgorithm(algorithm);
|
|
122
|
+
return (0, algorithms_2.getPlatformsUsingAlgorithm)(algorithm);
|
|
77
123
|
}
|
|
78
124
|
// Helper method to check if a platform uses a specific algorithm
|
|
79
125
|
static platformUsesAlgorithm(platform, algorithm) {
|
|
80
|
-
|
|
81
|
-
return platformUsesAlgorithm(platform, algorithm);
|
|
126
|
+
return (0, algorithms_2.platformUsesAlgorithm)(platform, algorithm);
|
|
82
127
|
}
|
|
83
128
|
// Helper method to validate signature config
|
|
84
129
|
static validateSignatureConfig(config) {
|
|
85
|
-
|
|
86
|
-
return validateSignatureConfig(config);
|
|
130
|
+
return (0, algorithms_2.validateSignatureConfig)(config);
|
|
87
131
|
}
|
|
88
132
|
// Simple token-based verification for platforms like Supabase
|
|
89
133
|
static async verifyTokenBased(request, webhookId, webhookToken) {
|
|
@@ -94,6 +138,7 @@ class WebhookVerificationService {
|
|
|
94
138
|
return {
|
|
95
139
|
isValid: false,
|
|
96
140
|
error: 'Missing required headers: x-webhook-id and x-webhook-token',
|
|
141
|
+
errorCode: 'MISSING_TOKEN',
|
|
97
142
|
platform: 'custom',
|
|
98
143
|
};
|
|
99
144
|
}
|
|
@@ -103,6 +148,7 @@ class WebhookVerificationService {
|
|
|
103
148
|
return {
|
|
104
149
|
isValid: false,
|
|
105
150
|
error: 'Invalid webhook ID or token',
|
|
151
|
+
errorCode: 'INVALID_TOKEN',
|
|
106
152
|
platform: 'custom',
|
|
107
153
|
};
|
|
108
154
|
}
|
|
@@ -117,7 +163,7 @@ class WebhookVerificationService {
|
|
|
117
163
|
return {
|
|
118
164
|
isValid: true,
|
|
119
165
|
platform: 'custom',
|
|
120
|
-
payload,
|
|
166
|
+
payload: payload,
|
|
121
167
|
metadata: {
|
|
122
168
|
id: idHeader,
|
|
123
169
|
algorithm: 'token-based',
|
|
@@ -128,6 +174,7 @@ class WebhookVerificationService {
|
|
|
128
174
|
return {
|
|
129
175
|
isValid: false,
|
|
130
176
|
error: `Token-based verification error: ${error.message}`,
|
|
177
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
131
178
|
platform: 'custom',
|
|
132
179
|
};
|
|
133
180
|
}
|
|
@@ -144,4 +191,9 @@ var algorithms_4 = require("./verifiers/algorithms");
|
|
|
144
191
|
Object.defineProperty(exports, "createAlgorithmVerifier", { enumerable: true, get: function () { return algorithms_4.createAlgorithmVerifier; } });
|
|
145
192
|
var custom_algorithms_2 = require("./verifiers/custom-algorithms");
|
|
146
193
|
Object.defineProperty(exports, "createCustomVerifier", { enumerable: true, get: function () { return custom_algorithms_2.createCustomVerifier; } });
|
|
194
|
+
var simple_2 = require("./normalization/simple");
|
|
195
|
+
Object.defineProperty(exports, "normalizePayload", { enumerable: true, get: function () { return simple_2.normalizePayload; } });
|
|
196
|
+
Object.defineProperty(exports, "getPlatformNormalizationCategory", { enumerable: true, get: function () { return simple_2.getPlatformNormalizationCategory; } });
|
|
197
|
+
Object.defineProperty(exports, "getPlatformsByCategory", { enumerable: true, get: function () { return simple_2.getPlatformsByCategory; } });
|
|
198
|
+
__exportStar(require("./adapters"), exports);
|
|
147
199
|
exports.default = WebhookVerificationService;
|
package/dist/nextjs.d.ts
ADDED
package/dist/nextjs.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookHandler = void 0;
|
|
4
|
+
var nextjs_1 = require("./adapters/nextjs");
|
|
5
|
+
Object.defineProperty(exports, "createWebhookHandler", { enumerable: true, get: function () { return nextjs_1.createWebhookHandler; } });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AnyNormalizedWebhook, NormalizeOptions, NormalizationCategory, WebhookPlatform } from '../types';
|
|
2
|
+
export declare function getPlatformNormalizationCategory(platform: WebhookPlatform): NormalizationCategory | null;
|
|
3
|
+
export declare function getPlatformsByCategory(category: NormalizationCategory): WebhookPlatform[];
|
|
4
|
+
export declare function normalizePayload(platform: WebhookPlatform, payload: any, normalize?: boolean | NormalizeOptions): AnyNormalizedWebhook | unknown;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPlatformNormalizationCategory = getPlatformNormalizationCategory;
|
|
4
|
+
exports.getPlatformsByCategory = getPlatformsByCategory;
|
|
5
|
+
exports.normalizePayload = normalizePayload;
|
|
6
|
+
function readPath(payload, path) {
|
|
7
|
+
return path.split('.').reduce((acc, key) => {
|
|
8
|
+
if (acc === undefined || acc === null) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return acc[key];
|
|
12
|
+
}, payload);
|
|
13
|
+
}
|
|
14
|
+
const platformNormalizers = {
|
|
15
|
+
stripe: {
|
|
16
|
+
platform: 'stripe',
|
|
17
|
+
category: 'payment',
|
|
18
|
+
normalize: (payload) => ({
|
|
19
|
+
category: 'payment',
|
|
20
|
+
event: readPath(payload, 'type') === 'payment_intent.succeeded'
|
|
21
|
+
? 'payment.succeeded'
|
|
22
|
+
: 'payment.unknown',
|
|
23
|
+
amount: readPath(payload, 'data.object.amount_received')
|
|
24
|
+
?? readPath(payload, 'data.object.amount'),
|
|
25
|
+
currency: String(readPath(payload, 'data.object.currency') ?? '').toUpperCase() || undefined,
|
|
26
|
+
customer_id: readPath(payload, 'data.object.customer'),
|
|
27
|
+
transaction_id: readPath(payload, 'data.object.id'),
|
|
28
|
+
metadata: {},
|
|
29
|
+
occurred_at: new Date().toISOString(),
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
polar: {
|
|
33
|
+
platform: 'polar',
|
|
34
|
+
category: 'payment',
|
|
35
|
+
normalize: (payload) => ({
|
|
36
|
+
category: 'payment',
|
|
37
|
+
event: readPath(payload, 'event') === 'payment.completed'
|
|
38
|
+
? 'payment.succeeded'
|
|
39
|
+
: 'payment.unknown',
|
|
40
|
+
amount: readPath(payload, 'payload.amount_cents'),
|
|
41
|
+
currency: String(readPath(payload, 'payload.currency_code') ?? '').toUpperCase() || undefined,
|
|
42
|
+
customer_id: readPath(payload, 'payload.customer_id'),
|
|
43
|
+
transaction_id: readPath(payload, 'payload.transaction_id'),
|
|
44
|
+
metadata: {},
|
|
45
|
+
occurred_at: new Date().toISOString(),
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
clerk: {
|
|
49
|
+
platform: 'clerk',
|
|
50
|
+
category: 'auth',
|
|
51
|
+
normalize: (payload) => ({
|
|
52
|
+
category: 'auth',
|
|
53
|
+
event: readPath(payload, 'type') || 'auth.unknown',
|
|
54
|
+
user_id: readPath(payload, 'data.id'),
|
|
55
|
+
email: readPath(payload, 'data.email_addresses.0.email_address'),
|
|
56
|
+
metadata: {},
|
|
57
|
+
occurred_at: new Date().toISOString(),
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
supabase: {
|
|
61
|
+
platform: 'supabase',
|
|
62
|
+
category: 'auth',
|
|
63
|
+
normalize: (payload) => ({
|
|
64
|
+
category: 'auth',
|
|
65
|
+
event: readPath(payload, 'type') || readPath(payload, 'event') || 'auth.unknown',
|
|
66
|
+
user_id: readPath(payload, 'record.id') || readPath(payload, 'id'),
|
|
67
|
+
email: readPath(payload, 'record.email') || readPath(payload, 'email'),
|
|
68
|
+
metadata: {},
|
|
69
|
+
occurred_at: new Date().toISOString(),
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
vercel: {
|
|
73
|
+
platform: 'vercel',
|
|
74
|
+
category: 'infrastructure',
|
|
75
|
+
normalize: (payload) => ({
|
|
76
|
+
category: 'infrastructure',
|
|
77
|
+
event: readPath(payload, 'type') || 'deployment.unknown',
|
|
78
|
+
project_id: readPath(payload, 'payload.project.id'),
|
|
79
|
+
deployment_id: readPath(payload, 'payload.deployment.id'),
|
|
80
|
+
status: 'unknown',
|
|
81
|
+
metadata: {},
|
|
82
|
+
occurred_at: new Date().toISOString(),
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
function getPlatformNormalizationCategory(platform) {
|
|
87
|
+
return platformNormalizers[platform]?.category || null;
|
|
88
|
+
}
|
|
89
|
+
function getPlatformsByCategory(category) {
|
|
90
|
+
return Object.values(platformNormalizers)
|
|
91
|
+
.filter((spec) => !!spec)
|
|
92
|
+
.filter((spec) => spec.category === category)
|
|
93
|
+
.map((spec) => spec.platform);
|
|
94
|
+
}
|
|
95
|
+
function resolveNormalizeOptions(normalize) {
|
|
96
|
+
if (typeof normalize === 'boolean') {
|
|
97
|
+
return {
|
|
98
|
+
enabled: normalize,
|
|
99
|
+
category: undefined,
|
|
100
|
+
includeRaw: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
enabled: normalize?.enabled ?? true,
|
|
105
|
+
category: normalize?.category,
|
|
106
|
+
includeRaw: normalize?.includeRaw ?? true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function buildUnknownNormalizedPayload(platform, payload, category, includeRaw, warning) {
|
|
110
|
+
return {
|
|
111
|
+
category: category || 'infrastructure',
|
|
112
|
+
event: payload?.type ?? payload?.event ?? 'unknown',
|
|
113
|
+
_platform: platform,
|
|
114
|
+
_raw: includeRaw ? payload : undefined,
|
|
115
|
+
warning,
|
|
116
|
+
occurred_at: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizePayload(platform, payload, normalize) {
|
|
120
|
+
const options = resolveNormalizeOptions(normalize);
|
|
121
|
+
if (!options.enabled) {
|
|
122
|
+
return payload;
|
|
123
|
+
}
|
|
124
|
+
const spec = platformNormalizers[platform];
|
|
125
|
+
const inferredCategory = spec?.category;
|
|
126
|
+
if (!spec) {
|
|
127
|
+
return buildUnknownNormalizedPayload(platform, payload, options.category, options.includeRaw);
|
|
128
|
+
}
|
|
129
|
+
if (options.category && options.category !== inferredCategory) {
|
|
130
|
+
return buildUnknownNormalizedPayload(platform, payload, inferredCategory, options.includeRaw, `Requested normalization category '${options.category}' does not match platform category '${inferredCategory}'`);
|
|
131
|
+
}
|
|
132
|
+
const normalized = spec.normalize(payload);
|
|
133
|
+
return {
|
|
134
|
+
...normalized,
|
|
135
|
+
_platform: platform,
|
|
136
|
+
_raw: options.includeRaw ? payload : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -117,6 +117,20 @@ exports.platformAlgorithmConfigs = {
|
|
|
117
117
|
},
|
|
118
118
|
description: 'Supabase webhooks use token-based authentication',
|
|
119
119
|
},
|
|
120
|
+
gitlab: {
|
|
121
|
+
platform: 'gitlab',
|
|
122
|
+
signatureConfig: {
|
|
123
|
+
algorithm: 'custom',
|
|
124
|
+
headerName: 'X-Gitlab-Token',
|
|
125
|
+
headerFormat: 'raw',
|
|
126
|
+
payloadFormat: 'raw',
|
|
127
|
+
customConfig: {
|
|
128
|
+
type: 'token-based',
|
|
129
|
+
idHeader: 'X-Gitlab-Token',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
description: 'GitLab webhooks use HMAC-SHA256 with X-Gitlab-Token header',
|
|
133
|
+
},
|
|
120
134
|
custom: {
|
|
121
135
|
platform: 'custom',
|
|
122
136
|
signatureConfig: {
|
package/dist/test.js
CHANGED
|
@@ -24,6 +24,10 @@ function createGitHubSignature(body, secret) {
|
|
|
24
24
|
hmac.update(body);
|
|
25
25
|
return `sha256=${hmac.digest('hex')}`;
|
|
26
26
|
}
|
|
27
|
+
function createGitLabSignature(body, secret) {
|
|
28
|
+
// GitLab just compares the token in X-Gitlab-Token header
|
|
29
|
+
return secret;
|
|
30
|
+
}
|
|
27
31
|
function createClerkSignature(body, secret, id, timestamp) {
|
|
28
32
|
const signedContent = `${id}.${timestamp}.${body}`;
|
|
29
33
|
const secretBytes = new Uint8Array(Buffer.from(secret.split('_')[1], 'base64'));
|
|
@@ -170,7 +174,9 @@ async function runTests() {
|
|
|
170
174
|
'content-type': 'application/json',
|
|
171
175
|
});
|
|
172
176
|
const invalidResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(invalidRequest, 'stripe', testSecret);
|
|
173
|
-
|
|
177
|
+
const invalidSigPassed = !invalidResult.isValid && (invalidResult.errorCode === 'INVALID_SIGNATURE'
|
|
178
|
+
|| invalidResult.errorCode === 'TIMESTAMP_EXPIRED');
|
|
179
|
+
console.log(' ✅ Invalid signature correctly rejected:', invalidSigPassed ? 'PASSED' : 'FAILED');
|
|
174
180
|
if (invalidResult.isValid) {
|
|
175
181
|
console.log(' ❌ Should have been rejected');
|
|
176
182
|
}
|
|
@@ -185,7 +191,8 @@ async function runTests() {
|
|
|
185
191
|
'content-type': 'application/json',
|
|
186
192
|
});
|
|
187
193
|
const missingHeaderResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(missingHeaderRequest, 'stripe', testSecret);
|
|
188
|
-
|
|
194
|
+
const missingHeaderPassed = !missingHeaderResult.isValid && missingHeaderResult.errorCode === 'MISSING_SIGNATURE';
|
|
195
|
+
console.log(' ✅ Missing headers correctly rejected:', missingHeaderPassed ? 'PASSED' : 'FAILED');
|
|
189
196
|
if (missingHeaderResult.isValid) {
|
|
190
197
|
console.log(' ❌ Should have been rejected');
|
|
191
198
|
}
|
|
@@ -193,6 +200,95 @@ async function runTests() {
|
|
|
193
200
|
catch (error) {
|
|
194
201
|
console.log(' ❌ Missing headers test failed:', error);
|
|
195
202
|
}
|
|
203
|
+
// Test 8: GitLab Webhook
|
|
204
|
+
console.log('\n8. Testing GitLab Webhook...');
|
|
205
|
+
try {
|
|
206
|
+
const gitlabSecret = testSecret;
|
|
207
|
+
const gitlabRequest = createMockRequest({
|
|
208
|
+
'X-Gitlab-Token': gitlabSecret,
|
|
209
|
+
'content-type': 'application/json',
|
|
210
|
+
});
|
|
211
|
+
const gitlabResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(gitlabRequest, 'gitlab', gitlabSecret);
|
|
212
|
+
console.log(' ✅ GitLab:', gitlabResult.isValid ? 'PASSED' : 'FAILED');
|
|
213
|
+
if (!gitlabResult.isValid) {
|
|
214
|
+
console.log(' ❌ Error:', gitlabResult.error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.log(' ❌ GitLab test failed:', error);
|
|
219
|
+
}
|
|
220
|
+
// Test 9: GitLab Invalid Token
|
|
221
|
+
console.log('\n9. Testing GitLab Invalid Token...');
|
|
222
|
+
try {
|
|
223
|
+
const gitlabRequest = createMockRequest({
|
|
224
|
+
'X-Gitlab-Token': 'wrong_secret',
|
|
225
|
+
'content-type': 'application/json',
|
|
226
|
+
});
|
|
227
|
+
const gitlabResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(gitlabRequest, 'gitlab', testSecret);
|
|
228
|
+
console.log(' ✅ Invalid token correctly rejected:', !gitlabResult.isValid ? 'PASSED' : 'FAILED');
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.log(' ❌ GitLab invalid token test failed:', error);
|
|
232
|
+
}
|
|
233
|
+
// Test 10: verifyAny should auto-detect Stripe
|
|
234
|
+
console.log('\n10. Testing verifyAny auto-detection...');
|
|
235
|
+
try {
|
|
236
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
237
|
+
const stripeSignature = createStripeSignature(testBody, testSecret, timestamp);
|
|
238
|
+
const request = createMockRequest({
|
|
239
|
+
'stripe-signature': stripeSignature,
|
|
240
|
+
'content-type': 'application/json',
|
|
241
|
+
});
|
|
242
|
+
const result = await index_1.WebhookVerificationService.verifyAny(request, {
|
|
243
|
+
stripe: testSecret,
|
|
244
|
+
github: 'wrong-secret',
|
|
245
|
+
});
|
|
246
|
+
console.log(' ✅ verifyAny:', result.isValid && result.platform === 'stripe' ? 'PASSED' : 'FAILED');
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.log(' ❌ verifyAny test failed:', error);
|
|
250
|
+
}
|
|
251
|
+
// Test 11: Normalization for Stripe
|
|
252
|
+
console.log('\n11. Testing payload normalization...');
|
|
253
|
+
try {
|
|
254
|
+
const normalizedStripeBody = JSON.stringify({
|
|
255
|
+
type: 'payment_intent.succeeded',
|
|
256
|
+
data: {
|
|
257
|
+
object: {
|
|
258
|
+
id: 'pi_123',
|
|
259
|
+
amount: 5000,
|
|
260
|
+
currency: 'usd',
|
|
261
|
+
customer: 'cus_456',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
266
|
+
const stripeSignature = createStripeSignature(normalizedStripeBody, testSecret, timestamp);
|
|
267
|
+
const request = createMockRequest({
|
|
268
|
+
'stripe-signature': stripeSignature,
|
|
269
|
+
'content-type': 'application/json',
|
|
270
|
+
}, normalizedStripeBody);
|
|
271
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'stripe', testSecret, 300, true);
|
|
272
|
+
const payload = result.payload;
|
|
273
|
+
const passed = result.isValid
|
|
274
|
+
&& payload.event === 'payment.succeeded'
|
|
275
|
+
&& payload.currency === 'USD'
|
|
276
|
+
&& payload.transaction_id === 'pi_123';
|
|
277
|
+
console.log(' ✅ Normalization:', passed ? 'PASSED' : 'FAILED');
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.log(' ❌ Normalization test failed:', error);
|
|
281
|
+
}
|
|
282
|
+
// Test 12: Category-aware normalization registry
|
|
283
|
+
console.log('\n12. Testing category-based platform registry...');
|
|
284
|
+
try {
|
|
285
|
+
const paymentPlatforms = (0, index_1.getPlatformsByCategory)('payment');
|
|
286
|
+
const hasStripeAndPolar = paymentPlatforms.includes('stripe') && paymentPlatforms.includes('polar');
|
|
287
|
+
console.log(' ✅ Category registry:', hasStripeAndPolar ? 'PASSED' : 'FAILED');
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
console.log(' ❌ Category registry test failed:', error);
|
|
291
|
+
}
|
|
196
292
|
console.log('\n🎉 All tests completed!');
|
|
197
293
|
}
|
|
198
294
|
// Run tests if this file is executed directly
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WebhookPlatform = 'custom' | 'clerk' | 'supabase' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'unknown';
|
|
1
|
+
export type WebhookPlatform = 'custom' | 'clerk' | 'supabase' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'unknown';
|
|
2
2
|
export declare enum WebhookPlatformKeys {
|
|
3
3
|
GitHub = "github",
|
|
4
4
|
Stripe = "stripe",
|
|
@@ -8,6 +8,7 @@ export declare enum WebhookPlatformKeys {
|
|
|
8
8
|
Vercel = "vercel",
|
|
9
9
|
Polar = "polar",
|
|
10
10
|
Supabase = "supabase",
|
|
11
|
+
GitLab = "gitlab",
|
|
11
12
|
Custom = "custom",
|
|
12
13
|
Unknown = "unknown"
|
|
13
14
|
}
|
|
@@ -22,11 +23,76 @@ export interface SignatureConfig {
|
|
|
22
23
|
payloadFormat?: 'raw' | 'timestamped' | 'custom';
|
|
23
24
|
customConfig?: Record<string, any>;
|
|
24
25
|
}
|
|
25
|
-
export
|
|
26
|
+
export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
|
|
27
|
+
export type NormalizationCategory = 'payment' | 'auth' | 'ecommerce' | 'infrastructure';
|
|
28
|
+
export interface BaseNormalizedWebhook {
|
|
29
|
+
category: NormalizationCategory;
|
|
30
|
+
event: string;
|
|
31
|
+
_platform: WebhookPlatform | string;
|
|
32
|
+
_raw: unknown;
|
|
33
|
+
occurred_at?: string;
|
|
34
|
+
}
|
|
35
|
+
export type PaymentWebhookEvent = 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'subscription.created' | 'subscription.cancelled' | 'payment.unknown';
|
|
36
|
+
export interface PaymentWebhookNormalized extends BaseNormalizedWebhook {
|
|
37
|
+
category: 'payment';
|
|
38
|
+
event: PaymentWebhookEvent;
|
|
39
|
+
amount?: number;
|
|
40
|
+
currency?: string;
|
|
41
|
+
customer_id?: string;
|
|
42
|
+
transaction_id?: string;
|
|
43
|
+
subscription_id?: string;
|
|
44
|
+
refund_amount?: number;
|
|
45
|
+
failure_reason?: string;
|
|
46
|
+
metadata?: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
export type AuthWebhookEvent = 'user.created' | 'user.updated' | 'user.deleted' | 'session.started' | 'session.ended' | 'auth.unknown';
|
|
49
|
+
export interface AuthWebhookNormalized extends BaseNormalizedWebhook {
|
|
50
|
+
category: 'auth';
|
|
51
|
+
event: AuthWebhookEvent;
|
|
52
|
+
user_id?: string;
|
|
53
|
+
email?: string;
|
|
54
|
+
phone?: string;
|
|
55
|
+
metadata?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
export interface EcommerceWebhookNormalized extends BaseNormalizedWebhook {
|
|
58
|
+
category: 'ecommerce';
|
|
59
|
+
event: string;
|
|
60
|
+
order_id?: string;
|
|
61
|
+
customer_id?: string;
|
|
62
|
+
amount?: number;
|
|
63
|
+
currency?: string;
|
|
64
|
+
metadata?: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
export interface InfrastructureWebhookNormalized extends BaseNormalizedWebhook {
|
|
67
|
+
category: 'infrastructure';
|
|
68
|
+
event: string;
|
|
69
|
+
project_id?: string;
|
|
70
|
+
deployment_id?: string;
|
|
71
|
+
status?: 'queued' | 'building' | 'ready' | 'error' | 'unknown';
|
|
72
|
+
metadata?: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
export interface UnknownNormalizedWebhook extends BaseNormalizedWebhook {
|
|
75
|
+
event: string;
|
|
76
|
+
warning?: string;
|
|
77
|
+
}
|
|
78
|
+
export type NormalizedPayloadByCategory = {
|
|
79
|
+
payment: PaymentWebhookNormalized;
|
|
80
|
+
auth: AuthWebhookNormalized;
|
|
81
|
+
ecommerce: EcommerceWebhookNormalized;
|
|
82
|
+
infrastructure: InfrastructureWebhookNormalized;
|
|
83
|
+
};
|
|
84
|
+
export type AnyNormalizedWebhook = PaymentWebhookNormalized | AuthWebhookNormalized | EcommerceWebhookNormalized | InfrastructureWebhookNormalized | UnknownNormalizedWebhook;
|
|
85
|
+
export interface NormalizeOptions {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
category?: NormalizationCategory;
|
|
88
|
+
includeRaw?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface WebhookVerificationResult<TPayload = unknown> {
|
|
26
91
|
isValid: boolean;
|
|
27
92
|
error?: string;
|
|
93
|
+
errorCode?: WebhookErrorCode;
|
|
28
94
|
platform: WebhookPlatform;
|
|
29
|
-
payload?:
|
|
95
|
+
payload?: TPayload;
|
|
30
96
|
metadata?: {
|
|
31
97
|
timestamp?: string;
|
|
32
98
|
id?: string | null;
|
|
@@ -38,6 +104,10 @@ export interface WebhookConfig {
|
|
|
38
104
|
secret: string;
|
|
39
105
|
toleranceInSeconds?: number;
|
|
40
106
|
signatureConfig?: SignatureConfig;
|
|
107
|
+
normalize?: boolean | NormalizeOptions;
|
|
108
|
+
}
|
|
109
|
+
export interface MultiPlatformSecrets {
|
|
110
|
+
[platform: string]: string | undefined;
|
|
41
111
|
}
|
|
42
112
|
export interface PlatformAlgorithmConfig {
|
|
43
113
|
platform: WebhookPlatform;
|
package/dist/types.js
CHANGED
|
@@ -11,6 +11,7 @@ var WebhookPlatformKeys;
|
|
|
11
11
|
WebhookPlatformKeys["Vercel"] = "vercel";
|
|
12
12
|
WebhookPlatformKeys["Polar"] = "polar";
|
|
13
13
|
WebhookPlatformKeys["Supabase"] = "supabase";
|
|
14
|
+
WebhookPlatformKeys["GitLab"] = "gitlab";
|
|
14
15
|
WebhookPlatformKeys["Custom"] = "custom";
|
|
15
16
|
WebhookPlatformKeys["Unknown"] = "unknown";
|
|
16
17
|
})(WebhookPlatformKeys || (exports.WebhookPlatformKeys = WebhookPlatformKeys = {}));
|
|
@@ -185,6 +185,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
185
185
|
return {
|
|
186
186
|
isValid: false,
|
|
187
187
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
188
|
+
errorCode: 'MISSING_SIGNATURE',
|
|
188
189
|
platform: this.platform,
|
|
189
190
|
};
|
|
190
191
|
}
|
|
@@ -204,6 +205,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
204
205
|
return {
|
|
205
206
|
isValid: false,
|
|
206
207
|
error: 'Webhook timestamp expired',
|
|
208
|
+
errorCode: 'TIMESTAMP_EXPIRED',
|
|
207
209
|
platform: this.platform,
|
|
208
210
|
};
|
|
209
211
|
}
|
|
@@ -228,6 +230,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
228
230
|
return {
|
|
229
231
|
isValid: false,
|
|
230
232
|
error: 'Invalid signature',
|
|
233
|
+
errorCode: 'INVALID_SIGNATURE',
|
|
231
234
|
platform: this.platform,
|
|
232
235
|
};
|
|
233
236
|
}
|
|
@@ -252,6 +255,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
252
255
|
return {
|
|
253
256
|
isValid: false,
|
|
254
257
|
error: `${this.platform} verification error: ${error.message}`,
|
|
258
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
255
259
|
platform: this.platform,
|
|
256
260
|
};
|
|
257
261
|
}
|
|
@@ -17,6 +17,7 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
17
17
|
return {
|
|
18
18
|
isValid: false,
|
|
19
19
|
error: `Missing token header: ${this.config.headerName}`,
|
|
20
|
+
errorCode: 'MISSING_TOKEN',
|
|
20
21
|
platform: 'custom',
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -26,6 +27,7 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
26
27
|
return {
|
|
27
28
|
isValid: false,
|
|
28
29
|
error: 'Invalid token',
|
|
30
|
+
errorCode: 'INVALID_TOKEN',
|
|
29
31
|
platform: 'custom',
|
|
30
32
|
};
|
|
31
33
|
}
|
|
@@ -51,6 +53,7 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
|
|
|
51
53
|
return {
|
|
52
54
|
isValid: false,
|
|
53
55
|
error: `Token-based verification error: ${error.message}`,
|
|
56
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
54
57
|
platform: 'custom',
|
|
55
58
|
};
|
|
56
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hookflo/tern",
|
|
3
|
-
"version": "2.0.0",
|
|
3
|
+
"version": "2.0.2-experimental.0",
|
|
4
4
|
"description": "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"release:patch": "npm run build && npm version patch && npm publish",
|
|
16
16
|
"release:minor": "npm run build && npm version minor && npm publish",
|
|
17
17
|
"release:major": "npm run build && npm version major && npm publish",
|
|
18
|
-
"clean": "
|
|
18
|
+
"clean": "rimraf dist",
|
|
19
19
|
"prebuild": "npm run clean"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
"eslint-plugin-import": "^2.32.0",
|
|
56
56
|
"jest": "29.5.0",
|
|
57
57
|
"prettier": "3.0.0",
|
|
58
|
+
"rimraf": "^6.0.1",
|
|
58
59
|
"ts-jest": "29.1.0",
|
|
60
|
+
"ts-node": "^10.9.2",
|
|
59
61
|
"typescript": "^5.0.0"
|
|
60
62
|
},
|
|
61
63
|
"engines": {
|
|
@@ -68,5 +70,23 @@
|
|
|
68
70
|
],
|
|
69
71
|
"publishConfig": {
|
|
70
72
|
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"exports": {
|
|
75
|
+
".": {
|
|
76
|
+
"require": "./dist/index.js",
|
|
77
|
+
"types": "./dist/index.d.ts"
|
|
78
|
+
},
|
|
79
|
+
"./express": {
|
|
80
|
+
"require": "./dist/express.js",
|
|
81
|
+
"types": "./dist/express.d.ts"
|
|
82
|
+
},
|
|
83
|
+
"./nextjs": {
|
|
84
|
+
"require": "./dist/nextjs.js",
|
|
85
|
+
"types": "./dist/nextjs.d.ts"
|
|
86
|
+
},
|
|
87
|
+
"./cloudflare": {
|
|
88
|
+
"require": "./dist/cloudflare.js",
|
|
89
|
+
"types": "./dist/cloudflare.d.ts"
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
92
|
}
|