@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 +36 -0
- package/LICENSE.md +9 -0
- package/README.md +385 -0
- package/dist/ContentfulWebhookHandler.d.ts +71 -0
- package/dist/ContentfulWebhookHandler.js +213 -0
- package/dist/__tests__/ContentfulWebhookHandler.test.d.ts +1 -0
- package/dist/__tests__/ContentfulWebhookHandler.test.js +576 -0
- package/dist/__tests__/index.test.d.ts +1 -0
- package/dist/__tests__/index.test.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/jest.config.cjs +33 -0
- package/package.json +45 -0
- package/src/ContentfulWebhookHandler.ts +260 -0
- package/src/__tests__/ContentfulWebhookHandler.test.ts +682 -0
- package/src/__tests__/index.test.ts +78 -0
- package/src/index.ts +21 -0
- package/tsconfig.json +22 -0
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
|
+
}
|