UNPKG

@hookflo/tern

Version:

A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms

426 lines (318 loc) 11 kB
# Tern - Algorithm Agnostic Webhook Verification Framework A robust, algorithm-agnostic webhook verification framework that supports multiple platforms with accurate signature verification and payload retrieval. The same framework that secures webhook verification at [Hookflo](https://hookflo.com). ```bash npm install @hookflo/tern ``` [![npm version](https://img.shields.io/npm/v/@hookflo/tern)](https://www.npmjs.com/package/@hookflo/tern) [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue)](https://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) Tern is a zero-dependency TypeScript framework for robust webhook verification across multiple platforms and algorithms. <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" /> ## Features - **Algorithm Agnostic**: Decouples platform logic from signature verification — verify based on cryptographic algorithm, not hardcoded platform rules. Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms - **Platform Specific**: Accurate implementations for **Stripe, GitHub, Supabase, Clerk**, and other platforms - **Flexible Configuration**: Custom signature configurations for any webhook format - **Type Safe**: Full TypeScript support with comprehensive type definitions - **Framework Agnostic**: Works with Express.js, Next.js, Cloudflare Workers, and more ## Why Tern? Most webhook verifiers are tightly coupled to specific platforms or hardcoded logic. Tern introduces a flexible, scalable, algorithm-first approach that: - Works across all major platforms - Supports custom signing logic - Keeps your code clean and modular - Avoids unnecessary dependencies - Is written in strict, modern TypeScript ## Installation ```bash npm install @hookflo/tern ``` ## Quick Start ### Basic Usage ```typescript import { WebhookVerificationService, platformManager } from '@hookflo/tern'; // Method 1: Using the service (recommended) const result = await WebhookVerificationService.verifyWithPlatformConfig( request, 'stripe', 'whsec_your_stripe_webhook_secret' ); // Method 2: Using platform manager (for platform-specific operations) const stripeResult = await platformManager.verify(request, 'stripe', 'whsec_your_secret'); if (result.isValid) { console.log('Webhook verified!', result.payload); } else { console.log('Verification failed:', result.error); } ``` ### Platform-Specific Usage ```typescript import { platformManager } from '@hookflo/tern'; // Run tests for a specific platform const testsPassed = await platformManager.runPlatformTests('stripe'); // Get platform configuration const config = platformManager.getConfig('stripe'); // Get platform documentation const docs = platformManager.getDocumentation('stripe'); ``` ### Platform-Specific Configurations ```typescript import { WebhookVerificationService } from '@hookflo/tern'; // Stripe webhook const stripeConfig = { platform: 'stripe', secret: 'whsec_your_stripe_webhook_secret', toleranceInSeconds: 300, }; // GitHub webhook const githubConfig = { platform: 'github', secret: 'your_github_webhook_secret', toleranceInSeconds: 300, }; // Clerk webhook const clerkConfig = { platform: 'clerk', secret: 'whsec_your_clerk_webhook_secret', toleranceInSeconds: 300, }; const result = await WebhookVerificationService.verify(request, stripeConfig); ``` ## Supported Platforms ### Stripe - **Signature Format**: `t={timestamp},v1={signature}` - **Algorithm**: HMAC-SHA256 - **Payload Format**: `{timestamp}.{body}` ### GitHub - **Signature Format**: `sha256={signature}` - **Algorithm**: HMAC-SHA256 - **Payload Format**: Raw body ### Clerk - **Signature Format**: `v1,{signature}` (space-separated) - **Algorithm**: HMAC-SHA256 with base64 encoding - **Payload Format**: `{id}.{timestamp}.{body}` ### Other Platforms - **Dodo Payments**: HMAC-SHA256 - **Shopify**: HMAC-SHA256 - **Vercel**: HMAC-SHA256 - **Polar**: HMAC-SHA256 - **Supabase**: Token-based authentication ## Custom Platform Configuration This framework is fully configuration-driven. You can verify webhooks from any provider—even if it is not built-in—by supplying a custom configuration object. This allows you to support new or proprietary platforms instantly, without waiting for a library update. ### Example: Standard HMAC-SHA256 Webhook ```typescript import { WebhookVerificationService } from '@hookflo/tern'; const acmeConfig = { platform: 'acmepay', secret: 'acme_secret', signatureConfig: { algorithm: 'hmac-sha256', headerName: 'x-acme-signature', headerFormat: 'raw', timestampHeader: 'x-acme-timestamp', timestampFormat: 'unix', payloadFormat: 'timestamped', // signs as {timestamp}.{body} } }; const result = await WebhookVerificationService.verify(request, acmeConfig); ``` ### Example: Svix/Standard Webhooks (Clerk, Dodo Payments, etc.) ```typescript const svixConfig = { platform: 'my-svix-platform', secret: 'whsec_abc123...', signatureConfig: { algorithm: 'hmac-sha256', headerName: 'webhook-signature', headerFormat: 'raw', timestampHeader: 'webhook-timestamp', timestampFormat: 'unix', payloadFormat: 'custom', customConfig: { payloadFormat: '{id}.{timestamp}.{body}', idHeader: 'webhook-id', // encoding: 'base64' // only if the provider uses base64, otherwise omit } } }; const result = await WebhookVerificationService.verify(request, svixConfig); ``` You can configure any combination of algorithm, header, payload, and encoding. See the `SignatureConfig` type for all options. ## Webhook Verification OK Tested Platforms - **Stripe** - **Supabase** - **Github** - **Clerk** - **Dodo Payments** - **Other Platforms** : Yet to verify.... ## Custom Configurations ### Custom HMAC-SHA256 ```typescript const customConfig = { platform: 'custom', secret: 'your_custom_secret', signatureConfig: { algorithm: 'hmac-sha256', headerName: 'x-custom-signature', headerFormat: 'prefixed', prefix: 'sha256=', payloadFormat: 'raw', }, }; ``` ### Custom Timestamped Payload ```typescript const timestampedConfig = { platform: 'custom', secret: 'your_custom_secret', signatureConfig: { algorithm: 'hmac-sha256', headerName: 'x-webhook-signature', headerFormat: 'raw', timestampHeader: 'x-webhook-timestamp', timestampFormat: 'unix', payloadFormat: 'timestamped', }, }; ``` ## Framework Integration ### Express.js ```typescript app.post('/webhooks/stripe', async (req, res) => { const result = await WebhookVerificationService.verifyWithPlatformConfig( req, 'stripe', process.env.STRIPE_WEBHOOK_SECRET ); if (!result.isValid) { return res.status(400).json({ error: result.error }); } // Process the webhook console.log('Stripe event:', result.payload.type); res.json({ received: true }); }); ``` ### Next.js API Route ```typescript // pages/api/webhooks/github.js export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const result = await WebhookVerificationService.verifyWithPlatformConfig( req, 'github', process.env.GITHUB_WEBHOOK_SECRET ); if (!result.isValid) { return res.status(400).json({ error: result.error }); } // Handle GitHub webhook const event = req.headers['x-github-event']; console.log('GitHub event:', event); res.json({ received: true }); } ``` ### Cloudflare Workers ```typescript addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { if (request.url.includes('/webhooks/clerk')) { const result = await WebhookVerificationService.verifyWithPlatformConfig( request, 'clerk', CLERK_WEBHOOK_SECRET ); if (!result.isValid) { return new Response(JSON.stringify({ error: result.error }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // Process Clerk webhook console.log('Clerk event:', result.payload.type); return new Response(JSON.stringify({ received: true })); } } ``` ## API Reference ### WebhookVerificationService #### `verify(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult>` Verifies a webhook using the provided configuration. #### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult>` Simplified verification using platform-specific configurations. #### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>` Verifies token-based webhooks (like Supabase). ### Types #### `WebhookVerificationResult` ```typescript interface WebhookVerificationResult { isValid: boolean; error?: string; platform: WebhookPlatform; payload?: any; metadata?: { timestamp?: string; id?: string | null; [key: string]: any; }; } ``` #### `WebhookConfig` ```typescript interface WebhookConfig { platform: WebhookPlatform; secret: string; toleranceInSeconds?: number; signatureConfig?: SignatureConfig; } ``` ## Testing ### Run All Tests ```bash npm test ``` ### Platform-Specific Testing ```bash # Test a specific platform npm run test:platform stripe # Test all platforms npm run test:all ``` ### Documentation and Analysis ```bash # Fetch platform documentation npm run docs:fetch # Generate diffs between versions npm run docs:diff # Analyze changes and generate reports npm run docs:analyze ``` ## Examples See the [examples.ts](./src/examples.ts) file for comprehensive usage examples. ## Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for detailed information on how to: - Set up your development environment - Add new platforms - Write tests - Submit pull requests - Follow our code style guidelines ### Quick Start for Contributors 1. Fork the repository 2. Clone your fork: `git clone https://github.com/your-username/tern.git` 3. Create a feature branch: `git checkout -b feature/your-feature-name` 4. Make your changes 5. Run tests: `npm test` 6. Submit a pull request ### Adding a New Platform See our [Platform Development Guide](CONTRIBUTING.md#adding-new-platforms) for step-by-step instructions on adding support for new webhook platforms. ## Code of Conduct This project adheres to our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing. ## 📄 License MIT License - see [LICENSE](./LICENSE) for details. ## 🔗 Links - [Documentation](./USAGE.md) - [Framework Summary](./FRAMEWORK_SUMMARY.md) - [Issues](https://github.com/Hookflo/tern/issues)