@hookflo/tern 1.0.1 โ†’ 1.0.3

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 CHANGED
@@ -1,20 +1,38 @@
1
- # Tern
1
+ # Tern - Algorithm Agnostic Webhook Verification Framework
2
2
 
3
- A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms. Built with TypeScript for maximum type safety and developer experience.
3
+ A robust, algorithm-agnostic webhook verification framework that supports multiple platforms with accurate signature verification and payload retrieval.
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/@hookflo/tern)](https://www.npmjs.com/package/@hookflo/tern)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ Tern is a zero-dependency TypeScript framework for robust webhook verification across multiple platforms and algorithms.
10
+
11
+ <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" />
5
12
  ## Features
6
13
 
7
- - **Algorithm-based verification**: Instead of platform-specific verifiers, use algorithm-based verifiers
8
- - **Platform configuration**: Each platform specifies which algorithm to use
9
- - **Extensible framework**: Easy to add new algorithms and platforms
10
- - **Most common algorithms**: HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
11
- - **TypeScript support**: Full type safety and IntelliSense
12
- - **Zero dependencies**: Only uses Node.js built-in modules
14
+ - **Algorithm Agnostic**: Decouples platform logic from signature verification โ€” verify based on cryptographic algorithm, not hardcoded platform rules.
15
+ Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
16
+
17
+ - **Platform Specific**: Accurate implementations for Stripe, GitHub, Clerk, and other platforms
18
+ - **Flexible Configuration**: Custom signature configurations for any webhook format
19
+ - **Type Safe**: Full TypeScript support with comprehensive type definitions
20
+ - **Framework Agnostic**: Works with Express.js, Next.js, Cloudflare Workers, and more
21
+
22
+ ## Why Tern?
23
+
24
+ Most webhook verifiers are tightly coupled to specific platforms or hardcoded logic. Tern introduces a flexible, scalable, algorithm-first approach that:
25
+
26
+ - Works across all major platforms
27
+ - Supports custom signing logic
28
+ - Keeps your code clean and modular
29
+ - Avoids unnecessary dependencies
30
+ - Is written in strict, modern TypeScript
13
31
 
14
32
  ## Installation
15
33
 
16
34
  ```bash
17
- npm install tern
35
+ npm install @hookflo/tern
18
36
  ```
19
37
 
20
38
  ## Quick Start
@@ -22,375 +40,265 @@ npm install tern
22
40
  ### Basic Usage
23
41
 
24
42
  ```typescript
25
- import { WebhookVerificationService } from 'tern';
43
+ import { WebhookVerificationService } from '@hookflo/tern';
26
44
 
27
- // Verify a GitHub webhook
45
+ // Verify a Stripe webhook
28
46
  const result = await WebhookVerificationService.verifyWithPlatformConfig(
29
47
  request,
30
- 'github',
31
- 'your-github-secret',
32
- 300
48
+ 'stripe',
49
+ 'whsec_your_stripe_webhook_secret'
33
50
  );
34
51
 
35
52
  if (result.isValid) {
36
- console.log('Webhook verified successfully:', result.payload);
53
+ console.log('Webhook verified!', result.payload);
37
54
  } else {
38
- console.error('Verification failed:', result.error);
55
+ console.log('Verification failed:', result.error);
39
56
  }
40
57
  ```
41
58
 
42
- ### Token-based Authentication (Supabase, Custom)
59
+ ### Platform-Specific Configurations
43
60
 
44
61
  ```typescript
45
- // For platforms that use simple token-based auth
46
- const result = await WebhookVerificationService.verifyTokenBased(
47
- request,
48
- 'your-webhook-id',
49
- 'your-webhook-token'
50
- );
51
- ```
62
+ import { WebhookVerificationService } from '@hookflo/tern';
52
63
 
53
- ### Custom Signature Configuration
64
+ // Stripe webhook
65
+ const stripeConfig = {
66
+ platform: 'stripe',
67
+ secret: 'whsec_your_stripe_webhook_secret',
68
+ toleranceInSeconds: 300,
69
+ };
54
70
 
55
- ```typescript
56
- import { WebhookVerificationService } from 'tern';
71
+ // GitHub webhook
72
+ const githubConfig = {
73
+ platform: 'github',
74
+ secret: 'your_github_webhook_secret',
75
+ toleranceInSeconds: 300,
76
+ };
57
77
 
58
- const config = {
59
- platform: 'custom',
60
- secret: 'your-secret',
61
- signatureConfig: {
62
- algorithm: 'hmac-sha256',
63
- headerName: 'x-signature',
64
- headerFormat: 'prefixed',
65
- prefix: 'sha256=',
66
- payloadFormat: 'raw'
67
- }
78
+ // Clerk webhook
79
+ const clerkConfig = {
80
+ platform: 'clerk',
81
+ secret: 'whsec_your_clerk_webhook_secret',
82
+ toleranceInSeconds: 300,
68
83
  };
69
84
 
70
- const result = await WebhookVerificationService.verify(request, config);
85
+ const result = await WebhookVerificationService.verify(request, stripeConfig);
71
86
  ```
72
87
 
73
- ## ๐Ÿ—๏ธ Supported Platforms
88
+ ## Supported Platforms
74
89
 
75
- ### HMAC-SHA256 (Most Common)
76
- - **GitHub**: `x-hub-signature-256` with `sha256=` prefix
77
- - **Stripe**: `stripe-signature` with comma-separated format
78
- - **Clerk**: `svix-signature` with base64 encoding
79
- - **Dodo Payments**: `webhook-signature` with raw format
80
- - **Shopify**: `x-shopify-hmac-sha256`
81
- - **Vercel**: `x-vercel-signature`
82
- - **Polar**: `x-polar-signature`
90
+ ### Stripe
91
+ - **Signature Format**: `t={timestamp},v1={signature}`
92
+ - **Algorithm**: HMAC-SHA256
93
+ - **Payload Format**: `{timestamp}.{body}`
83
94
 
84
- ### Custom Algorithms
85
- - **Supabase**: Token-based authentication
86
- - **Clerk**: Custom base64 encoding
87
- - **Stripe**: Custom comma-separated format
95
+ ### GitHub
96
+ - **Signature Format**: `sha256={signature}`
97
+ - **Algorithm**: HMAC-SHA256
98
+ - **Payload Format**: Raw body
88
99
 
89
- ## ๐Ÿ”ง API Reference
100
+ ### Clerk
101
+ - **Signature Format**: `v1,{signature}` (space-separated)
102
+ - **Algorithm**: HMAC-SHA256 with base64 encoding
103
+ - **Payload Format**: `{id}.{timestamp}.{body}`
90
104
 
91
- ### WebhookVerificationService
105
+ ### Other Platforms
106
+ - **Dodo Payments**: HMAC-SHA256
107
+ - **Shopify**: HMAC-SHA256
108
+ - **Vercel**: HMAC-SHA256
109
+ - **Polar**: HMAC-SHA256
110
+ - **Supabase**: Token-based authentication
92
111
 
93
- #### `verify(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult>`
112
+ ## Custom Configurations
94
113
 
95
- Verify a webhook using a configuration object.
114
+ ### Custom HMAC-SHA256
96
115
 
97
116
  ```typescript
98
- const config = {
99
- platform: 'github',
100
- secret: 'your-secret',
101
- toleranceInSeconds: 300
117
+ const customConfig = {
118
+ platform: 'custom',
119
+ secret: 'your_custom_secret',
120
+ signatureConfig: {
121
+ algorithm: 'hmac-sha256',
122
+ headerName: 'x-custom-signature',
123
+ headerFormat: 'prefixed',
124
+ prefix: 'sha256=',
125
+ payloadFormat: 'raw',
126
+ },
102
127
  };
103
-
104
- const result = await WebhookVerificationService.verify(request, config);
105
- ```
106
-
107
- #### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult>`
108
-
109
- Verify a webhook using platform-specific configuration.
110
-
111
- ```typescript
112
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
113
- request,
114
- 'stripe',
115
- 'your-stripe-secret',
116
- 300
117
- );
118
128
  ```
119
129
 
120
- #### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
121
-
122
- Verify a webhook using simple token-based authentication.
130
+ ### Custom Timestamped Payload
123
131
 
124
132
  ```typescript
125
- const result = await WebhookVerificationService.verifyTokenBased(
126
- request,
127
- 'your-webhook-id',
128
- 'your-webhook-token'
129
- );
133
+ const timestampedConfig = {
134
+ platform: 'custom',
135
+ secret: 'your_custom_secret',
136
+ signatureConfig: {
137
+ algorithm: 'hmac-sha256',
138
+ headerName: 'x-webhook-signature',
139
+ headerFormat: 'raw',
140
+ timestampHeader: 'x-webhook-timestamp',
141
+ timestampFormat: 'unix',
142
+ payloadFormat: 'timestamped',
143
+ },
144
+ };
130
145
  ```
131
146
 
132
- ### Utility Functions
133
-
134
- #### `detectPlatformFromHeaders(headers: Headers): WebhookPlatform | null`
147
+ ## Framework Integration
135
148
 
136
- Automatically detect the platform from request headers.
149
+ ### Express.js
137
150
 
138
151
  ```typescript
139
- import { detectPlatformFromHeaders } from 'tern';
140
-
141
- const platform = detectPlatformFromHeaders(request.headers);
142
- if (platform) {
152
+ app.post('/webhooks/stripe', async (req, res) => {
143
153
  const result = await WebhookVerificationService.verifyWithPlatformConfig(
144
- request, platform, 'your-secret', 300
154
+ req,
155
+ 'stripe',
156
+ process.env.STRIPE_WEBHOOK_SECRET
145
157
  );
146
- }
147
- ```
148
-
149
- #### `getPlatformsUsingAlgorithm(algorithm: string): WebhookPlatform[]`
150
-
151
- Get all platforms that use a specific algorithm.
152
-
153
- ```typescript
154
- import { WebhookVerificationService } from 'tern';
155
-
156
- const hmacPlatforms = WebhookVerificationService.getPlatformsUsingAlgorithm('hmac-sha256');
157
- // Returns: ['github', 'stripe', 'clerk', 'dodopayments', ...]
158
- ```
159
-
160
- ## ๐ŸŽฏ Usage Examples
161
-
162
- ### Express.js Integration
163
-
164
- ```typescript
165
- import express from 'express';
166
- import { WebhookVerificationService } from 'tern';
167
-
168
- const app = express();
169
-
170
- app.post('/webhook', async (req, res) => {
171
- try {
172
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
173
- req,
174
- 'github',
175
- process.env.GITHUB_WEBHOOK_SECRET,
176
- 300
177
- );
178
158
 
179
- if (result.isValid) {
180
- // Process the webhook
181
- console.log('Webhook received:', result.payload);
182
- res.status(200).json({ success: true });
183
- } else {
184
- res.status(401).json({ error: result.error });
185
- }
186
- } catch (error) {
187
- res.status(500).json({ error: 'Internal server error' });
159
+ if (!result.isValid) {
160
+ return res.status(400).json({ error: result.error });
188
161
  }
162
+
163
+ // Process the webhook
164
+ console.log('Stripe event:', result.payload.type);
165
+ res.json({ received: true });
189
166
  });
190
167
  ```
191
168
 
192
169
  ### Next.js API Route
193
170
 
194
171
  ```typescript
195
- // pages/api/webhook.ts
196
- import { NextApiRequest, NextApiResponse } from 'next';
197
- import { WebhookVerificationService } from 'tern';
198
-
199
- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
172
+ // pages/api/webhooks/github.js
173
+ export default async function handler(req, res) {
200
174
  if (req.method !== 'POST') {
201
175
  return res.status(405).json({ error: 'Method not allowed' });
202
176
  }
203
177
 
204
- try {
205
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
206
- req as any,
207
- 'stripe',
208
- process.env.STRIPE_WEBHOOK_SECRET,
209
- 300
210
- );
178
+ const result = await WebhookVerificationService.verifyWithPlatformConfig(
179
+ req,
180
+ 'github',
181
+ process.env.GITHUB_WEBHOOK_SECRET
182
+ );
211
183
 
212
- if (result.isValid) {
213
- // Handle the webhook
214
- console.log('Stripe webhook:', result.payload);
215
- res.status(200).json({ received: true });
216
- } else {
217
- res.status(401).json({ error: result.error });
218
- }
219
- } catch (error) {
220
- res.status(500).json({ error: 'Internal server error' });
184
+ if (!result.isValid) {
185
+ return res.status(400).json({ error: result.error });
221
186
  }
187
+
188
+ // Handle GitHub webhook
189
+ const event = req.headers['x-github-event'];
190
+ console.log('GitHub event:', event);
191
+
192
+ res.json({ received: true });
222
193
  }
223
194
  ```
224
195
 
225
- ### Platform Detection
196
+ ### Cloudflare Workers
226
197
 
227
198
  ```typescript
228
- import { detectPlatformFromHeaders, WebhookVerificationService } from 'tern';
199
+ addEventListener('fetch', event => {
200
+ event.respondWith(handleRequest(event.request));
201
+ });
229
202
 
230
- async function handleWebhook(request: Request) {
231
- const platform = detectPlatformFromHeaders(request.headers);
232
-
233
- if (!platform) {
234
- throw new Error('Unknown webhook platform');
235
- }
203
+ async function handleRequest(request) {
204
+ if (request.url.includes('/webhooks/clerk')) {
205
+ const result = await WebhookVerificationService.verifyWithPlatformConfig(
206
+ request,
207
+ 'clerk',
208
+ CLERK_WEBHOOK_SECRET
209
+ );
236
210
 
237
- const secret = getSecretForPlatform(platform); // Your secret management
238
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
239
- request,
240
- platform,
241
- secret,
242
- 300
243
- );
211
+ if (!result.isValid) {
212
+ return new Response(JSON.stringify({ error: result.error }), {
213
+ status: 400,
214
+ headers: { 'Content-Type': 'application/json' }
215
+ });
216
+ }
244
217
 
245
- return result;
218
+ // Process Clerk webhook
219
+ console.log('Clerk event:', result.payload.type);
220
+ return new Response(JSON.stringify({ received: true }));
221
+ }
246
222
  }
247
223
  ```
248
224
 
249
- ### Custom Platform Integration
250
-
251
- ```typescript
252
- import { WebhookVerificationService } from 'tern';
253
-
254
- const customConfig = {
255
- platform: 'custom',
256
- secret: 'your-custom-secret',
257
- signatureConfig: {
258
- algorithm: 'hmac-sha256',
259
- headerName: 'x-custom-signature',
260
- headerFormat: 'raw',
261
- payloadFormat: 'raw'
262
- }
263
- };
225
+ ## API Reference
264
226
 
265
- const result = await WebhookVerificationService.verify(request, customConfig);
266
- ```
227
+ ### WebhookVerificationService
267
228
 
268
- ## ๐Ÿ”ง Adding New Platforms
229
+ #### `verify(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult>`
269
230
 
270
- ### Step 1: Add Platform Type
231
+ Verifies a webhook using the provided configuration.
271
232
 
272
- ```typescript
273
- // In your project, extend the types
274
- declare module 'tern' {
275
- interface WebhookPlatform {
276
- 'your-platform': 'your-platform';
277
- }
278
- }
279
- ```
233
+ #### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult>`
280
234
 
281
- ### Step 2: Use Custom Configuration
235
+ Simplified verification using platform-specific configurations.
282
236
 
283
- ```typescript
284
- const config = {
285
- platform: 'custom',
286
- secret: 'your-secret',
287
- signatureConfig: {
288
- algorithm: 'hmac-sha256', // or 'custom'
289
- headerName: 'x-your-signature',
290
- headerFormat: 'raw',
291
- payloadFormat: 'raw'
292
- }
293
- };
294
- ```
237
+ #### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
295
238
 
296
- ## ๐Ÿ“Š Platform Support Matrix
239
+ Verifies token-based webhooks (like Supabase).
297
240
 
298
- | Platform | Algorithm | Header | Format |
299
- |----------|-----------|--------|--------|
300
- | GitHub | HMAC-SHA256 | `x-hub-signature-256` | `sha256=...` |
301
- | Stripe | HMAC-SHA256 | `stripe-signature` | `t=...,v1=...` |
302
- | Clerk | Custom | `svix-signature` | Base64 |
303
- | Supabase | Token-based | `x-webhook-token` | Simple |
304
- | Shopify | HMAC-SHA256 | `x-shopify-hmac-sha256` | Raw |
305
- | Vercel | HMAC-SHA256 | `x-vercel-signature` | Raw |
306
- | Polar | HMAC-SHA256 | `x-polar-signature` | Raw |
241
+ ### Types
307
242
 
308
- ## ๐Ÿ” Error Handling
243
+ #### `WebhookVerificationResult`
309
244
 
310
245
  ```typescript
311
- try {
312
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
313
- request,
314
- 'github',
315
- 'your-secret',
316
- 300
317
- );
318
-
319
- if (!result.isValid) {
320
- console.error('Verification failed:', result.error);
321
- return res.status(401).json({ error: result.error });
322
- }
323
-
324
- // Process webhook
325
- console.log('Webhook verified:', result.payload);
326
- } catch (error) {
327
- console.error('Verification error:', error);
328
- return res.status(500).json({ error: 'Internal server error' });
246
+ interface WebhookVerificationResult {
247
+ isValid: boolean;
248
+ error?: string;
249
+ platform: WebhookPlatform;
250
+ payload?: any;
251
+ metadata?: {
252
+ timestamp?: string;
253
+ id?: string | null;
254
+ [key: string]: any;
255
+ };
329
256
  }
330
257
  ```
331
258
 
332
- ## ๐Ÿงช Testing
259
+ #### `WebhookConfig`
333
260
 
334
261
  ```typescript
335
- import { WebhookVerificationService } from 'tern';
336
-
337
- // Create a mock request
338
- const mockRequest = new Request('http://localhost/webhook', {
339
- method: 'POST',
340
- headers: {
341
- 'x-hub-signature-256': 'sha256=abc123',
342
- 'content-type': 'application/json'
343
- },
344
- body: JSON.stringify({ test: 'data' })
345
- });
262
+ interface WebhookConfig {
263
+ platform: WebhookPlatform;
264
+ secret: string;
265
+ toleranceInSeconds?: number;
266
+ signatureConfig?: SignatureConfig;
267
+ }
268
+ ```
346
269
 
347
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
348
- mockRequest,
349
- 'github',
350
- 'test-secret',
351
- 300
352
- );
270
+ ## Testing
353
271
 
