@kyuzan/mountain-webhook-sdk
Version:
Webhook signature verification SDK for MOUNTAIN platform
322 lines (248 loc) • 8.17 kB
Markdown
# @kyuzan/mountain-webhook-sdk
Secure webhook signature verification SDK for MOUNTAIN platform events.
## Installation
```bash
npm install @kyuzan/mountain-webhook-sdk
# or
yarn add @kyuzan/mountain-webhook-sdk
# or
pnpm add @kyuzan/mountain-webhook-sdk
```
## Quick Start
```typescript
import { MountainWebhookSdk } from '@kyuzan/mountain-webhook-sdk';
// Initialize the SDK
const sdk = MountainWebhookSdk.initialize();
// Verify webhook signature and get event data
const result = sdk.getEventFromRequest(
request, // Your webhook request object
'whsec_your_webhook_secret_here' // Webhook secret from MOUNTAIN dashboard
);
if (result.isValid) {
console.log('Event verified successfully:', result.payload);
// Process the event
} else {
console.error('Verification failed:', result.error);
}
```
## Features
- ✅ **Secure signature verification** using HMAC-SHA256
- ✅ **Timestamp validation** to prevent replay attacks
- ✅ **TypeScript support** with full type definitions
- ✅ **Framework agnostic** - works with any Node.js framework
- ✅ **Easy integration** with Express, Fastify, Next.js, and more
## Usage Examples
### Express.js
```typescript
import express from 'express';
import { MountainWebhookSdk } from '@kyuzan/mountain-webhook-sdk';
const app = express();
const sdk = MountainWebhookSdk.initialize();
// Important: Use raw body parser for webhook endpoints
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const webhookSecret = process.env.MOUNTAIN_WEBHOOK_SECRET!;
const result = sdk.getEventFromRequest(
{
headers: req.headers,
body: req.body.toString(), // Convert buffer to string
},
webhookSecret
);
if (result.isValid) {
console.log('Event received:', result.payload);
// Process the event based on the data
handleEvent(result.payload);
res.status(200).send('OK');
} else {
console.error('Webhook verification failed:', result.error);
res.status(400).send('Invalid signature');
}
});
function handleEvent(payload: any) {
// Your event processing logic here
console.log('Processing event:', {
id: payload.id,
transactionHash: payload.transactionHash,
blockNumber: payload.blockNumber,
event: payload.event,
});
}
```
### Next.js API Route
```typescript
// pages/api/webhook.ts or app/api/webhook/route.ts
import { NextRequest } from 'next/server';
import { MountainWebhookSdk } from '@kyuzan/mountain-webhook-sdk';
const sdk = MountainWebhookSdk.initialize();
export async function POST(request: NextRequest) {
try {
const body = await request.text(); // Get raw body as string
const webhookSecret = process.env.MOUNTAIN_WEBHOOK_SECRET!;
const result = sdk.getEventFromRequest(
{
headers: Object.fromEntries(request.headers),
body,
},
webhookSecret
);
if (result.isValid) {
// Process the verified event
await processEvent(result.payload);
return new Response('OK', { status: 200 });
} else {
console.error('Webhook verification failed:', result.error);
return new Response('Invalid signature', { status: 400 });
}
} catch (error) {
console.error('Webhook processing error:', error);
return new Response('Internal server error', { status: 500 });
}
}
async function processEvent(payload: any) {
// Your event processing logic
console.log('Event processed:', payload);
}
```
### Fastify
```typescript
import Fastify from 'fastify';
import { MountainWebhookSdk } from '@kyuzan/mountain-webhook-sdk';
const fastify = Fastify();
const sdk = MountainWebhookSdk.initialize();
fastify.addContentTypeParser('application/json', { parseAs: 'string' },
(req, body, done) => {
done(null, body);
}
);
fastify.post('/webhook', async (request, reply) => {
const webhookSecret = process.env.MOUNTAIN_WEBHOOK_SECRET!;
const result = sdk.getEventFromRequest(
{
headers: request.headers,
body: request.body as string,
},
webhookSecret
);
if (result.isValid) {
console.log('Event verified:', result.payload);
reply.status(200).send('OK');
} else {
console.error('Verification failed:', result.error);
reply.status(400).send('Invalid signature');
}
});
```
## API Reference
### `MountainWebhookSdk.initialize(options?)`
Initialize the SDK with optional configuration.
**Parameters:**
- `options` (optional): SDK configuration options
**Returns:** `MountainWebhookSdk` instance
### `sdk.getEventFromRequest(request, webhookSecret, toleranceInSeconds?)`
Verify webhook signature and extract event payload.
**Parameters:**
- `request`: Object containing headers and body
- `headers`: Request headers object
- `body`: Raw request body as string
- `webhookSecret`: Webhook secret from MOUNTAIN dashboard (starts with `whsec_`)
- `toleranceInSeconds` (optional): Timestamp tolerance in seconds (default: 300)
**Returns:** `EventVerificationResult`
```typescript
type EventVerificationResult = {
isValid: boolean;
error?: string;
payload?: EventPayload;
};
type EventPayload = {
id: string;
event: object; // Decoded event log
transactionHash: string;
blockNumber: number;
transactionIndex: number;
logIndex: number;
blockTimestamp: number;
};
```
## Security Best Practices
### 1. Always verify signatures
Never skip signature verification in production:
```typescript
const result = sdk.getEventFromRequest(request, webhookSecret);
if (!result.isValid) {
// Always reject invalid signatures
return res.status(400).send('Invalid signature');
}
```
### 2. Use raw body parser
Webhook signatures are calculated on the raw request body:
```typescript
// ✅ Correct - raw body parser
app.use('/webhook', express.raw({ type: 'application/json' }));
// ❌ Wrong - JSON parser modifies the body
app.use('/webhook', express.json());
```
### 3. Set appropriate timestamp tolerance
The default tolerance is 5 minutes, but you can adjust it:
```typescript
// More strict (1 minute)
const result = sdk.getEventFromRequest(request, secret, 60);
// More lenient (10 minutes)
const result = sdk.getEventFromRequest(request, secret, 600);
```
### 4. Secure your webhook secret
Store your webhook secret securely using environment variables:
```typescript
// ✅ Good
const webhookSecret = process.env.MOUNTAIN_WEBHOOK_SECRET;
// ❌ Bad - never hardcode secrets
const webhookSecret = 'whsec_hardcoded_secret';
```
## Error Handling
The SDK returns detailed error messages for debugging:
```typescript
const result = sdk.getEventFromRequest(request, webhookSecret);
if (!result.isValid) {
switch (result.error) {
case 'No signature found in request':
// Missing X-Mountain-Signature header
break;
case 'Invalid signature format':
// Malformed signature header
break;
case 'Signature timestamp is outside of tolerance window':
// Request too old or too new
break;
case 'Invalid webhook secret':
// Webhook secret format is incorrect
break;
case 'Signature verification failed':
// Signature doesn't match
break;
default:
// Other errors (JSON parsing, etc.)
break;
}
}
```
## Getting Webhook Secrets
1. Log in to the [MOUNTAIN Dashboard](https://mountain-public-docs.netlify.app/)
2. Navigate to your project settings
3. Go to the "Webhooks" section
4. Copy your webhook secret (starts with `whsec_`)
## TypeScript Support
This package includes full TypeScript definitions:
```typescript
import type {
EventVerificationResult,
EventPayload
} from '@kyuzan/mountain-webhook-sdk';
```
## Documentation
For more information about MOUNTAIN webhooks and event types, visit:
- [MOUNTAIN Documentation](https://mountain-public-docs.netlify.app/)
- [Webhook Guide](https://mountain-public-docs.netlify.app/docs/services/mountain-events)
## License
MIT
## Support
For support and questions, please visit [MOUNTAIN Documentation](https://mountain-public-docs.netlify.app/) or create an issue on [GitHub](https://github.com/KyuzanInc/mountain/issues).