npm package: @leakguard/webhook
Description: Secure webhook verification with minimal overhead
Author: LeakGuard Team
Latest version: 3.0.2

# @leakguard/webhook

🔒 **Secure webhook verification with minimal overhead**

Verify incoming webhooks from providers like Stripe, GitHub, Zapier, and more with production-grade security and framework-agnostic design.

## Why @leakguard/webhook?

### The Problem
Webhooks are a critical attack vector. Without proper verification:
- **Spoofed requests** can trigger unauthorized actions
- **Replay attacks** can cause duplicate processing
- **Unauthorized access** to webhook endpoints
- **Data integrity** cannot be guaranteed

### Why It's Hard
- **Cryptographic complexity**: HMAC verification, timing attacks, signature formats
- **Provider variations**: Each service has different header formats and algorithms
- **Framework integration**: Request body handling varies across Express, Fastify, NestJS
- **Edge cases**: Encoding issues, missing headers, malformed signatures

### Why Not Roll Your Own?
```typescript
// ❌ Vulnerable to timing attacks
if (providedSignature !== expectedSignature) {
  return res.status(401).send('Invalid');
}

// ❌ Improper signature parsing
const signature = header.split('=')[1]; // Breaks on complex formats

// ❌ Missing replay protection
// No timestamp validation or duplicate detection

// ✅ LeakGuard handles all of this correctly
```

## Installation

```bash
npm install @leakguard/webhook

# Peer dependencies for specific frameworks
npm install express @types/express          # For Express
npm install fastify                         # For Fastify  
npm install @nestjs/common @nestjs/core     # For NestJS
```

## Quick Start

### Express

```typescript
import express from 'express';
import { createStripeStrategy, expressWebhookVerify, expressRawBody } from '@leakguard/webhook';

const app = express();

// Important: Use rawBody middleware for HMAC verification
app.use('/webhooks', expressRawBody());

// Verify Stripe webhooks
app.use('/webhooks/stripe', expressWebhookVerify({
  strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}));

app.post('/webhooks/stripe', (req, res) => {
  // Webhook is verified ✅
  const event = req.body;
  console.log('Verified Stripe event:', event.type);
  res.sendStatus(200);
});
```

### NestJS

```typescript
import { Controller, Post, UseGuards, MiddlewareConsumer, Module } from '@nestjs/common';
import { 
  WebhookVerificationGuard, 
  RawBodyMiddleware,
  createStripeStrategy 
} from '@leakguard/webhook';

@Controller('webhooks')
export class WebhookController {
  @Post('stripe')
  @UseGuards(new WebhookVerificationGuard({
    strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
  }))
  handleStripeWebhook(@Req() req: Request) {
    // Webhook is verified ✅
    console.log('Verification metadata:', req.webhookVerification);
    return { received: true };
  }
}

@Module({
  controllers: [WebhookController]
})
export class WebhookModule {
  configure(consumer: MiddlewareConsumer) {
    // Apply raw body middleware to webhook routes
    consumer.apply(RawBodyMiddleware).forRoutes('webhooks/*');
  }
}
```

### Fastify

```typescript
import fastify from 'fastify';
import { fastifyWebhookVerify, createGitHubStrategy } from '@leakguard/webhook';

const app = fastify();

// Register webhook verification plugin
await app.register(fastifyWebhookVerify, {
  webhook: {
    strategies: [createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!)],
    routes: ['/webhooks/github']
  }
});

app.post('/webhooks/github', async (request, reply) => {
  // Webhook is verified ✅
  const payload = request.body;
  console.log('GitHub event:', payload);
  return { received: true };
});
```

## Verification Strategies

### HMAC Signature Verification

Verify cryptographic signatures using HMAC algorithms:

```typescript
import { HMACStrategy } from '@leakguard/webhook';

const strategy = new HMACStrategy({
  secret: 'your-webhook-secret',
  algorithm: 'sha256',           // sha1, sha256, sha512
  headerName: 'x-signature-256',
  signaturePrefix: 'sha256=',
  tolerance: 300                 // 5 minutes timestamp tolerance
});
```

### Token-based Verification

Verify static tokens or API keys:

```typescript
import { TokenStrategy } from '@leakguard/webhook';

const strategy = new TokenStrategy({
  token: 'your-webhook-token',
  headerName: 'authorization',    // or 'x-api-key', etc.
  queryParam: 'token'            // Optional: also check query params
});

// Supports Bearer token format
// Header: "Authorization: Bearer your-webhook-token"
```

### IP Allowlist Verification

Restrict webhooks to specific IP addresses or CIDR ranges:

```typescript
import { IPWhitelistStrategy } from '@leakguard/webhook';

const strategy = new IPWhitelistStrategy({
  allowedIPs: [
    '192.168.1.1',      // Single IP
    '10.0.0.0/8',       // CIDR range
    '172.16.0.0/12'
  ],
  trustProxy: true      // Trust X-Forwarded-For headers
});
```

### Custom Verification

Implement your own verification logic:

```typescript
import { CustomStrategy } from '@leakguard/webhook';

const strategy = new CustomStrategy(async (request) => {
  // Your custom logic here
  const isValid = await yourVerificationLogic(request);
  
  return {
    verified: isValid,
    error: isValid ? undefined : 'Custom verification failed',
    metadata: { customField: 'value' }
  };
});
```

## Provider Presets

Pre-configured strategies for popular webhook providers:

### Stripe

```typescript
import { createStripeStrategy } from '@leakguard/webhook';

const strategy = createStripeStrategy(
  process.env.STRIPE_WEBHOOK_SECRET!,
  300 // Optional: timestamp tolerance in seconds
);

// Handles Stripe's signature format:
// stripe-signature: t=1640995200,v1=abc123...
```

### GitHub

```typescript
import { 
  createGitHubStrategy, 
  createGitHubLegacyStrategy 
} from '@leakguard/webhook';

// Modern GitHub webhooks (SHA-256)
const strategy = createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!);

// Legacy GitHub webhooks (SHA-1) 
const legacyStrategy = createGitHubLegacyStrategy(process.env.GITHUB_WEBHOOK_SECRET!);
```

### Zapier

```typescript
import { createZapierStrategy } from '@leakguard/webhook';

const strategy = createZapierStrategy(process.env.ZAPIER_TOKEN!);
```

### Webflow

```typescript
import { createWebflowStrategy } from '@leakguard/webhook';

const strategy = createWebflowStrategy(process.env.WEBFLOW_SECRET!);
```

### Slack

```typescript
import { createSlackStrategy } from '@leakguard/webhook';

const strategy = createSlackStrategy(
  process.env.SLACK_SIGNING_SECRET!,
  300 // Timestamp tolerance
);
```

### Discord & Twilio (IP-based)

```typescript
import { createDiscordStrategy, createTwilioStrategy } from '@leakguard/webhook';

// Discord webhooks (IP allowlist)
const discordStrategy = createDiscordStrategy();

// Twilio webhooks (IP allowlist)  
const twilioStrategy = createTwilioStrategy();
```

### Shopify & E-commerce

```typescript
import { createShopifyStrategy, createSquareStrategy, createLemonSqueezyStrategy } from '@leakguard/webhook';

// Shopify webhooks
const shopifyStrategy = createShopifyStrategy(process.env.SHOPIFY_SECRET!);

// Square webhooks
const squareStrategy = createSquareStrategy(process.env.SQUARE_SIGNATURE_KEY!);

// LemonSqueezy webhooks
const lemonSqueezyStrategy = createLemonSqueezyStrategy(process.env.LEMONSQUEEZY_SECRET!);
```

### Developer & DevOps Tools

```typescript
import { 
  createGitLabStrategy, 
  createBitbucketStrategy,
  createCircleCIStrategy,
  createVercelStrategy,
  createNetlifyStrategy 
} from '@leakguard/webhook';

// GitLab webhooks
const gitlabStrategy = createGitLabStrategy(process.env.GITLAB_TOKEN!);

// Bitbucket webhooks  
const bitbucketStrategy = createBitbucketStrategy(process.env.BITBUCKET_SECRET!);

// CircleCI webhooks
const circleciStrategy = createCircleCIStrategy(process.env.CIRCLECI_SECRET!);

// Vercel webhooks
const vercelStrategy = createVercelStrategy(process.env.VERCEL_SECRET!);

// Netlify webhooks
const netlifyStrategy = createNetlifyStrategy(process.env.NETLIFY_SECRET!);
```