354
- console.log('Test result:', result);
272
+ Run the test suite:
273
+
274
+ ```bash
275
+ npm test
355
276
  ```
356
277
 
357
- ## ๐Ÿ“ˆ Performance
278
+ Run examples:
358
279
 
359
- - **Zero dependencies**: Only uses Node.js built-in modules
360
- - **Optimized algorithms**: Efficient HMAC verification
361
- - **Timing-safe comparisons**: Prevents timing attacks
362
- - **Minimal overhead**: Lightweight and fast
280
+ ```bash
281
+ npm run examples
282
+ ```
363
283
 
364
- ## ๐Ÿ”’ Security Features
284
+ ## Examples
365
285
 
366
- - **Timing-safe comparisons**: Prevents timing attacks
367
- - **Proper validation**: Comprehensive input validation
368
- - **Secure defaults**: Secure by default configuration
369
- - **Algorithm flexibility**: Support for multiple signature algorithms
286
+ See the [examples.ts](./src/examples.ts) file for comprehensive usage examples.
370
287
 
371
- ## ๐Ÿค Contributing
288
+ ## Contributing
372
289
 
373
290
  1. Fork the repository
374
291
  2. Create a feature branch
375
292
  3. Make your changes
376
- 4. Add tests
293
+ 4. Add tests for new functionality
377
294
  5. Submit a pull request
