Validates Contentful webhook signatures using HMAC-SHA256, parses webhook payloads, and provides type-safe interfaces for all webhook event types
npm install @bernierllc/contentful-webhook-handlerValidates Contentful webhook signatures using HMAC-SHA256, parses webhook payloads, and provides type-safe interfaces for all webhook event types.
- 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
``bash`
npm install @bernierllc/contentful-webhook-handler
`typescript
import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
const handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!
});
`
`typescript
// 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 });
});
`
`typescript
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'Action: ${eventType.action}
console.log(); // 'create' | 'publish' | 'delete' | etc.
// Route based on event type
if (eventType.resource === 'Entry' && eventType.action === 'publish') {
await handleEntryPublish(result.payload);
}
}
`
`typescript`
const handler = new ContentfulWebhookHandler({
webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
enableTimestampValidation: true,
maxTimestampDeltaSeconds: 300 // 5 minutes
});
`typescript`
// When rotating secrets
handler.updateSecret(newWebhookSecret);
#### Constructor Options
`typescript`
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
Validates signature and parses webhook payload.
Parameters:
- body: Raw webhook body as string (must be unparsed JSON)headers
- : Webhook headers (must include x-contentful-signature and x-contentful-topic)
Returns:
`typescript`
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 from x-contentful-signature header
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:
`typescript`
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
- Entry created
- Entry.save - Entry saved (draft)
- Entry.auto_save - Entry auto-saved
- Entry.publish - Entry published
- Entry.unpublish - Entry unpublished
- Entry.archive - Entry archived
- Entry.unarchive - Entry unarchived
- Entry.delete - Entry deleted$3
- Asset.create - Asset created
- Asset.save - Asset saved
- Asset.auto_save - Asset auto-saved
- Asset.publish - Asset published
- Asset.unpublish - Asset unpublished
- Asset.archive - Asset archived
- Asset.unarchive - Asset unarchived
- Asset.delete - Asset deleted$3
- ContentType.create - Content type created
- ContentType.save - Content type saved
- ContentType.publish - Content type published
- ContentType.unpublish - Content type unpublished
- ContentType.delete - Content type deletedExpress.js Integration Example
`typescript
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
1. Always validate signatures: Never trust webhook data without signature validation
2. Use raw body: Ensure you pass the unparsed JSON string to
parseWebhook()
3. Enable timestamp validation: Prevent replay attacks by validating webhook timestamps
4. Rotate secrets regularly: Use updateSecret() to rotate your webhook secrets
5. Use HTTPS: Always use HTTPS endpoints for webhooks in production
6. Rate limiting: Implement rate limiting on webhook endpointsError Handling
`typescript
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
$3
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
`typescript
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.
$3
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:
`typescript
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 utilitiesCopyright (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.
- @bernierllc/contentful-types - Contentful TypeScript types
- @bernierllc/contentful-webhook-service - Webhook processing service
- @bernierllc/webhook-receiver - Generic webhook receiver