### Productivity & CRM Tools

```typescript
import { 
  createLinearStrategy,
  createNotionStrategy,
  createAsanaStrategy,
  createAirtableStrategy,
  createCalendlyStrategy,
  createFigmaStrategy 
} from '@leakguard/webhook';

// Linear webhooks
const linearStrategy = createLinearStrategy(process.env.LINEAR_SECRET!);

// Notion webhooks
const notionStrategy = createNotionStrategy(process.env.NOTION_SECRET!);

// Asana webhooks
const asanaStrategy = createAsanaStrategy(process.env.ASANA_SECRET!);

// Airtable webhooks
const airtableStrategy = createAirtableStrategy(process.env.AIRTABLE_SECRET!);

// Calendly webhooks
const calendlyStrategy = createCalendlyStrategy(process.env.CALENDLY_SECRET!);

// Figma webhooks
const figmaStrategy = createFigmaStrategy(process.env.FIGMA_SECRET!);
```

### Email & Communication

```typescript
import { 
  createSendGridStrategy,
  createMailgunStrategy,
  createIntercomStrategy,
  createHubSpotStrategy 
} from '@leakguard/webhook';

// SendGrid webhooks
const sendgridStrategy = createSendGridStrategy(process.env.SENDGRID_SECRET!);

// Mailgun webhooks
const mailgunStrategy = createMailgunStrategy(process.env.MAILGUN_SIGNING_KEY!);

// Intercom webhooks
const intercomStrategy = createIntercomStrategy(process.env.INTERCOM_SECRET!);

// HubSpot webhooks
const hubspotStrategy = createHubSpotStrategy(process.env.HUBSPOT_SECRET!);
```

### Enterprise & Support

```typescript
import { 
  createSalesforceStrategy,
  createZendeskStrategy,
  createDocuSignStrategy,
  createDropboxStrategy,
  createBoxStrategy 
} from '@leakguard/webhook';

// Salesforce webhooks
const salesforceStrategy = createSalesforceStrategy(process.env.SALESFORCE_SECRET!);

// Zendesk webhooks
const zendeskStrategy = createZendeskStrategy(process.env.ZENDESK_SECRET!);

// DocuSign webhooks
const docusignStrategy = createDocuSignStrategy(process.env.DOCUSIGN_SECRET!);

// Dropbox webhooks
const dropboxStrategy = createDropboxStrategy(process.env.DROPBOX_SECRET!);

// Box webhooks
const boxStrategy = createBoxStrategy(process.env.BOX_SECRET!);
```

### Multiple Providers

```typescript
import { createProviderStrategies } from '@leakguard/webhook';

const strategies = createProviderStrategies({
  stripe: { secret: process.env.STRIPE_SECRET! },
  github: { secret: process.env.GITHUB_SECRET! },
  shopify: { secret: process.env.SHOPIFY_SECRET! },
  linear: { secret: process.env.LINEAR_SECRET! },
  sendgrid: { secret: process.env.SENDGRID_SECRET! },
  vercel: { secret: process.env.VERCEL_SECRET! }
});
```

## Configuration Options

### Multiple Strategies

Combine multiple verification strategies:

```typescript
const config = {
  strategies: [
    createStripeStrategy(stripeSecret),
    new TokenStrategy({ token: backupToken }),
    new IPWhitelistStrategy({ allowedIPs: ['trusted-ip'] })
  ],
  
  // Require ALL strategies to pass (default: false)
  requireAll: true,
  
  // Stateless timestamp validation (mitigates many replay attacks)
  timestampValidation: {
    timestampHeader: 'x-timestamp',
    tolerance: 300,
    timestampFormat: 'unix' // or 'iso'
  },
  
  // Event handlers
  onSuccess: (request) => {
    console.log('Webhook verified successfully');
  },
  
  onFailure: (result, request) => {
    console.error('Webhook verification failed:', result.error);
  }
};
```

### Framework-Specific Options

#### Express Options

