An open API service indexing awesome lists of open source software.

https://github.com/hookflo/tern

A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms.
https://github.com/hookflo/tern

hmac npm svix typescript webhook-verifier webhooks

Last synced: 4 months ago
JSON representation

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

Awesome Lists containing this project

README

          

# 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).

⭐ Star this repo to support the project and help others discover it!

💬 Join the discussion & contribute in our Discord: [Hookflo Community](https://discord.com/invite/SNmCjU97nr)

```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.

tern bird nature

## 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
- **GitLab**: 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`

Verifies a webhook using the provided configuration.

#### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise`

Simplified verification using platform-specific configurations.

#### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise`

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)