@bernierllc/contentful-webhook-handler
v1.0.6
Published
Validates Contentful webhook signatures using HMAC-SHA256, parses webhook payloads, and provides type-safe interfaces for all webhook event types
Readme
@bernierllc/contentful-webhook-handler
Validates Contentful webhook signatures using HMAC-SHA256, parses webhook payloads, and provides type-safe interfaces for all webhook event types.
Features
- HMAC-SHA256 Signature Validation: Securely verify webhook authenticity
- Type-Safe Payload Parsing: Full TypeScript support for all Contentful webhook events
- Event Type Detection: Extract resource type and action from webhook topics
- Replay Attack Prevention: Optional timestamp validation
- Timing-Safe Comparison: Prevents timing attacks during signature validation
- Comprehensive Error Handling: Detailed error messages for debugging
Installation
npm install @bernierllc/contentful-webhook-handlerUsage
Basic Setup
import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
const handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!
});Validate and Parse Webhook
// In your webhook endpoint handler
app.post('/webhooks/contentful', (req, res) => {
const body = JSON.stringify(req.body);
const headers = {
'x-contentful-signature': req.headers['x-contentful-signature'],
'x-contentful-topic': req.headers['x-contentful-topic']
};
const result = handler.parseWebhook(body, headers);
if (!result.valid) {
console.error('Invalid webhook:', result.error);
return res.status(401).json({ error: result.error });
}
// Process valid webhook
const { payload, topic } = result;
console.log(`Received ${topic} for ${payload.sys.type} ${payload.sys.id}`);
res.status(200).json({ success: true });
});Extract Event Type
const result = handler.parseWebhook(body, headers);
if (result.valid && result.topic) {
const eventType = handler.getEventType(result.topic);
console.log(`Resource: ${eventType.resource}`); // 'Entry' | 'Asset' | 'ContentType'
console.log(`Action: ${eventType.action}`); // 'create' | 'publish' | 'delete' | etc.
// Route based on event type
if (eventType.resource === 'Entry' && eventType.action === 'publish') {
await handleEntryPublish(result.payload);
}
}Enable Replay Attack Prevention
const handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
enableTimestampValidation: true,
maxTimestampDeltaSeconds: 300 // 5 minutes
});Update Webhook Secret
// When rotating secrets
handler.updateSecret(newWebhookSecret);API
ContentfulWebhookHandler
Constructor Options
interface ContentfulWebhookHandlerOptions {
webhookSecret: string; // Required: Your Contentful webhook secret
logger?: Logger; // Optional: Custom logger instance
enableTimestampValidation?: boolean; // Optional: Enable replay attack prevention (default: false)
maxTimestampDeltaSeconds?: number; // Optional: Max timestamp age in seconds (default: 300)
}Methods
parseWebhook(body: string, headers: Partial<ContentfulWebhookHeaders>): WebhookValidationResult
Validates signature and parses webhook payload.
Parameters:
body: Raw webhook body as string (must be unparsed JSON)headers: Webhook headers (must includex-contentful-signatureandx-contentful-topic)
Returns:
interface WebhookValidationResult {
valid: boolean; // True if webhook is valid
error?: string; // Error message if invalid
payload?: ContentfulWebhookPayload; // Parsed payload if valid
topic?: ContentfulWebhookTopic; // Webhook topic if valid
}validateSignature(body: string, signature: string): boolean
Validates webhook signature using HMAC-SHA256.
Parameters:
body: Raw webhook body as stringsignature: Base64-encoded signature fromx-contentful-signatureheader
Returns: true if signature is valid, false otherwise
getEventType(topic: ContentfulWebhookTopic): EventType
Extracts resource type and action from webhook topic.
Parameters:
topic: Contentful webhook topic (e.g., "Entry.publish")
Returns:
interface EventType {
resource: 'Entry' | 'Asset' | 'ContentType';
action: 'create' | 'save' | 'auto_save' | 'publish' | 'unpublish' | 'delete' | 'archive' | 'unarchive';
}updateSecret(newSecret: string): void
Updates the webhook secret (useful for secret rotation).
Parameters:
newSecret: New webhook secret
Supported Webhook Topics
Entry Events
Entry.create- Entry createdEntry.save- Entry saved (draft)Entry.auto_save- Entry auto-savedEntry.publish- Entry publishedEntry.unpublish- Entry unpublishedEntry.archive- Entry archivedEntry.unarchive- Entry unarchivedEntry.delete- Entry deleted
Asset Events
Asset.create- Asset createdAsset.save- Asset savedAsset.auto_save- Asset auto-savedAsset.publish- Asset publishedAsset.unpublish- Asset unpublishedAsset.archive- Asset archivedAsset.unarchive- Asset unarchivedAsset.delete- Asset deleted
ContentType Events
ContentType.create- Content type createdContentType.save- Content type savedContentType.publish- Content type publishedContentType.unpublish- Content type unpublishedContentType.delete- Content type deleted
Express.js Integration Example
import express from 'express';
import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
const app = express();
// IMPORTANT: Use express.text() to preserve raw body for signature validation
app.use('/webhooks/contentful', express.text({ type: 'application/json' }));
const handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
enableTimestampValidation: true
});
app.post('/webhooks/contentful', (req, res) => {
const result = handler.parseWebhook(req.body, {
'x-contentful-signature': req.headers['x-contentful-signature'] as string,
'x-contentful-topic': req.headers['x-contentful-topic'] as string
});
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
// Process webhook
const eventType = handler.getEventType(result.topic!);
switch (eventType.resource) {
case 'Entry':
if (eventType.action === 'publish') {
console.log('Entry published:', result.payload!.sys.id);
}
break;
case 'Asset':
if (eventType.action === 'delete') {
console.log('Asset deleted:', result.payload!.sys.id);
}
break;
}
res.status(200).json({ success: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Security Best Practices
- Always validate signatures: Never trust webhook data without signature validation
- Use raw body: Ensure you pass the unparsed JSON string to
parseWebhook() - Enable timestamp validation: Prevent replay attacks by validating webhook timestamps
- Rotate secrets regularly: Use
updateSecret()to rotate your webhook secrets - Use HTTPS: Always use HTTPS endpoints for webhooks in production
- Rate limiting: Implement rate limiting on webhook endpoints
Error Handling
const result = handler.parseWebhook(body, headers);
if (!result.valid) {
switch (result.error) {
case 'Missing signature header (x-contentful-signature)':
// Handle missing signature
break;
case 'Missing topic header (x-contentful-topic)':
// Handle missing topic
break;
case 'Invalid signature':
// Handle signature validation failure
break;
case 'Invalid timestamp (possible replay attack)':
// Handle replay attack attempt
break;
default:
// Handle other errors (e.g., JSON parsing)
console.error('Webhook error:', result.error);
}
}MECE Architecture
This package follows MECE (Mutually Exclusive, Collectively Exhaustive) principles:
Includes:
- HMAC-SHA256 signature validation
- Webhook payload parsing and typing
- Event type detection and routing
- Header validation
- Replay attack prevention
Excludes:
- ❌ Webhook receiving/HTTP server (use
@bernierllc/webhook-receiver) - ❌ Webhook queueing (use
@bernierllc/message-queue) - ❌ Webhook processing logic (use
@bernierllc/contentful-webhook-service)
Integration Documentation
Logger Integration
This package integrates with @bernierllc/logger for structured logging of webhook validation events. The logger provides:
- Structured JSON output for all validation events
- Configurable log levels (info, warn, error)
- Context-aware logging with service metadata
- Optional custom logger injection via constructor
import { Logger } from '@bernierllc/logger';
const customLogger = new Logger({
service: 'my-webhook-service',
level: 'debug'
});
const handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
logger: customLogger
});The integration uses graceful degradation - if no custom logger is provided, a default logger instance is created automatically.
NeverHub Integration
This package is designed for NeverHub integration when used in service-layer packages like @bernierllc/contentful-webhook-service. While the core handler itself doesn't directly integrate with NeverHub, it provides the building blocks for NeverHub-aware webhook processing:
- Type-safe webhook payloads for NeverHub event routing
- Signature validation for secure webhook ingestion
- Event type extraction for NeverHub event classification
When building a service layer with NeverHub integration, use this pattern:
import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
class ContentfulWebhookService {
private handler: ContentfulWebhookHandler;
private neverhub?: NeverHubAdapter;
async initialize() {
this.handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!
});
// Auto-detect NeverHub
if (await NeverHubAdapter.detect()) {
this.neverhub = new NeverHubAdapter();
await this.neverhub.register({
type: 'contentful-webhook-service',
capabilities: ['webhook-processing', 'signature-validation']
});
}
}
async processWebhook(body: string, headers: any) {
const result = this.handler.parseWebhook(body, headers);
if (result.valid && this.neverhub) {
// Route validated webhooks to NeverHub
await this.neverhub.emit('contentful.webhook', {
topic: result.topic,
payload: result.payload
});
}
return result;
}
}This architecture ensures graceful degradation - the webhook handler works with or without NeverHub, allowing for flexible deployment scenarios.
Dependencies
@bernierllc/contentful-types- Contentful type definitions@bernierllc/logger- Logging utilities
License
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
Related Packages
- @bernierllc/contentful-types - Contentful TypeScript types
- @bernierllc/contentful-webhook-service - Webhook processing service
- @bernierllc/webhook-receiver - Generic webhook receiver