```typescript
const expressConfig = {
  strategies: [/* your strategies */],
  
  skipRoutes: ['/health', '/metrics'],    // Skip verification
  skipMethods: ['GET', 'OPTIONS'],        // Skip HTTP methods
  
  onFailure: (error, req, res, next) => {
    // Custom error handling
    res.status(401).json({ 
      error: 'Webhook verification failed',
      details: error 
    });
  },
  
  onSuccess: (req, res, next) => {
    // Custom success handling
    req.customField = 'verified';
    next();
  }
};
```

#### NestJS Options

```typescript
const nestConfig = {
  strategies: [/* your strategies */],
  skipRoutes: ['/health'],
  skipMethods: ['GET']
};

// Use as Guard
@UseGuards(new WebhookVerificationGuard(nestConfig))

// Use as Middleware
export class ConfiguredMiddleware extends WebhookVerificationMiddleware {
  constructor() {
    super(nestConfig);
  }
}
```

#### Fastify Options

```typescript
const fastifyConfig = {
  webhook: {
    strategies: [/* your strategies */],
    routes: ['/webhooks/*'],  // Apply only to specific routes
    
    onFailure: (error, request, reply) => {
      reply.code(401).send({ error });
    },
    
    onSuccess: (request, reply) => {
      // Custom success logic
    }
  }
};
```

## Stateless Security Architecture

### Why Stateless?

This package is designed for **modern deployment patterns**:
- ✅ **Serverless functions** (AWS Lambda, Vercel, Cloudflare Workers)
- ✅ **Kubernetes autoscaling** with ephemeral pods  
- ✅ **Docker containers** without persistent storage
- ✅ **Multi-region deployments** without shared state

### Replay Protection Strategy

**What we provide** ✅
- **Timestamp validation**: Rejects requests older than X seconds
- **Signature verification**: Cryptographic integrity protection  
- **IP allowlisting**: Restrict to known sources
- **Timing-safe comparisons**: Prevent timing attacks

```typescript
const config = {
  strategies: [createStripeStrategy(secret)],
  
  // Stateless replay mitigation
  timestampValidation: {
    timestampHeader: 'x-timestamp',
    tolerance: 300, // 5 minutes - adjust based on your needs
    timestampFormat: 'unix'
  }
};
```

**What we DON'T provide** ❌
- **Perfect replay protection**: Requires external storage (Redis/DB)
- **Request deduplication**: Would break stateless architecture
- **Global nonce tracking**: Incompatible with autoscaling

### When You Need Full Replay Protection

If your application requires **perfect replay protection**:

```typescript
// Option 1: Add your own request tracking
const processedRequests = new Set(); // Or Redis/DB

app.use('/webhook', (req, res, next) => {
  const requestId = createRequestId(req);
  
  if (processedRequests.has(requestId)) {
    return res.status(409).send('Duplicate request');
  }
  
  processedRequests.add(requestId);
  next();
});

// Option 2: Use provider-specific replay protection
// Many providers (Stripe, GitHub) include their own replay protection

// Option 3: Accept the tradeoff
// For many use cases, timestamp validation is sufficient
```

### Security vs Scalability Tradeoffs

| Approach | Replay Protection | Scalability | Complexity |
|----------|------------------|-------------|------------|
| **Stateless (LeakGuard)** | Good | Excellent | Low |
| **Redis/DB tracking** | Perfect | Good | Medium |
| **Provider built-in** | Perfect | Excellent | Low |

## Security Best Practices

### 1. Always Use Raw Body for HMAC

```typescript
// ✅ Correct - Raw body preserved
app.use('/webhooks', rawBody());
app.use('/webhooks', webhookVerify({ ... }));

// ❌ Wrong - Body already parsed
app.use(express.json());
app.use('/webhooks', webhookVerify({ ... }));
```

### 2. Use HTTPS Only

```typescript
// ✅ Force HTTPS in production
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    return res.redirect(`https://${req.header('host')}${req.url}`);
  }
  next();
});
```

### 3. Implement Rate Limiting

```typescript
import rateLimit from 'express-rate-limit';

