@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.
@@ -0,0 +1,576 @@
1
+ "use strict";
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ const ContentfulWebhookHandler_1 = require("../ContentfulWebhookHandler");
15
+ const logger_1 = require("@bernierllc/logger");
16
+ describe('ContentfulWebhookHandler', () => {
17
+ const TEST_SECRET = 'test-webhook-secret';
18
+ let handler;
19
+ beforeEach(() => {
20
+ handler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
21
+ webhookSecret: TEST_SECRET
22
+ });
23
+ });
24
+ describe('constructor', () => {
25
+ it('should create handler with required options', () => {
26
+ expect(handler).toBeInstanceOf(ContentfulWebhookHandler_1.ContentfulWebhookHandler);
27
+ });
28
+ it('should accept custom logger', () => {
29
+ const customLogger = new logger_1.Logger({
30
+ level: logger_1.LogLevel.INFO,
31
+ transports: [new logger_1.ConsoleTransport()],
32
+ context: {
33
+ service: 'CustomContext',
34
+ version: '1.0.0',
35
+ environment: 'test'
36
+ }
37
+ });
38
+ const customHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
39
+ webhookSecret: TEST_SECRET,
40
+ logger: customLogger
41
+ });
42
+ expect(customHandler).toBeInstanceOf(ContentfulWebhookHandler_1.ContentfulWebhookHandler);
43
+ });
44
+ it('should accept timestamp validation options', () => {
45
+ const timestampHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
46
+ webhookSecret: TEST_SECRET,
47
+ enableTimestampValidation: true,
48
+ maxTimestampDeltaSeconds: 600
49
+ });
50
+ expect(timestampHandler).toBeInstanceOf(ContentfulWebhookHandler_1.ContentfulWebhookHandler);
51
+ });
52
+ });
53
+ describe('validateSignature', () => {
54
+ it('should validate correct signature', () => {
55
+ const body = '{"test":"data"}';
56
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
57
+ hmac.update(body);
58
+ const signature = hmac.digest('base64');
59
+ const isValid = handler.validateSignature(body, signature);
60
+ expect(isValid).toBe(true);
61
+ });
62
+ it('should reject invalid signature', () => {
63
+ const body = '{"test":"data"}';
64
+ const invalidSignature = 'invalid-signature-base64';
65
+ const isValid = handler.validateSignature(body, invalidSignature);
66
+ expect(isValid).toBe(false);
67
+ });
68
+ it('should reject tampered body', () => {
69
+ const originalBody = '{"test":"data"}';
70
+ const tamperedBody = '{"test":"tampered"}';
71
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
72
+ hmac.update(originalBody);
73
+ const signature = hmac.digest('base64');
74
+ const isValid = handler.validateSignature(tamperedBody, signature);
75
+ expect(isValid).toBe(false);
76
+ });
77
+ it('should reject signature with different secret', () => {
78
+ const body = '{"test":"data"}';
79
+ const hmac = crypto_1.default.createHmac('sha256', 'different-secret');
80
+ hmac.update(body);
81
+ const signature = hmac.digest('base64');
82
+ const isValid = handler.validateSignature(body, signature);
83
+ expect(isValid).toBe(false);
84
+ });
85
+ it('should handle empty body', () => {
86
+ const body = '';
87
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
88
+ hmac.update(body);
89
+ const signature = hmac.digest('base64');
90
+ const isValid = handler.validateSignature(body, signature);
91
+ expect(isValid).toBe(true);
92
+ });
93
+ it('should return false for buffer length mismatch', () => {
94
+ const body = '{"test":"data"}';
95
+ const shortSignature = 'short';
96
+ const isValid = handler.validateSignature(body, shortSignature);
97
+ expect(isValid).toBe(false);
98
+ });
99
+ it('should handle crypto errors gracefully', () => {
100
+ const body = '{"test":"data"}';
101
+ // Intentionally malformed base64 that will cause issues
102
+ const badSignature = '!!!invalid-base64!!!';
103
+ const isValid = handler.validateSignature(body, badSignature);
104
+ expect(isValid).toBe(false);
105
+ });
106
+ it('should handle non-Error exceptions in signature validation', () => {
107
+ const testHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
108
+ webhookSecret: TEST_SECRET
109
+ });
110
+ const body = '{"test":"data"}';
111
+ // Mock crypto.createHmac to throw a non-Error
112
+ const originalCreateHmac = crypto_1.default.createHmac;
113
+ crypto_1.default.createHmac = jest.fn().mockImplementationOnce(() => {
114
+ // eslint-disable-next-line @typescript-eslint/no-throw-literal
115
+ throw 'crypto error';
116
+ });
117
+ try {
118
+ const isValid = testHandler.validateSignature(body, 'signature');
119
+ expect(isValid).toBe(false);
120
+ }
121
+ finally {
122
+ // Restore original
123
+ crypto_1.default.createHmac = originalCreateHmac;
124
+ }
125
+ });
126
+ });
127
+ describe('parseWebhook', () => {
128
+ const createValidPayload = () => ({
129
+ sys: {
130
+ type: 'Entry',
131
+ id: 'test-entry-123',
132
+ space: {
133
+ sys: {
134
+ type: 'Link',
135
+ linkType: 'Space',
136
+ id: 'space-123'
137
+ }
138
+ },
139
+ environment: {
140
+ sys: {
141
+ type: 'Link',
142
+ linkType: 'Environment',
143
+ id: 'master'
144
+ }
145
+ },
146
+ contentType: {
147
+ sys: {
148
+ type: 'Link',
149
+ linkType: 'ContentType',
150
+ id: 'blogPost'
151
+ }
152
+ },
153
+ revision: 1,
154
+ createdAt: new Date().toISOString(),
155
+ updatedAt: new Date().toISOString()
156
+ },
157
+ fields: {
158
+ title: { 'en-US': 'Test Title' },
159
+ content: { 'en-US': 'Test content' }
160
+ }
161
+ });
162
+ const signBody = (body) => {
163
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
164
+ hmac.update(body);
165
+ return hmac.digest('base64');
166
+ };
167
+ it('should parse valid webhook', () => {
168
+ const payload = createValidPayload();
169
+ const body = JSON.stringify(payload);
170
+ const signature = signBody(body);
171
+ const headers = {
172
+ 'x-contentful-signature': signature,
173
+ 'x-contentful-topic': 'Entry.publish'
174
+ };
175
+ const result = handler.parseWebhook(body, headers);
176
+ expect(result.valid).toBe(true);
177
+ expect(result.error).toBeUndefined();
178
+ expect(result.payload).toEqual(payload);
179
+ expect(result.topic).toBe('Entry.publish');
180
+ });
181
+ it('should reject webhook without signature header', () => {
182
+ const payload = createValidPayload();
183
+ const body = JSON.stringify(payload);
184
+ const headers = {
185
+ 'x-contentful-topic': 'Entry.publish'
186
+ };
187
+ const result = handler.parseWebhook(body, headers);
188
+ expect(result.valid).toBe(false);
189
+ expect(result.error).toBe('Missing signature header (x-contentful-signature)');
190
+ expect(result.payload).toBeUndefined();
191
+ });
192
+ it('should reject webhook without topic header', () => {
193
+ const payload = createValidPayload();
194
+ const body = JSON.stringify(payload);
195
+ const signature = signBody(body);
196
+ const headers = {
197
+ 'x-contentful-signature': signature
198
+ };
199
+ const result = handler.parseWebhook(body, headers);
200
+ expect(result.valid).toBe(false);
201
+ expect(result.error).toBe('Missing topic header (x-contentful-topic)');
202
+ expect(result.payload).toBeUndefined();
203
+ });
204
+ it('should reject webhook with invalid signature', () => {
205
+ const payload = createValidPayload();
206
+ const body = JSON.stringify(payload);
207
+ const headers = {
208
+ 'x-contentful-signature': 'invalid-signature',
209
+ 'x-contentful-topic': 'Entry.publish'
210
+ };
211
+ const result = handler.parseWebhook(body, headers);
212
+ expect(result.valid).toBe(false);
213
+ expect(result.error).toBe('Invalid signature');
214
+ expect(result.payload).toBeUndefined();
215
+ });
216
+ it('should reject webhook with invalid JSON', () => {
217
+ const body = 'invalid json {';
218
+ const signature = signBody(body);
219
+ const headers = {
220
+ 'x-contentful-signature': signature,
221
+ 'x-contentful-topic': 'Entry.publish'
222
+ };
223
+ const result = handler.parseWebhook(body, headers);
224
+ expect(result.valid).toBe(false);
225
+ expect(result.error).toContain('Unexpected');
226
+ expect(result.payload).toBeUndefined();
227
+ });
228
+ it('should handle non-Error exceptions in parsing', () => {
229
+ // Create a new handler for this test to avoid affecting other tests
230
+ const testHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
231
+ webhookSecret: TEST_SECRET
232
+ });
233
+ const body = '{"test":"data"}';
234
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
235
+ hmac.update(body);
236
+ const signature = hmac.digest('base64');
237
+ // Mock JSON.parse to throw a non-Error
238
+ const originalParse = global.JSON.parse;
239
+ global.JSON.parse = jest.fn().mockImplementationOnce(() => {
240
+ // eslint-disable-next-line @typescript-eslint/no-throw-literal
241
+ throw 'string error';
242
+ });
243
+ try {
244
+ const headers = {
245
+ 'x-contentful-signature': signature,
246
+ 'x-contentful-topic': 'Entry.publish'
247
+ };
248
+ const result = testHandler.parseWebhook(body, headers);
249
+ expect(result.valid).toBe(false);
250
+ expect(result.error).toBe('string error');
251
+ }
252
+ finally {
253
+ // Restore original
254
+ global.JSON.parse = originalParse;
255
+ }
256
+ });
257
+ it('should handle all webhook topics', () => {
258
+ const topics = [
259
+ 'Entry.create',
260
+ 'Entry.publish',
261
+ 'Asset.delete',
262
+ 'ContentType.save'
263
+ ];
264
+ topics.forEach(topic => {
265
+ const payload = createValidPayload();
266
+ const body = JSON.stringify(payload);
267
+ const signature = signBody(body);
268
+ const headers = {
269
+ 'x-contentful-signature': signature,
270
+ 'x-contentful-topic': topic
271
+ };
272
+ const result = handler.parseWebhook(body, headers);
273
+ expect(result.valid).toBe(true);
274
+ expect(result.topic).toBe(topic);
275
+ });
276
+ });
277
+ });
278
+ describe('timestamp validation', () => {
279
+ const createPayloadWithTimestamp = (timestamp) => ({
280
+ sys: {
281
+ type: 'Entry',
282
+ id: 'test-entry-123',
283
+ space: {
284
+ sys: {
285
+ type: 'Link',
286
+ linkType: 'Space',
287
+ id: 'space-123'
288
+ }
289
+ },
290
+ environment: {
291
+ sys: {
292
+ type: 'Link',
293
+ linkType: 'Environment',
294
+ id: 'master'
295
+ }
296
+ },
297
+ contentType: {
298
+ sys: {
299
+ type: 'Link',
300
+ linkType: 'ContentType',
301
+ id: 'blogPost'
302
+ }
303
+ },
304
+ updatedAt: timestamp
305
+ }
306
+ });
307
+ const signBody = (body) => {
308
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
309
+ hmac.update(body);
310
+ return hmac.digest('base64');
311
+ };
312
+ it('should accept recent timestamp when validation enabled', () => {
313
+ const timestampHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
314
+ webhookSecret: TEST_SECRET,
315
+ enableTimestampValidation: true,
316
+ maxTimestampDeltaSeconds: 300
317
+ });
318
+ const payload = createPayloadWithTimestamp(new Date().toISOString());
319
+ const body = JSON.stringify(payload);
320
+ const signature = signBody(body);
321
+ const headers = {
322
+ 'x-contentful-signature': signature,
323
+ 'x-contentful-topic': 'Entry.publish'
324
+ };
325
+ const result = timestampHandler.parseWebhook(body, headers);
326
+ expect(result.valid).toBe(true);
327
+ });
328
+ it('should reject old timestamp when validation enabled', () => {
329
+ const timestampHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
330
+ webhookSecret: TEST_SECRET,
331
+ enableTimestampValidation: true,
332
+ maxTimestampDeltaSeconds: 300
333
+ });
334
+ // Create timestamp 10 minutes ago (600 seconds)
335
+ const oldDate = new Date(Date.now() - 600000);
336
+ const payload = createPayloadWithTimestamp(oldDate.toISOString());
337
+ const body = JSON.stringify(payload);
338
+ const signature = signBody(body);
339
+ const headers = {
340
+ 'x-contentful-signature': signature,
341
+ 'x-contentful-topic': 'Entry.publish'
342
+ };
343
+ const result = timestampHandler.parseWebhook(body, headers);
344
+ expect(result.valid).toBe(false);
345
+ expect(result.error).toBe('Invalid timestamp (possible replay attack)');
346
+ });
347
+ it('should accept any timestamp when validation disabled', () => {
348
+ // Default handler has timestamp validation disabled
349
+ const oldDate = new Date(Date.now() - 600000);
350
+ const payload = createPayloadWithTimestamp(oldDate.toISOString());
351
+ const body = JSON.stringify(payload);
352
+ const signature = signBody(body);
353
+ const headers = {
354
+ 'x-contentful-signature': signature,
355
+ 'x-contentful-topic': 'Entry.publish'
356
+ };
357
+ const result = handler.parseWebhook(body, headers);
358
+ expect(result.valid).toBe(true);
359
+ });
360
+ it('should reject payload without timestamp when validation enabled', () => {
361
+ const timestampHandler = new ContentfulWebhookHandler_1.ContentfulWebhookHandler({
362
+ webhookSecret: TEST_SECRET,
363
+ enableTimestampValidation: true,
364
+ maxTimestampDeltaSeconds: 300
365
+ });
366
+ const payload = {
367
+ sys: {
368
+ type: 'Entry',
369
+ id: 'test-entry-123',
370
+ space: {
371
+ sys: {
372
+ type: 'Link',
373
+ linkType: 'Space',
374
+ id: 'space-123'
375
+ }
376
+ },
377
+ environment: {
378
+ sys: {
379
+ type: 'Link',
380
+ linkType: 'Environment',
381
+ id: 'master'
382
+ }
383
+ }
384
+ }
385
+ };
386
+ const body = JSON.stringify(payload);
387
+ const signature = signBody(body);
388
+ const headers = {
389
+ 'x-contentful-signature': signature,
390
+ 'x-contentful-topic': 'Entry.publish'
391
+ };
392
+ const result = timestampHandler.parseWebhook(body, headers);
393
+ expect(result.valid).toBe(false);
394
+ expect(result.error).toBe('Invalid timestamp (possible replay attack)');
395
+ });
396
+ });
397
+ describe('getEventType', () => {
398
+ it('should parse Entry.publish topic', () => {
399
+ const result = handler.getEventType('Entry.publish');
400
+ expect(result.resource).toBe('Entry');
401
+ expect(result.action).toBe('publish');
402
+ });
403
+ it('should parse Asset.delete topic', () => {
404
+ const result = handler.getEventType('Asset.delete');
405
+ expect(result.resource).toBe('Asset');
406
+ expect(result.action).toBe('delete');
407
+ });
408
+ it('should parse ContentType.save topic', () => {
409
+ const result = handler.getEventType('ContentType.save');
410
+ expect(result.resource).toBe('ContentType');
411
+ expect(result.action).toBe('save');
412
+ });
413
+ it('should parse Entry.create topic', () => {
414
+ const result = handler.getEventType('Entry.create');
415
+ expect(result.resource).toBe('Entry');
416
+ expect(result.action).toBe('create');
417
+ });
418
+ it('should parse Entry.auto_save topic', () => {
419
+ const result = handler.getEventType('Entry.auto_save');
420
+ expect(result.resource).toBe('Entry');
421
+ expect(result.action).toBe('auto_save');
422
+ });
423
+ it('should parse Entry.archive topic', () => {
424
+ const result = handler.getEventType('Entry.archive');
425
+ expect(result.resource).toBe('Entry');
426
+ expect(result.action).toBe('archive');
427
+ });
428
+ it('should parse Entry.unarchive topic', () => {
429
+ const result = handler.getEventType('Entry.unarchive');
430
+ expect(result.resource).toBe('Entry');
431
+ expect(result.action).toBe('unarchive');
432
+ });
433
+ it('should parse Entry.unpublish topic', () => {
434
+ const result = handler.getEventType('Entry.unpublish');
435
+ expect(result.resource).toBe('Entry');
436
+ expect(result.action).toBe('unpublish');
437
+ });
438
+ it('should throw error for invalid topic format', () => {
439
+ expect(() => {
440
+ handler.getEventType('InvalidTopic');
441
+ }).toThrow('Invalid topic format: InvalidTopic');
442
+ });
443
+ it('should throw error for too many parts', () => {
444
+ expect(() => {
445
+ handler.getEventType('Entry.publish.extra');
446
+ }).toThrow('Invalid topic format: Entry.publish.extra');
447
+ });
448
+ it('should throw error for invalid resource', () => {
449
+ expect(() => {
450
+ handler.getEventType('InvalidResource.publish');
451
+ }).toThrow('Invalid resource type: InvalidResource');
452
+ });
453
+ it('should throw error for invalid action', () => {
454
+ expect(() => {
455
+ handler.getEventType('Entry.invalidAction');
456
+ }).toThrow('Invalid action type: invalidAction');
457
+ });
458
+ });
459
+ describe('updateSecret', () => {
460
+ it('should update webhook secret', () => {
461
+ const newSecret = 'new-test-secret';
462
+ handler.updateSecret(newSecret);
463
+ // Verify new secret works
464
+ const body = '{"test":"data"}';
465
+ const hmac = crypto_1.default.createHmac('sha256', newSecret);
466
+ hmac.update(body);
467
+ const signature = hmac.digest('base64');
468
+ const isValid = handler.validateSignature(body, signature);
469
+ expect(isValid).toBe(true);
470
+ });
471
+ it('should invalidate old secret after update', () => {
472
+ const body = '{"test":"data"}';
473
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
474
+ hmac.update(body);
475
+ const oldSignature = hmac.digest('base64');
476
+ // Update to new secret
477
+ handler.updateSecret('new-test-secret');
478
+ // Old signature should no longer work
479
+ const isValid = handler.validateSignature(body, oldSignature);
480
+ expect(isValid).toBe(false);
481
+ });
482
+ });
483
+ describe('integration scenarios', () => {
484
+ it('should handle complete Entry.publish webhook', () => {
485
+ const payload = {
486
+ sys: {
487
+ type: 'Entry',
488
+ id: 'blog-post-123',
489
+ space: {
490
+ sys: {
491
+ type: 'Link',
492
+ linkType: 'Space',
493
+ id: 'my-space'
494
+ }
495
+ },
496
+ environment: {
497
+ sys: {
498
+ type: 'Link',
499
+ linkType: 'Environment',
500
+ id: 'master'
501
+ }
502
+ },
503
+ contentType: {
504
+ sys: {
505
+ type: 'Link',
506
+ linkType: 'ContentType',
507
+ id: 'blogPost'
508
+ }
509
+ },
510
+ revision: 5,
511
+ createdAt: '2025-01-01T00:00:00Z',
512
+ updatedAt: new Date().toISOString()
513
+ },
514
+ fields: {
515
+ title: { 'en-US': 'My Blog Post' },
516
+ content: { 'en-US': 'Blog post content here' }
517
+ }
518
+ };
519
+ const body = JSON.stringify(payload);
520
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
521
+ hmac.update(body);
522
+ const signature = hmac.digest('base64');
523
+ const headers = {
524
+ 'x-contentful-signature': signature,
525
+ 'x-contentful-topic': 'Entry.publish',
526
+ 'x-contentful-webhook-name': 'test-webhook',
527
+ 'x-contentful-crn': 'crn:contentful:::content:spaces/my-space'
528
+ };
529
+ const result = handler.parseWebhook(body, headers);
530
+ expect(result.valid).toBe(true);
531
+ expect(result.payload?.sys.id).toBe('blog-post-123');
532
+ expect(result.topic).toBe('Entry.publish');
533
+ if (result.topic) {
534
+ const eventType = handler.getEventType(result.topic);
535
+ expect(eventType.resource).toBe('Entry');
536
+ expect(eventType.action).toBe('publish');
537
+ }
538
+ });
539
+ it('should handle Asset.delete webhook', () => {
540
+ const payload = {
541
+ sys: {
542
+ type: 'Asset',
543
+ id: 'asset-456',
544
+ space: {
545
+ sys: {
546
+ type: 'Link',
547
+ linkType: 'Space',
548
+ id: 'my-space'
549
+ }
550
+ },
551
+ environment: {
552
+ sys: {
553
+ type: 'Link',
554
+ linkType: 'Environment',
555
+ id: 'master'
556
+ }
557
+ },
558
+ revision: 1,
559
+ updatedAt: new Date().toISOString()
560
+ }
561
+ };
562
+ const body = JSON.stringify(payload);
563
+ const hmac = crypto_1.default.createHmac('sha256', TEST_SECRET);
564
+ hmac.update(body);
565
+ const signature = hmac.digest('base64');
566
+ const headers = {
567
+ 'x-contentful-signature': signature,
568
+ 'x-contentful-topic': 'Asset.delete'
569
+ };
570
+ const result = handler.parseWebhook(body, headers);
571
+ expect(result.valid).toBe(true);
572
+ expect(result.payload?.sys.type).toBe('Asset');
573
+ expect(result.topic).toBe('Asset.delete');
574
+ });
575
+ });
576
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
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
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ const index_1 = require("../index");
11
+ describe('index exports', () => {
12
+ it('should export ContentfulWebhookHandler class', () => {
13
+ expect(index_1.ContentfulWebhookHandler).toBeDefined();
14
+ expect(typeof index_1.ContentfulWebhookHandler).toBe('function');
15
+ });
16
+ it('should create a ContentfulWebhookHandler instance', () => {
17
+ const handler = new index_1.ContentfulWebhookHandler({
18
+ webhookSecret: 'test-secret'
19
+ });
20
+ expect(handler).toBeInstanceOf(index_1.ContentfulWebhookHandler);
21
+ });
22
+ it('should export type definitions', () => {
23
+ // Type check - these should compile
24
+ const payload = {
25
+ sys: {
26
+ type: 'Entry',
27
+ id: 'test',
28
+ space: {
29
+ sys: { type: 'Link', linkType: 'Space', id: 'space' }
30
+ },
31
+ environment: {
32
+ sys: { type: 'Link', linkType: 'Environment', id: 'master' }
33
+ }
34
+ }
35
+ };
36
+ const validationResult = {
37
+ valid: true,
38
+ payload,
39
+ topic: 'Entry.publish'
40
+ };
41
+ const eventType = {
42
+ resource: 'Entry',
43
+ action: 'publish'
44
+ };
45
+ const options = {
46
+ webhookSecret: 'test'
47
+ };
48
+ const topic = 'Entry.publish';
49
+ const headers = {
50
+ 'x-contentful-signature': 'sig',
51
+ 'x-contentful-topic': 'Entry.publish',
52
+ 'x-contentful-webhook-name': 'test',
53
+ 'x-contentful-crn': 'crn:test'
54
+ };
55
+ // If these compile, the types are exported correctly
56
+ expect(validationResult).toBeDefined();
57
+ expect(eventType).toBeDefined();
58
+ expect(options).toBeDefined();
59
+ expect(topic).toBeDefined();
60
+ expect(headers).toBeDefined();
61
+ });
62
+ });
@@ -0,0 +1,2 @@
1
+ export { ContentfulWebhookHandler, WebhookValidationResult, EventType, ContentfulWebhookHandlerOptions } from './ContentfulWebhookHandler';
2
+ export type { ContentfulWebhookPayload, ContentfulWebhookTopic, ContentfulWebhookHeaders } from '@bernierllc/contentful-types';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
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
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ContentfulWebhookHandler = void 0;
11
+ var ContentfulWebhookHandler_1 = require("./ContentfulWebhookHandler");
12
+ Object.defineProperty(exports, "ContentfulWebhookHandler", { enumerable: true, get: function () { return ContentfulWebhookHandler_1.ContentfulWebhookHandler; } });