npm package: @kyuzan/mountain-webhook-sdk
Description: Webhook signature verification SDK for MOUNTAIN platform
Author: Kyuzan Inc.
Latest version: 1.0.0

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