// ✅ Rate limit webhook endpoints
const webhookLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/webhooks', webhookLimiter);
```

### 4. Use Different Secrets Per Provider

```typescript
// ✅ Separate secrets for each provider
const strategies = [
  createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET),
  createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET),
  // Don't reuse secrets across providers
];
```

### 5. Enable Timestamp Validation

```typescript
// ✅ Enable timestamp validation for time-sensitive webhooks
const config = {
  strategies: [strategy],
  timestampValidation: {
    timestampHeader: 'x-timestamp',
    tolerance: 300 // 5 minutes
  }
};
```

## Performance

### Benchmarks

- **HMAC (small payload)**: ~50,000 ops/sec
- **HMAC (large payload)**: ~5,000 ops/sec  
- **Token verification**: ~100,000+ ops/sec
- **IP allowlist**: ~150,000+ ops/sec
- **Multiple strategies**: ~20,000 ops/sec

### Optimization Tips

1. **Use specific strategies**: Avoid unnecessary verification steps
2. **Order strategies by speed**: Fast strategies first when using `requireAll: false`
3. **Enable replay protection selectively**: Only for time-sensitive webhooks
4. **Use IP allowlisting when possible**: Fastest verification method

```typescript
// ✅ Optimized configuration
const config = {
  strategies: [
    // Fast strategies first
    new IPWhitelistStrategy({ allowedIPs: ['trusted-range'] }),
    new TokenStrategy({ token: 'backup-token' }),
    // Slower HMAC verification last
    new HMACStrategy({ secret: 'hmac-secret' })
  ],
  requireAll: false // Exit early on first success
};
```

## Troubleshooting

### Common Issues

#### 1. HMAC Verification Fails

```typescript
// Check these common issues:

// ✅ Ensure raw body is available
console.log('Raw body type:', typeof request.rawBody);
console.log('Raw body length:', request.rawBody?.length);

// ✅ Check signature header format
console.log('Signature header:', request.headers['x-signature']);

// ✅ Verify secret is correct
console.log('Secret (first 4 chars):', secret.substring(0, 4));

// ✅ Check algorithm matches provider
const strategy = new HMACStrategy({
  secret: 'correct-secret',
  algorithm: 'sha256', // Make sure this matches provider
  headerName: 'x-hub-signature-256',
  signaturePrefix: 'sha256='
});
```

#### 2. Missing Headers

```typescript
// ✅ Handle missing headers gracefully
const config = {
  strategies: [strategy],
  onFailure: (result, request) => {
    console.log('Available headers:', Object.keys(request.headers));
    console.log('Failure reason:', result.error);
  }
};
```

#### 3. Timestamp Issues

```typescript
// ✅ Debug timestamp validation
const config = {
  timestampValidation: {
    timestampHeader: 'x-timestamp',
    tolerance: 600, // Increase tolerance temporarily
    timestampFormat: 'unix'
  },
  onFailure: (result) => {
    console.log('Current time:', Math.floor(Date.now() / 1000));
    console.log('Request timestamp:', /* extract from headers */);
  }
};
```

## Error Handling

### Custom Error Responses

```typescript
// Express custom error handling
app.use('/webhooks', webhookVerify({
  strategies: [strategy],
  onFailure: (error, req, res, next) => {
    // Log for debugging
    console.error('Webhook verification failed:', {
      error,
      headers: req.headers,
      ip: req.ip,
      url: req.url
    });
    
    // Return generic error (don't leak verification details)
    res.status(401).json({
      error: 'Unauthorized',
      timestamp: new Date().toISOString()
    });
  }
}));

// NestJS exception handling
@UseGuards(new WebhookVerificationGuard(config))
@Post('webhook')
async handleWebhook(@Req() req: Request) {
  try {
    // Process webhook
    return { success: true };
  } catch (error) {
    // Webhook was verified, but processing failed
    throw new InternalServerErrorException('Webhook processing failed');
  }
}
```

## API Reference

### Core Types

```typescript
interface WebhookRequest {
  headers: Record<string, string | string[] | undefined>;
  body: Buffer | string;
  rawBody?: Buffer;
  ip?: string;
}

interface VerificationResult {
  verified: boolean;
  error?: string;
  metadata?: Record<string, unknown>;
}

