@bernierllc/contentful-webhook-handler 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.cjs ADDED
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ parserOptions: {
12
+ ecmaVersion: 2022,
13
+ sourceType: 'module',
14
+ project: './tsconfig.json',
15
+ },
16
+ plugins: ['@typescript-eslint'],
17
+ extends: [
18
+ 'eslint:recommended',
19
+ 'plugin:@typescript-eslint/recommended',
20
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
21
+ ],
22
+ rules: {
23
+ '@typescript-eslint/no-explicit-any': 'error',
24
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
25
+ '@typescript-eslint/explicit-function-return-type': 'off',
26
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
27
+ '@typescript-eslint/no-unsafe-assignment': 'warn',
28
+ '@typescript-eslint/no-unsafe-member-access': 'warn',
29
+ '@typescript-eslint/no-unsafe-call': 'warn',
30
+ '@typescript-eslint/no-unsafe-return': 'warn',
31
+ },
32
+ env: {
33
+ node: true,
34
+ jest: true,
35
+ },
36
+ };
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ # Limited-Use License
2
+
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+
9
+ For licensing inquiries, please contact: legal@bernierllc.com
package/README.md ADDED
@@ -0,0 +1,385 @@
1
+ # @bernierllc/contentful-webhook-handler
2
+
3
+ Validates Contentful webhook signatures using HMAC-SHA256, parses webhook payloads, and provides type-safe interfaces for all webhook event types.
4
+
5
+ ## Features
6
+
7
+ - **HMAC-SHA256 Signature Validation**: Securely verify webhook authenticity
8
+ - **Type-Safe Payload Parsing**: Full TypeScript support for all Contentful webhook events
9
+ - **Event Type Detection**: Extract resource type and action from webhook topics
10
+ - **Replay Attack Prevention**: Optional timestamp validation
11
+ - **Timing-Safe Comparison**: Prevents timing attacks during signature validation
12
+ - **Comprehensive Error Handling**: Detailed error messages for debugging
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @bernierllc/contentful-webhook-handler
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Setup
23
+
24
+ ```typescript
25
+ import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
26
+
27
+ const handler = new ContentfulWebhookHandler({
28
+ webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!
29
+ });
30
+ ```
31
+
32
+ ### Validate and Parse Webhook
33
+
34
+ ```typescript
35
+ // In your webhook endpoint handler
36
+ app.post('/webhooks/contentful', (req, res) => {
37
+ const body = JSON.stringify(req.body);
38
+ const headers = {
39
+ 'x-contentful-signature': req.headers['x-contentful-signature'],
40
+ 'x-contentful-topic': req.headers['x-contentful-topic']
41
+ };
42
+
43
+ const result = handler.parseWebhook(body, headers);
44
+
45
+ if (!result.valid) {
46
+ console.error('Invalid webhook:', result.error);
47
+ return res.status(401).json({ error: result.error });
48
+ }
49
+
50
+ // Process valid webhook
51
+ const { payload, topic } = result;
52
+ console.log(`Received ${topic} for ${payload.sys.type} ${payload.sys.id}`);
53
+
54
+ res.status(200).json({ success: true });
55
+ });
56
+ ```
57
+
58
+ ### Extract Event Type
59
+
60
+ ```typescript
61
+ const result = handler.parseWebhook(body, headers);
62
+
63
+ if (result.valid && result.topic) {
64
+ const eventType = handler.getEventType(result.topic);
65
+
66
+ console.log(`Resource: ${eventType.resource}`); // 'Entry' | 'Asset' | 'ContentType'
67
+ console.log(`Action: ${eventType.action}`); // 'create' | 'publish' | 'delete' | etc.
68
+
69
+ // Route based on event type
70
+ if (eventType.resource === 'Entry' && eventType.action === 'publish') {
71
+ await handleEntryPublish(result.payload);
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### Enable Replay Attack Prevention
77
+
78
+ ```typescript
79
+ const handler = new ContentfulWebhookHandler({
80
+ webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
81
+ enableTimestampValidation: true,
82
+ maxTimestampDeltaSeconds: 300 // 5 minutes
83
+ });
84
+ ```
85
+
86
+ ### Update Webhook Secret
87
+
88
+ ```typescript
89
+ // When rotating secrets
90
+ handler.updateSecret(newWebhookSecret);
91
+ ```
92
+
93
+ ## API
94
+
95
+ ### `ContentfulWebhookHandler`
96
+
97
+ #### Constructor Options
98
+
99
+ ```typescript
100
+ interface ContentfulWebhookHandlerOptions {
101
+ webhookSecret: string; // Required: Your Contentful webhook secret
102
+ logger?: Logger; // Optional: Custom logger instance
103
+ enableTimestampValidation?: boolean; // Optional: Enable replay attack prevention (default: false)
104
+ maxTimestampDeltaSeconds?: number; // Optional: Max timestamp age in seconds (default: 300)
105
+ }
106
+ ```
107
+
108
+ #### Methods
109
+
110
+ ##### `parseWebhook(body: string, headers: Partial<ContentfulWebhookHeaders>): WebhookValidationResult`
111
+
112
+ Validates signature and parses webhook payload.
113
+
114
+ **Parameters:**
115
+ - `body`: Raw webhook body as string (must be unparsed JSON)
116
+ - `headers`: Webhook headers (must include `x-contentful-signature` and `x-contentful-topic`)
117
+
118
+ **Returns:**
119
+ ```typescript
120
+ interface WebhookValidationResult {
121
+ valid: boolean; // True if webhook is valid
122
+ error?: string; // Error message if invalid
123
+ payload?: ContentfulWebhookPayload; // Parsed payload if valid
124
+ topic?: ContentfulWebhookTopic; // Webhook topic if valid
125
+ }
126
+ ```
127
+
128
+ ##### `validateSignature(body: string, signature: string): boolean`
129
+
130
+ Validates webhook signature using HMAC-SHA256.
131
+
132
+ **Parameters:**
133
+ - `body`: Raw webhook body as string
134
+ - `signature`: Base64-encoded signature from `x-contentful-signature` header
135
+
136
+ **Returns:** `true` if signature is valid, `false` otherwise
137
+
138
+ ##### `getEventType(topic: ContentfulWebhookTopic): EventType`
139
+
140
+ Extracts resource type and action from webhook topic.
141
+
142
+ **Parameters:**
143
+ - `topic`: Contentful webhook topic (e.g., "Entry.publish")
144
+
145
+ **Returns:**
146
+ ```typescript
147
+ interface EventType {
148
+ resource: 'Entry' | 'Asset' | 'ContentType';
149
+ action: 'create' | 'save' | 'auto_save' | 'publish' | 'unpublish' | 'delete' | 'archive' | 'unarchive';
150
+ }
151
+ ```
152
+
153
+ ##### `updateSecret(newSecret: string): void`
154
+
155
+ Updates the webhook secret (useful for secret rotation).
156
+
157
+ **Parameters:**
158
+ - `newSecret`: New webhook secret
159
+
160
+ ## Supported Webhook Topics
161
+
162
+ ### Entry Events
163
+ - `Entry.create` - Entry created
164
+ - `Entry.save` - Entry saved (draft)
165
+ - `Entry.auto_save` - Entry auto-saved
166
+ - `Entry.publish` - Entry published
167
+ - `Entry.unpublish` - Entry unpublished
168
+ - `Entry.archive` - Entry archived
169
+ - `Entry.unarchive` - Entry unarchived
170
+ - `Entry.delete` - Entry deleted
171
+
172
+ ### Asset Events
173
+ - `Asset.create` - Asset created
174
+ - `Asset.save` - Asset saved
175
+ - `Asset.auto_save` - Asset auto-saved
176
+ - `Asset.publish` - Asset published
177
+ - `Asset.unpublish` - Asset unpublished
178
+ - `Asset.archive` - Asset archived
179
+ - `Asset.unarchive` - Asset unarchived
180
+ - `Asset.delete` - Asset deleted
181
+
182
+ ### ContentType Events
183
+ - `ContentType.create` - Content type created
184
+ - `ContentType.save` - Content type saved
185
+ - `ContentType.publish` - Content type published
186
+ - `ContentType.unpublish` - Content type unpublished
187
+ - `ContentType.delete` - Content type deleted
188
+
189
+ ## Express.js Integration Example
190
+
191
+ ```typescript
192
+ import express from 'express';
193
+ import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
194
+
195
+ const app = express();
196
+
197
+ // IMPORTANT: Use express.text() to preserve raw body for signature validation
198
+ app.use('/webhooks/contentful', express.text({ type: 'application/json' }));
199
+
200
+ const handler = new ContentfulWebhookHandler({
201
+ webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
202
+ enableTimestampValidation: true
203
+ });
204
+
205
+ app.post('/webhooks/contentful', (req, res) => {
206
+ const result = handler.parseWebhook(req.body, {
207
+ 'x-contentful-signature': req.headers['x-contentful-signature'] as string,
208
+ 'x-contentful-topic': req.headers['x-contentful-topic'] as string
209
+ });
210
+
211
+ if (!result.valid) {
212
+ return res.status(401).json({ error: result.error });
213
+ }
214
+
215
+ // Process webhook
216
+ const eventType = handler.getEventType(result.topic!);
217
+
218
+ switch (eventType.resource) {
219
+ case 'Entry':
220
+ if (eventType.action === 'publish') {
221
+ console.log('Entry published:', result.payload!.sys.id);
222
+ }
223
+ break;
224
+ case 'Asset':
225
+ if (eventType.action === 'delete') {
226
+ console.log('Asset deleted:', result.payload!.sys.id);
227
+ }
228
+ break;
229
+ }
230
+
231
+ res.status(200).json({ success: true });
232
+ });
233
+
234
+ app.listen(3000, () => {
235
+ console.log('Webhook server running on port 3000');
236
+ });
237
+ ```
238
+
239
+ ## Security Best Practices
240
+
241
+ 1. **Always validate signatures**: Never trust webhook data without signature validation
242
+ 2. **Use raw body**: Ensure you pass the unparsed JSON string to `parseWebhook()`
243
+ 3. **Enable timestamp validation**: Prevent replay attacks by validating webhook timestamps
244
+ 4. **Rotate secrets regularly**: Use `updateSecret()` to rotate your webhook secrets
245
+ 5. **Use HTTPS**: Always use HTTPS endpoints for webhooks in production
246
+ 6. **Rate limiting**: Implement rate limiting on webhook endpoints
247
+
248
+ ## Error Handling
249
+
250
+ ```typescript
251
+ const result = handler.parseWebhook(body, headers);
252
+
253
+ if (!result.valid) {
254
+ switch (result.error) {
255
+ case 'Missing signature header (x-contentful-signature)':
256
+ // Handle missing signature
257
+ break;
258
+ case 'Missing topic header (x-contentful-topic)':
259
+ // Handle missing topic
260
+ break;
261
+ case 'Invalid signature':
262
+ // Handle signature validation failure
263
+ break;
264
+ case 'Invalid timestamp (possible replay attack)':
265
+ // Handle replay attack attempt
266
+ break;
267
+ default:
268
+ // Handle other errors (e.g., JSON parsing)
269
+ console.error('Webhook error:', result.error);
270
+ }
271
+ }
272
+ ```
273
+
274
+ ## MECE Architecture
275
+
276
+ This package follows MECE (Mutually Exclusive, Collectively Exhaustive) principles:
277
+
278
+ **Includes:**
279
+ - HMAC-SHA256 signature validation
280
+ - Webhook payload parsing and typing
281
+ - Event type detection and routing
282
+ - Header validation
283
+ - Replay attack prevention
284
+
285
+ **Excludes:**
286
+ - ❌ Webhook receiving/HTTP server (use `@bernierllc/webhook-receiver`)
287
+ - ❌ Webhook queueing (use `@bernierllc/message-queue`)
288
+ - ❌ Webhook processing logic (use `@bernierllc/contentful-webhook-service`)
289
+
290
+ ## Integration Documentation
291
+
292
+ ### Logger Integration
293
+
294
+ This package integrates with `@bernierllc/logger` for structured logging of webhook validation events. The logger provides:
295
+
296
+ - Structured JSON output for all validation events
297
+ - Configurable log levels (info, warn, error)
298
+ - Context-aware logging with service metadata
299
+ - Optional custom logger injection via constructor
300
+
301
+ ```typescript
302
+ import { Logger } from '@bernierllc/logger';
303
+
304
+ const customLogger = new Logger({
305
+ service: 'my-webhook-service',
306
+ level: 'debug'
307
+ });
308
+
309
+ const handler = new ContentfulWebhookHandler({
310
+ webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!,
311
+ logger: customLogger
312
+ });
313
+ ```
314
+
315
+ The integration uses graceful degradation - if no custom logger is provided, a default logger instance is created automatically.
316
+
317
+ ### NeverHub Integration
318
+
319
+ 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:
320
+
321
+ - Type-safe webhook payloads for NeverHub event routing
322
+ - Signature validation for secure webhook ingestion
323
+ - Event type extraction for NeverHub event classification
324
+
325
+ When building a service layer with NeverHub integration, use this pattern:
326
+
327
+ ```typescript
328
+ import { ContentfulWebhookHandler } from '@bernierllc/contentful-webhook-handler';
329
+ import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
330
+
331
+ class ContentfulWebhookService {
332
+ private handler: ContentfulWebhookHandler;
333
+ private neverhub?: NeverHubAdapter;
334
+
335
+ async initialize() {
336
+ this.handler = new ContentfulWebhookHandler({
337
+ webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET!
338
+ });
339
+
340
+ // Auto-detect NeverHub
341
+ if (await NeverHubAdapter.detect()) {
342
+ this.neverhub = new NeverHubAdapter();
343
+ await this.neverhub.register({
344
+ type: 'contentful-webhook-service',
345
+ capabilities: ['webhook-processing', 'signature-validation']
346
+ });
347
+ }
348
+ }
349
+
350
+ async processWebhook(body: string, headers: any) {
351
+ const result = this.handler.parseWebhook(body, headers);
352
+
353
+ if (result.valid && this.neverhub) {
354
+ // Route validated webhooks to NeverHub
355
+ await this.neverhub.emit('contentful.webhook', {
356
+ topic: result.topic,
357
+ payload: result.payload
358
+ });
359
+ }
360
+
361
+ return result;
362
+ }
363
+ }
364
+ ```
365
+
366
+ This architecture ensures graceful degradation - the webhook handler works with or without NeverHub, allowing for flexible deployment scenarios.
367
+
368
+ ## Dependencies
369
+
370
+ - `@bernierllc/contentful-types` - Contentful type definitions
371
+ - `@bernierllc/logger` - Logging utilities
372
+
373
+ ## License
374
+
375
+ Copyright (c) 2025 Bernier LLC
376
+
377
+ This file is licensed to the client under a limited-use license.
378
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
379
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
380
+
381
+ ## Related Packages
382
+
383
+ - [@bernierllc/contentful-types](../contentful-types) - Contentful TypeScript types
384
+ - [@bernierllc/contentful-webhook-service](../../service/contentful-webhook-service) - Webhook processing service
385
+ - [@bernierllc/webhook-receiver](../webhook-receiver) - Generic webhook receiver
@@ -0,0 +1,71 @@
1
+ import { ContentfulWebhookPayload, ContentfulWebhookTopic, ContentfulWebhookHeaders } from '@bernierllc/contentful-types';
2
+ import { Logger } from '@bernierllc/logger';
3
+ export interface WebhookValidationResult {
4
+ valid: boolean;
5
+ error?: string;
6
+ payload?: ContentfulWebhookPayload;
7
+ topic?: ContentfulWebhookTopic;
8
+ }
9
+ export interface EventType {
10
+ resource: 'Entry' | 'Asset' | 'ContentType';
11
+ action: 'create' | 'save' | 'auto_save' | 'publish' | 'unpublish' | 'delete' | 'archive' | 'unarchive';
12
+ }
13
+ export interface ContentfulWebhookHandlerOptions {
14
+ webhookSecret: string;
15
+ logger?: Logger;
16
+ enableTimestampValidation?: boolean;
17
+ maxTimestampDeltaSeconds?: number;
18
+ }
19
+ /**
20
+ * Validates Contentful webhook signatures using HMAC-SHA256,
21
+ * parses webhook payloads, and provides type-safe interfaces
22
+ * for all webhook event types.
23
+ */
24
+ export declare class ContentfulWebhookHandler {
25
+ private webhookSecret;
26
+ private logger;
27
+ private enableTimestampValidation;
28
+ private maxTimestampDeltaSeconds;
29
+ /**
30
+ * Creates a new ContentfulWebhookHandler instance
31
+ *
32
+ * @param options Configuration options
33
+ */
34
+ constructor(options: ContentfulWebhookHandlerOptions);
35
+ /**
36
+ * Validates a webhook signature using HMAC-SHA256
37
+ *
38
+ * @param body Raw webhook body as string
39
+ * @param signature Signature from x-contentful-signature header
40
+ * @returns True if signature is valid
41
+ */
42
+ validateSignature(body: string, signature: string): boolean;
43
+ /**
44
+ * Validates webhook timestamp to prevent replay attacks
45
+ *
46
+ * @param payload Parsed webhook payload
47
+ * @returns True if timestamp is within acceptable range
48
+ */
49
+ private validateTimestamp;
50
+ /**
51
+ * Parses and validates a Contentful webhook
52
+ *
53
+ * @param body Raw webhook body as string
54
+ * @param headers Webhook headers
55
+ * @returns Validation result with parsed payload if valid
56
+ */
57
+ parseWebhook(body: string, headers: Partial<ContentfulWebhookHeaders>): WebhookValidationResult;
58
+ /**
59
+ * Extracts resource type and action from a topic string
60
+ *
61
+ * @param topic Contentful webhook topic (e.g., "Entry.publish")
62
+ * @returns Object with resource type and action
63
+ */
64
+ getEventType(topic: ContentfulWebhookTopic): EventType;
65
+ /**
66
+ * Updates the webhook secret
67
+ *
68
+ * @param newSecret New webhook secret
69
+ */
70
+ updateSecret(newSecret: string): void;
71
+ }