UNPKG

@kyuzan/mountain-webhook-sdk

Version:

Webhook signature verification SDK for MOUNTAIN platform

322 lines (248 loc) 8.17 kB
# @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).