interface WebhookConfig {
  strategies: VerificationStrategy[];
  requireAll?: boolean;
  timestampValidation?: TimestampValidationOptions;
  onFailure?: (result: VerificationResult, request: WebhookRequest) => void;
  onSuccess?: (request: WebhookRequest) => void;
}
```

### Strategy Classes

- `HMACStrategy(options: HMACOptions)`
- `TokenStrategy(options: TokenOptions)`  
- `IPWhitelistStrategy(options: IPWhitelistOptions)`
- `CustomStrategy(verifyFn: CustomVerificationFunction)`

### Provider Functions

- `createStripeStrategy(secret: string, tolerance?: number)`
- `createGitHubStrategy(secret: string)`
- `createZapierStrategy(token: string)`
- `createWebflowStrategy(secret: string)`
- `createSlackStrategy(secret: string, tolerance?: number)`
- `createDiscordStrategy()`
- `createTwilioStrategy()`

### Framework Adapters

- `expressWebhookVerify(config: ExpressWebhookOptions)`
- `expressRawBody()`
- `fastifyWebhookVerify`
- `WebhookVerificationGuard` (NestJS)
- `WebhookVerificationMiddleware` (NestJS)

## Examples

### Multi-Provider Webhook Handler

```typescript
import express from 'express';
import { 
  createStripeStrategy,
  createGitHubStrategy,
  createZapierStrategy,
  expressWebhookVerify,
  expressRawBody
} from '@leakguard/webhook';

const app = express();

// Global raw body middleware
app.use('/webhooks', expressRawBody());

// Stripe webhooks
app.use('/webhooks/stripe', expressWebhookVerify({
  strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}));

app.post('/webhooks/stripe', (req, res) => {
  const event = req.body;
  console.log('Stripe event:', event.type);
  
  switch (event.type) {
    case 'payment_intent.succeeded':
      // Handle payment success
      break;
    case 'customer.subscription.deleted':
      // Handle subscription cancellation  
      break;
  }
  
  res.sendStatus(200);
});

// GitHub webhooks
app.use('/webhooks/github', expressWebhookVerify({
  strategies: [createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!)]
}));

app.post('/webhooks/github', (req, res) => {
  const event = req.headers['x-github-event'];
  const payload = req.body;
  
  console.log('GitHub event:', event);
  
  if (event === 'push') {
    // Handle repository push
  }
  
  res.sendStatus(200);
});

// Zapier webhooks (token-based)
app.use('/webhooks/zapier', expressWebhookVerify({
  strategies: [createZapierStrategy(process.env.ZAPIER_TOKEN!)]
}));

app.post('/webhooks/zapier', (req, res) => {
  console.log('Zapier payload:', req.body);
  res.sendStatus(200);
});
```

### High-Security Configuration

```typescript
import { 
  HMACStrategy,
  TokenStrategy,
  IPWhitelistStrategy,
  expressWebhookVerify 
} from '@leakguard/webhook';

// Multi-layer verification
const highSecurityConfig = {
  strategies: [
    // Layer 1: IP allowlist
    new IPWhitelistStrategy({
      allowedIPs: ['trusted-cidr-range/24'],
      trustProxy: true
    }),
    
    // Layer 2: Token verification  
    new TokenStrategy({
      token: process.env.BACKUP_TOKEN!,
      headerName: 'x-api-key'
    }),
    
    // Layer 3: HMAC signature
    new HMACStrategy({
      secret: process.env.HMAC_SECRET!,
      algorithm: 'sha256',
      headerName: 'x-signature-256'
    })
  ],
  
  // Require ALL strategies to pass
  requireAll: true,
  
  // Strict replay protection
  replayProtection: {
    timestampHeader: 'x-timestamp',
    tolerance: 60, // 1 minute window
    timestampFormat: 'unix'
  },
  
  onFailure: (result, request) => {
    // Log security events
    console.error('Security violation:', {
      error: result.error,
      ip: request.ip,
      timestamp: new Date().toISOString(),
      headers: request.headers
    });
  }
};

app.use('/webhooks/critical', expressWebhookVerify(highSecurityConfig));
```

## License

MIT - see [LICENSE](../../LICENSE) for details.