378
295
 
379
296
  ## ๐Ÿ“„ License
380
297
 
381
- MIT License - see LICENSE file for details.
382
-
383
- ## ๐Ÿ†˜ Support
384
-
385
- - **Issues**: [GitHub Issues](https://github.com/yourusername/tern/issues)
386
- - **Documentation**: [GitHub Wiki](https://github.com/yourusername/tern/wiki)
387
- - **Discussions**: [GitHub Discussions](https://github.com/yourusername/tern/discussions)
298
+ MIT License - see [LICENSE](./LICENSE) for details.
388
299
 
389
- ## ๐Ÿš€ Roadmap
300
+ ## ๐Ÿ”— Links
390
301
 
391
- - [ ] RSA-SHA256 support
392
- - [ ] Ed25519 support
393
- - [ ] Performance optimizations
394
- - [ ] More platform integrations
395
- - [ ] Built-in rate limiting
396
- - [ ] Monitoring and metrics
302
+ - [Documentation](./USAGE.md)
303
+ - [Framework Summary](./FRAMEWORK_SUMMARY.md)
304
+ - [Issues](https://github.com/your-repo/tern/issues)
@@ -1,28 +1,8 @@
1
- export declare function exampleBasicUsage(request: Request): Promise<import("./types").WebhookVerificationResult>;
2
- export declare function exampleTokenBased(request: Request): Promise<import("./types").WebhookVerificationResult>;
3
- export declare function exampleCustomSignature(request: Request): Promise<import("./types").WebhookVerificationResult>;
4
- export declare function examplePlatformDetection(request: Request): Promise<import("./types").WebhookVerificationResult>;
5
- export declare function exampleErrorHandling(request: Request): Promise<{
6
- success: boolean;
7
- error: string | undefined;
8
- payload?: undefined;
9
- } | {
10
- success: boolean;
11
- payload: any;
12
- error?: undefined;
13
- }>;
14
- export declare function exampleBatchVerification(request: Request): Promise<({
15
- platform: string;
16
- success: boolean;
17
- result: import("./types").WebhookVerificationResult;
18
- error?: undefined;
19
- } | {
20
- platform: string;
21
- success: boolean;
22
- error: string;
23
- result?: undefined;
24
- })[]>;
25
- export declare function exampleExpressIntegration(): any;
26
- export declare function exampleNextJsApiRoute(): (req: any, res: any) => Promise<any>;
27
- export declare function exampleCustomPlatform(request: Request): Promise<import("./types").WebhookVerificationResult>;
28
- export declare function exampleHelperMethods(): void;
1
+ export declare function examplePlatformSpecific(): Promise<void>;
2
+ export declare function exampleCustomSignature(): Promise<void>;
3
+ export declare function exampleSimplifiedPlatform(): Promise<void>;
4
+ export declare function exampleTokenBased(): Promise<void>;
5
+ export declare function exampleErrorHandling(): Promise<void>;
6
+ export declare function examplePlatformInfo(): void;
7
+ export declare function exampleRealWorldUsage(): Promise<void>;
8
+ export declare function runAllExamples(): Promise<void>;