@rodit/rodit-auth-be 9.11.14

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,1388 @@
1
+ /**
2
+ * Webhook event handling
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ // webhookhandlermw.js
7
+ // Reusable webhook handler for RODiT SDK
8
+
9
+ const crypto = require("crypto");
10
+ const https = require("https");
11
+ const { Agent } = require("undici");
12
+ const config = require('../../services/configsdk');
13
+ const { isStrictEnvironment, getNodeEnv } = require('../../services/env');
14
+ const logger = require("../../services/logger");
15
+ const { createLogContext, logErrorWithMetrics } = logger;
16
+ const { ulid } = require("ulid");
17
+ const { sendError } = require("../../services/error-response");
18
+ const nacl = require("tweetnacl");
19
+ const stateManager = require("../blockchain/statemanager");
20
+ const { authenticate_webhook } = require("../auth/authentication");
21
+
22
+ /**
23
+ * Create a raw body parser middleware specifically for webhook endpoints
24
+ * This preserves the raw body for signature verification
25
+ * @returns {Function} Express middleware
26
+ */
27
+ function createRawBodyParser() {
28
+ return (req, res, next) => {
29
+ if (req.headers['content-type'] !== 'application/json') {
30
+ const requestId = req.requestId || req.headers['x-request-id'] || ulid();
31
+ return sendError(res, {
32
+ statusCode: 415,
33
+ requestId,
34
+ code: 'UNSUPPORTED_MEDIA_TYPE',
35
+ message: 'Only application/json is supported'
36
+ });
37
+ }
38
+
39
+ let data = '';
40
+ req.setEncoding('utf8');
41
+
42
+ req.on('data', (chunk) => {
43
+ data += chunk;
44
+ });
45
+
46
+ req.on('end', () => {
47
+ // Store the raw body for signature verification
48
+ req.rawBody = data;
49
+
50
+ // Parse JSON for convenience
51
+ try {
52
+ req.body = JSON.parse(data);
53
+ next();
54
+ } catch (e) {
55
+ const requestId = req.requestId || req.headers['x-request-id'] || ulid();
56
+ return sendError(res, {
57
+ statusCode: 400,
58
+ requestId,
59
+ code: 'INVALID_JSON_PAYLOAD',
60
+ message: 'Invalid JSON payload'
61
+ });
62
+ }
63
+ });
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Create middleware for webhook request processing
69
+ * Webhooks use digital signature authentication only - no API tokens needed
70
+ * @returns {Function} Express middleware
71
+ */
72
+ function createWebhookProcessingMiddleware() {
73
+ return (req, res, next) => {
74
+ // Mark this as a webhook request for logging purposes
75
+ req.isWebhookRequest = true;
76
+ next();
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Create middleware to attach the server's public key to the request
82
+ * @param {Object} stateManager - State manager instance
83
+ * @returns {Function} Express middleware
84
+ */
85
+ function createPublicKeyMiddleware(stateManager) {
86
+ return async (req, res, next) => {
87
+ const requestId = crypto.randomUUID();
88
+ const logContext = {
89
+ requestId,
90
+ apiEndpoint: req.path,
91
+ method: req.method,
92
+ headers: Object.keys(req.headers),
93
+ hasSignature: !!req.headers["x-signature"],
94
+ hasTimestamp: !!req.headers["x-timestamp"],
95
+ };
96
+
97
+ try {
98
+ // Check if this is a test environment where we should bypass signature verification
99
+ // Get the peer public key from the state manager
100
+ const peerBase64urlJwkPublicKey = stateManager.getPeerBase64urlJwkPublicKey();
101
+
102
+ // If the peer public key is not available and we're not in test mode, return an error
103
+ if (!peerBase64urlJwkPublicKey) {
104
+ logger.warnWithContext("Peer public key not available in state manager", logContext);
105
+
106
+ // On main, we need the key
107
+ if (isStrictEnvironment()) {
108
+ logger.errorWithContext("Peer public key not available in main environment", logContext);
109
+ return sendError(res, {
110
+ statusCode: 500,
111
+ requestId,
112
+ code: "PEER_KEY_UNAVAILABLE",
113
+ message: "Peer public key not available"
114
+ });
115
+ }
116
+
117
+ // In development or test, we'll continue without the key and skip verification
118
+ logger.infoWithContext("Continuing without peer public key in non-main environment", {
119
+ ...logContext,
120
+ environment: getNodeEnv()
121
+ });
122
+ }
123
+
124
+ if (peerBase64urlJwkPublicKey) {
125
+ // Log that we're using the peer public key
126
+ logger.infoWithContext("Using peer public key from state manager", {
127
+ ...logContext,
128
+ keyFormat: "JWK",
129
+ keyFound: true
130
+ });
131
+
132
+ try {
133
+ logger.debugWithContext("Processing peer public key", {
134
+ ...logContext,
135
+ keyLength: peerBase64urlJwkPublicKey ? peerBase64urlJwkPublicKey.length : 0,
136
+ keyFormat: "base64url_encoded_hex"
137
+ });
138
+
139
+ // The key is already in base64url format and should be decoded directly to bytes
140
+ req.peer_bytes_ed25519_public_key = new Uint8Array(
141
+ Buffer.from(peerBase64urlJwkPublicKey, "base64url")
142
+ );
143
+ req.server_bytes_ed25519_public_key = req.peer_bytes_ed25519_public_key;
144
+ req.server_public_key_base64url = peerBase64urlJwkPublicKey;
145
+
146
+ logger.debugWithContext("Processed peer public key", {
147
+ ...logContext,
148
+ keyLength: req.peer_bytes_ed25519_public_key.length,
149
+ keyFormat: "base64url_decoded_to_bytes"
150
+ });
151
+ } catch (jwkError) {
152
+ logger.errorWithContext("Error converting JWK peer public key", {
153
+ ...logContext,
154
+ error: jwkError.message
155
+ });
156
+ return sendError(res, {
157
+ statusCode: 500,
158
+ requestId,
159
+ code: "PEER_KEY_PROCESSING_ERROR",
160
+ message: "Error processing peer public key",
161
+ details: { cause: jwkError.message }
162
+ });
163
+ }
164
+ }
165
+
166
+ next();
167
+ } catch (error) {
168
+ logger.errorWithContext("Error extracting server public key", {
169
+ ...logContext,
170
+ error: error.message,
171
+ stack: error.stack,
172
+ });
173
+ return sendError(res, {
174
+ statusCode: 500,
175
+ requestId,
176
+ code: "SERVER_CONFIG_ERROR",
177
+ message: "Server configuration error"
178
+ });
179
+ }
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Create middleware to authenticate webhook requests
185
+ * @returns {Function} Express middleware
186
+ */
187
+ function createWebhookAuthenticationMiddleware() {
188
+ return async (req, res, next) => {
189
+ const requestId = crypto.randomUUID();
190
+ const logContext = {
191
+ requestId,
192
+ apiEndpoint: req.path,
193
+ method: req.method,
194
+ headers: Object.keys(req.headers),
195
+ bodyKeys: Object.keys(req.jsonBody || req.body || {}),
196
+ bodySize: req.jsonBody ? JSON.stringify(req.jsonBody).length : 0,
197
+ };
198
+
199
+ try {
200
+ const signature_hex_ofpayload = req.headers["x-signature"];
201
+ const timestamp = req.headers["x-timestamp"];
202
+
203
+ // Use the raw body that was captured by our middleware
204
+ const payload = req.rawBody;
205
+
206
+ if (!signature_hex_ofpayload || !timestamp || !payload) {
207
+ logger.debugWithContext("Missing required webhook authentication parameters", {
208
+ ...logContext,
209
+ hasSignature: !!signature_hex_ofpayload,
210
+ hasTimestamp: !!timestamp,
211
+ hasPayload: !!payload
212
+ });
213
+ return sendError(res, {
214
+ statusCode: 400,
215
+ requestId,
216
+ code: 'MISSING_AUTH_PARAMS',
217
+ message: "Missing required authentication parameters"
218
+ });
219
+ }
220
+
221
+ // Log the payload hash and signature for debugging
222
+ const payloadHash = crypto
223
+ .createHash("sha256")
224
+ .update(payload)
225
+ .digest("hex");
226
+
227
+ logger.debugWithContext("Webhook payload hash and signature", {
228
+ ...logContext,
229
+ payloadHash: payloadHash,
230
+ payloadWithTimestamp: payload + (timestamp || ''),
231
+ payloadWithTimestampHash: crypto
232
+ .createHash("sha256")
233
+ .update(payload + (timestamp || ''))
234
+ .digest("hex"),
235
+ signature: signature_hex_ofpayload,
236
+ timestamp: timestamp
237
+ });
238
+
239
+ // Update log context with body info
240
+ if (Array.isArray(req.body)) {
241
+ logContext.bodyIsArray = true;
242
+ logContext.bodyLength = req.body.length;
243
+ } else {
244
+ logContext.bodyIsArray = false;
245
+ logContext.bodyKeys = Object.keys(req.body || {});
246
+ }
247
+
248
+ logContext.bodySize = payload.length;
249
+ logContext.hasSignature = !!signature_hex_ofpayload;
250
+ logContext.hasTimestamp = !!timestamp;
251
+
252
+ // Check if we have the server's public key
253
+ if (!req.server_public_key_base64url) {
254
+ // In test environments or with bypass flag, we might want to bypass verification
255
+ const isTestEnv = String(config.get('NODE_ENV', 'development')).toLowerCase() === 'test';
256
+ const bypassWebhookVerification = config.get('SECURITY_OPTIONS.BYPASS_WEBHOOK_VERIFICATION', false) === true;
257
+ if (isTestEnv || bypassWebhookVerification) {
258
+ logger.warnWithContext("Bypassing webhook authentication in test environment", logContext);
259
+ return next();
260
+ }
261
+
262
+ return sendError(res, {
263
+ statusCode: 500,
264
+ requestId,
265
+ code: "SERVER_CONFIG_ERROR",
266
+ message: "Server configuration error"
267
+ });
268
+ }
269
+
270
+ // Authenticate the webhook using the server's public key
271
+ logger.debugWithContext("Authenticating webhook signature", logContext);
272
+ const publicKeyBase64url = req.server_public_key_base64url;
273
+
274
+ // Call the authentication function with proper error handling
275
+ let authResult;
276
+ try {
277
+ authResult = await authenticate_webhook(
278
+ payload,
279
+ signature_hex_ofpayload,
280
+ timestamp,
281
+ publicKeyBase64url
282
+ );
283
+ } catch (authError) {
284
+ return sendError(res, {
285
+ statusCode: 500,
286
+ requestId,
287
+ code: "WEBHOOK_AUTH_ERROR",
288
+ message: "Webhook authentication error",
289
+ details: { cause: authError.message }
290
+ });
291
+ }
292
+
293
+ if (!authResult.isValid) {
294
+ logger.warnWithContext("Invalid webhook signature", {
295
+ ...logContext,
296
+ result: 'failure',
297
+ reason: 'Invalid webhook signature',
298
+ error: authResult.error?.message,
299
+ code: authResult.error?.code || 'UNKNOWN_ERROR'
300
+ });
301
+ return sendError(res, {
302
+ statusCode: 401,
303
+ requestId,
304
+ code: authResult.error?.code || 'WEBHOOK_SIGNATURE_INVALID',
305
+ message: authResult.error?.message || "Invalid webhook signature"
306
+ });
307
+ }
308
+
309
+ logger.infoWithContext("Webhook authenticated successfully", {
310
+ ...logContext,
311
+ authDuration: authResult.duration,
312
+ component: "WebhookHandler"
313
+ });
314
+
315
+ // Store authentication result for later use
316
+ req.webhookAuthResult = authResult;
317
+
318
+ next();
319
+ } catch (error) {
320
+ logger.errorWithContext("Error authenticating webhook", {
321
+ ...logContext,
322
+ error: error.message,
323
+ stack: error.stack
324
+ });
325
+ return sendError(res, {
326
+ statusCode: 500,
327
+ requestId,
328
+ code: "WEBHOOK_AUTHENTICATION_ERROR",
329
+ message: "Webhook authentication error"
330
+ });
331
+ }
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Process a webhook event and extract its data
337
+ * @param {Object} req - Express request object
338
+ * @param {Object} logContext - Logging context
339
+ * @returns {Object} Extracted event data
340
+ */
341
+ function processWebhookEvent(req, logContext = {}) {
342
+ try {
343
+ // Check if the body is valid before attempting to destructure
344
+ if (!req.body || typeof req.body !== 'object') {
345
+ logger.errorWithContext("Invalid webhook payload format", {
346
+ ...logContext,
347
+ component: "WebhookHandler",
348
+ bodyType: typeof req.body,
349
+ bodyIsNull: req.body === null,
350
+ contentType: req.headers['content-type']
351
+ });
352
+ return { error: "Invalid payload format" };
353
+ }
354
+
355
+ const { event, data, isError, timestamp: payloadTimestamp, requestId: payloadRequestId } = req.body;
356
+
357
+ const eventType = typeof event === "string" ? event.trim() : "";
358
+
359
+ if (!eventType) {
360
+ logger.errorWithContext("Webhook payload missing event type", {
361
+ ...logContext,
362
+ component: "WebhookHandler",
363
+ rawEventValue: event,
364
+ hasEventField: Object.prototype.hasOwnProperty.call(req.body, "event"),
365
+ });
366
+ return { error: "Event type is required but was not provided" };
367
+ }
368
+
369
+ logger.infoWithContext("Processing webhook payload", {
370
+ ...logContext,
371
+ component: "WebhookHandler",
372
+ event: eventType,
373
+ eventType,
374
+ isError,
375
+ payloadTimestamp,
376
+ payloadRequestId,
377
+ dataKeys: data ? Object.keys(data) : [],
378
+ dataType: typeof data,
379
+ dataSize: data ? JSON.stringify(data).length : 0
380
+ });
381
+
382
+ return {
383
+ type: eventType,
384
+ name: eventType,
385
+ event: eventType,
386
+ data,
387
+ isError,
388
+ timestamp: payloadTimestamp,
389
+ requestId: payloadRequestId,
390
+ error: null
391
+ };
392
+ } catch (error) {
393
+ logger.debugWithContext("Error processing webhook payload", {
394
+ ...logContext,
395
+ component: "WebhookHandler",
396
+ error: error.message,
397
+ stack: error.stack
398
+ });
399
+ return { error: error.message };
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Create a complete webhook handler for Express
405
+ * @param {Object} stateManager - State manager instance
406
+ * @param {Object} configuration - Configuration configuration
407
+ * @returns {Object} Webhook handler with middleware and utilities
408
+ */
409
+ function createWebhookHandler(stateManager, configuration = {}) {
410
+ const rawBodyParser = createRawBodyParser();
411
+ const webhookProcessingMiddleware = createWebhookProcessingMiddleware();
412
+ const publicKeyMiddleware = createPublicKeyMiddleware(stateManager);
413
+ const authenticationMiddleware = createWebhookAuthenticationMiddleware();
414
+
415
+ return {
416
+ // Middleware
417
+ rawBodyParser,
418
+ webhookProcessingMiddleware,
419
+ publicKeyMiddleware,
420
+ authenticationMiddleware,
421
+
422
+ // Utility functions
423
+ processWebhookEvent,
424
+
425
+ // Combined middleware for easy setup
426
+ middleware: [
427
+ rawBodyParser,
428
+ webhookProcessingMiddleware,
429
+ publicKeyMiddleware,
430
+ authenticationMiddleware
431
+ ],
432
+
433
+ // Helper to apply middleware based on route
434
+ applyMiddleware: (app, express, options = {}) => {
435
+ const endpoints = Array.isArray(options.endpoints) && options.endpoints.length > 0
436
+ ? options.endpoints
437
+ : ['/webhook'];
438
+ const normalizedEndpoints = endpoints.map((endpoint) => {
439
+ const endpointString = String(endpoint || '/webhook');
440
+ return endpointString.startsWith('/') ? endpointString : `/${endpointString}`;
441
+ });
442
+ const endpointSet = new Set(normalizedEndpoints);
443
+
444
+ // Apply raw body parser only to configured webhook routes
445
+ app.use((req, res, next) => {
446
+ if (endpointSet.has(req.path)) {
447
+ rawBodyParser(req, res, next);
448
+ } else {
449
+ express.json()(req, res, next);
450
+ }
451
+ });
452
+
453
+ // Apply webhook processing + key extraction middleware to all webhook routes
454
+ for (const endpoint of normalizedEndpoints) {
455
+ app.use(endpoint, webhookProcessingMiddleware);
456
+ app.use(endpoint, publicKeyMiddleware);
457
+ }
458
+
459
+ return app;
460
+ }
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Send a webhook notification with comprehensive logging
466
+ *
467
+ * @param {Object} data - Webhook envelope. Expected shape: { event: string, data?: any, isError?: boolean }
468
+ * @param {Object} req - Express request object (optional)
469
+ * @param {Object} options - Options object (optional)
470
+ * @param {string} options.endpoint - Target endpoint path (e.g., '/webhook', '/hooks/wake', '/hooks/agent'). Defaults to '/webhook'
471
+ * @returns {Promise<Object>} Webhook delivery result with requestId
472
+ */
473
+ async function send_webhook(data, req = null, options = {}) {
474
+ // Derive fields from envelope
475
+ const event = data && typeof data === 'object' ? (data.event || 'generic_event') : 'generic_event';
476
+ let isError = !!(data && data.isError);
477
+
478
+ // Always generate a new correlation ID
479
+ const requestId = ulid();
480
+
481
+ // Rebind data to the actual payload object (inner data if present, else entire envelope)
482
+ if (data && Object.prototype.hasOwnProperty.call(data, 'data')) {
483
+ data = data.data;
484
+ }
485
+ const startTime = Date.now();
486
+
487
+ // Create a context object for consistent logging
488
+ const webhookContext = {
489
+ event,
490
+ requestId,
491
+ isError,
492
+ dataType: typeof data,
493
+ operation: "webhook",
494
+ method: "send_webhook",
495
+ component: "WebhookHandler"
496
+ };
497
+
498
+ // Create base context for all logs in this function
499
+ const baseContext = createLogContext("RoditAuth", "send_webhook", {
500
+ requestId,
501
+ event,
502
+ isError,
503
+ dataSize: typeof data === "object" ? JSON.stringify(data).length : "unknown"
504
+ });
505
+
506
+ // Log the webhook attempt
507
+ logger.debugWithContext("Starting webhook delivery", baseContext);
508
+
509
+ // Also log with the infoWithContext pattern used in cruda.js
510
+ logger.infoWithContext("Sending webhook", {
511
+ ...webhookContext,
512
+ status: "attempt",
513
+ eventType: event
514
+ });
515
+
516
+ try {
517
+ // Webhook URL must come from peer JWT token only
518
+ if (!req || !req.user || !req.user.rodit_webhookurl) {
519
+ const duration = Date.now() - startTime;
520
+
521
+ logger.warnWithContext("Peer JWT webhook URL missing", {
522
+ ...baseContext,
523
+ duration,
524
+ hasReq: !!req,
525
+ hasReqUser: !!(req && req.user),
526
+ hasWebhookUrl: !!(req && req.user && req.user.rodit_webhookurl)
527
+ });
528
+
529
+ // Emit metrics for dashboards
530
+ logger.metric &&
531
+ logger.metric("webhook_delivery_duration_ms", duration, {
532
+ component: "WebhookHandler",
533
+ success: false,
534
+ event,
535
+ error: "WEBHOOK_URL_MISSING",
536
+ });
537
+ logger.metric &&
538
+ logger.metric("webhook_delivery_failures_total", 1, {
539
+ component: "WebhookHandler",
540
+ reason: "PEER_JWT_MISSING",
541
+ event,
542
+ });
543
+
544
+ // Log error with new logErrorWithMetrics helper
545
+ logErrorWithMetrics(
546
+ "Peer JWT webhook URL missing",
547
+ createLogContext(
548
+ "WebhookHandler",
549
+ "webhook_url_error",
550
+ {
551
+ ...webhookContext,
552
+ status: "error"
553
+ }
554
+ ),
555
+ new Error("Peer JWT webhook URL not available"),
556
+ "webhook_error_count",
557
+ { error_type: "peer_jwt_missing" }
558
+ );
559
+
560
+ return {
561
+ isValid: false,
562
+ error: {
563
+ code: "WEBHOOK_URL_MISSING",
564
+ message: "Webhook URL not available in peer JWT token",
565
+ requestId,
566
+ },
567
+ };
568
+ }
569
+
570
+ // Use the webhook URL from the peer's JWT token
571
+ const webhookUrl = req.user.rodit_webhookurl;
572
+
573
+ // Extract endpoint from options (defaults to /webhook)
574
+ const endpoint = options.endpoint || '/webhook';
575
+
576
+ logger.debugWithContext("Using webhook URL from peer identity context", {
577
+ ...baseContext,
578
+ webhookSource: "peer_context",
579
+ webhookUrl,
580
+ endpoint
581
+ });
582
+
583
+ // Normalize base URL and endpoint so we always produce exactly one slash
584
+ // between host and path (e.g. https://host/hooks/wake).
585
+ const cleanWebhookUrl = webhookUrl
586
+ .replace(/^(https?:\/\/)/, "")
587
+ .replace(/\/+$/, "");
588
+ const normalizedEndpoint = `/${String(endpoint || "/webhook").replace(/^\/+/, "")}`;
589
+ const formattedWebhookUrl = `https://${cleanWebhookUrl}${normalizedEndpoint}`;
590
+
591
+ logger.debugWithContext("Webhook URL details", {
592
+ ...baseContext,
593
+ rawWebhookUrl: webhookUrl,
594
+ endpoint,
595
+ formattedWebhookUrl
596
+ });
597
+
598
+ const timestamp = Date.now();
599
+
600
+ // Ensure data is serializable before stringifying
601
+ let sanitizedData;
602
+ try {
603
+ // Test if data can be properly serialized
604
+ if (typeof data === 'object' && data !== null) {
605
+ // Create a deep copy to avoid modifying the original data
606
+ sanitizedData = JSON.parse(JSON.stringify(data));
607
+ } else if (data === undefined || data === null) {
608
+ // Handle null/undefined explicitly
609
+ sanitizedData = null;
610
+ } else if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
611
+ // Primitive types can be used directly
612
+ sanitizedData = data;
613
+ } else {
614
+ // For other types (functions, symbols, etc.), create a string representation
615
+ sanitizedData = {
616
+ type: typeof data,
617
+ stringValue: String(data)
618
+ };
619
+ }
620
+ } catch (serializeError) {
621
+ // If data can't be serialized, create a simplified version
622
+ logger.debugWithContext("Data serialization failed, creating simplified version", {
623
+ ...baseContext,
624
+ error: serializeError.message
625
+ });
626
+
627
+ // Create a simplified version with basic properties
628
+ sanitizedData = {
629
+ type: typeof data,
630
+ summary: "Data could not be serialized to JSON",
631
+ error: serializeError.message
632
+ };
633
+ }
634
+
635
+ // Create the payload object
636
+ const payloadObj = {
637
+ event,
638
+ data: sanitizedData,
639
+ isError,
640
+ requestId,
641
+ };
642
+
643
+ // Create the payload with consistent JSON formatting
644
+ // Sort keys to ensure canonical representation regardless of object creation order
645
+ const payload = JSON.stringify(payloadObj, function(key, value) {
646
+ // Handle special numeric values consistently
647
+ if (typeof value === 'number') {
648
+ if (isNaN(value)) return 'NaN';
649
+ if (value === Infinity) return 'Infinity';
650
+ if (value === -Infinity) return '-Infinity';
651
+ }
652
+ return value;
653
+ }, 0);
654
+
655
+ // Ensure consistent handling of Unicode characters
656
+ const normalizedPayload = payload.normalize('NFC');
657
+
658
+ logger.debug("Preparing webhook payload", {
659
+ component: "WebhookHandler",
660
+ method: "send_webhook",
661
+ requestId,
662
+ payloadSize: normalizedPayload.length,
663
+ event,
664
+ });
665
+
666
+ // Create the string to hash: payload + timestamp
667
+ // This binds the timestamp to the payload for signature verification
668
+ const payloadWithTimestamp = normalizedPayload + timestamp.toString();
669
+
670
+ logger.debugWithContext("Creating payload+timestamp string for signing", {
671
+ ...baseContext,
672
+ payloadSize: normalizedPayload.length,
673
+ timestampLength: timestamp.toString().length,
674
+ combinedLength: payloadWithTimestamp.length
675
+ });
676
+
677
+ // Generate hash of payload+timestamp
678
+ const sha256_ofpayload = crypto
679
+ .createHash("sha256")
680
+ .update(payloadWithTimestamp)
681
+ .digest();
682
+
683
+ // Log hash details for visibility
684
+ logger.debug("Webhook hash details", {
685
+ component: "WebhookHandler",
686
+ method: "send_webhook",
687
+ requestId,
688
+ hashHex: sha256_ofpayload.toString('hex'),
689
+ hashLength: sha256_ofpayload.length
690
+ });
691
+
692
+ const config_own_rodit = await stateManager.getConfigOwnRodit();
693
+ if (!config_own_rodit || !config_own_rodit.own_rodit_bytes_private_key) {
694
+ throw new Error("Own RODiT private key unavailable for webhook signing");
695
+ }
696
+
697
+ logger.debugWithContext("Creating signature", {
698
+ ...baseContext,
699
+ hasPrivateKey: !!config_own_rodit.own_rodit_bytes_private_key
700
+ });
701
+
702
+ // Convert private key and generate signature
703
+ const own_rodit_private_key = new Uint8Array(
704
+ config_own_rodit.own_rodit_bytes_private_key
705
+ );
706
+
707
+ // Log the public key from state manager
708
+ const publicKey = stateManager.getOwnBase64urlJwkPublicKey();
709
+
710
+ // Log the key in multiple formats for precise comparison
711
+ logger.debug("Webhook signing key information", {
712
+ component: "WebhookHandler",
713
+ method: "send_webhook",
714
+ requestId,
715
+ publicKeyBase64url: publicKey,
716
+ publicKeyHex: publicKey ? Buffer.from(publicKey, 'base64url').toString('hex') : null,
717
+ keyLength: publicKey ? Buffer.from(publicKey, 'base64url').length : 0
718
+ });
719
+
720
+ const signatureStartTime = Date.now();
721
+ const signature_ofpayload = nacl.sign.detached(
722
+ sha256_ofpayload,
723
+ own_rodit_private_key
724
+ );
725
+ const signatureDuration = Date.now() - signatureStartTime;
726
+
727
+ // Log signature generation metrics
728
+ logger.metric &&
729
+ logger.metric("signature_generation_duration_ms", signatureDuration, {
730
+ component: "WebhookHandler",
731
+ });
732
+
733
+ const signature_hex_ofpayload =
734
+ Buffer.from(signature_ofpayload).toString("hex");
735
+
736
+ // Log signature details for visibility and comparison with client logs
737
+ logger.debugWithContext("Webhook signature details", {
738
+ ...baseContext,
739
+ signatureHex: signature_hex_ofpayload,
740
+ signatureBase64: Buffer.from(signature_ofpayload).toString("base64"),
741
+ signatureBase64url: Buffer.from(signature_ofpayload).toString("base64").replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
742
+ signatureLength: signature_hex_ofpayload.length,
743
+ signatureByteLength: signature_ofpayload.length
744
+ });
745
+
746
+ // Log the exact hash that was signed for comparison
747
+ logger.debugWithContext("Webhook hash that was signed", {
748
+ ...baseContext,
749
+ hashHex: Buffer.from(sha256_ofpayload).toString('hex'),
750
+ hashBase64: Buffer.from(sha256_ofpayload).toString('base64'),
751
+ hashBase64url: Buffer.from(sha256_ofpayload).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
752
+ hashLength: sha256_ofpayload.length
753
+ });
754
+
755
+ logger.debugWithContext("Sending webhook request", {
756
+ ...baseContext,
757
+ webhookUrl: formattedWebhookUrl,
758
+ timestamp: timestamp.toString(),
759
+ payload: ['debug', 'trace'].includes(config.get('LOG_LEVEL', 'info')) ? payload : undefined, // Only log payload in debug mode
760
+ signatureHex: signature_hex_ofpayload
761
+ });
762
+
763
+ // Prepare headers for the webhook request
764
+ // Only include webhook-specific authentication headers (digital signature)
765
+ // No API bearer tokens - webhook security relies on cryptographic signatures
766
+ const headers = {
767
+ "Content-Type": "application/json",
768
+ "X-Signature": signature_hex_ofpayload,
769
+ "X-Timestamp": timestamp.toString(),
770
+ "X-Request-ID": requestId
771
+ };
772
+
773
+ // Log the exact headers being sent
774
+ logger.debugWithContext("Webhook request headers", {
775
+ ...baseContext,
776
+ headers: headers,
777
+ signatureHeader: signature_hex_ofpayload,
778
+ timestampHeader: timestamp.toString()
779
+ });
780
+
781
+ // SELF-VERIFICATION: Call authenticate_webhook with the same parameters the client will use
782
+ // This helps determine if the issue is in the signature generation/verification or in the data flow
783
+ try {
784
+ logger.info("Performing self-verification before sending webhook", {
785
+ component: "WebhookHandler",
786
+ method: "send_webhook",
787
+ requestId
788
+ });
789
+
790
+ // Get our own public key for verification
791
+ const publicKeyForVerification = stateManager.getOwnBase64urlJwkPublicKey();
792
+
793
+ // Call authenticate_webhook with the same parameters the client will receive
794
+ const verificationResult = await authenticate_webhook(
795
+ payload, // The exact payload being sent
796
+ signature_hex_ofpayload, // The signature in hex format
797
+ timestamp.toString(), // The timestamp as a string
798
+ publicKeyForVerification // Our own public key for verification
799
+ );
800
+
801
+ logger.infoWithContext("Self-verification result", {
802
+ ...baseContext,
803
+ selfVerificationSuccess: verificationResult.isValid,
804
+ selfVerificationError: verificationResult.error ? verificationResult.error.message : null
805
+ });
806
+
807
+ if (!verificationResult.isValid) {
808
+ logger.warnWithContext("Self-verification failed - client verification will likely fail too", {
809
+ ...baseContext,
810
+ error: verificationResult.error ? verificationResult.error.message : "Unknown verification error"
811
+ });
812
+ }
813
+ } catch (verificationError) {
814
+ logErrorWithMetrics(
815
+ "Error during self-verification",
816
+ baseContext,
817
+ verificationError,
818
+ "webhook_verification_error",
819
+ { error_type: "self_verification_error" }
820
+ );
821
+ }
822
+
823
+ // Configure HTTPS agent to skip TLS verification if configured
824
+ // This is necessary when webhook destinations use self-signed certificates
825
+ // Since mutual authentication via digital signatures is already in place,
826
+ // skipping TLS verification is safe in this context
827
+ const skipTlsVerify = config.has('SECURITY_OPTIONS.WEBHOOK_TLS_SKIP_VERIFY')
828
+ ? String(config.get('SECURITY_OPTIONS.WEBHOOK_TLS_SKIP_VERIFY')).toLowerCase() === 'true'
829
+ : false;
830
+
831
+ let fetchOptions = {
832
+ method: "POST",
833
+ headers: headers,
834
+ body: payload,
835
+ };
836
+
837
+ if (skipTlsVerify) {
838
+ // Create custom undici Agent that accepts self-signed certificates
839
+ // Node.js fetch uses undici under the hood and requires 'dispatcher' option
840
+ const undiciAgent = new Agent({
841
+ connect: {
842
+ rejectUnauthorized: false
843
+ }
844
+ });
845
+ fetchOptions.dispatcher = undiciAgent;
846
+
847
+ logger.debugWithContext("Webhook TLS verification disabled", {
848
+ ...baseContext,
849
+ skipTlsVerify: true,
850
+ reason: "SECURITY_OPTIONS.WEBHOOK_TLS_SKIP_VERIFY=true"
851
+ });
852
+ }
853
+
854
+ // Send webhook request
855
+ const fetchStartTime = Date.now();
856
+ const response = await fetch(formattedWebhookUrl, fetchOptions);
857
+ const fetchDuration = Date.now() - fetchStartTime;
858
+
859
+ // Log fetch duration metrics
860
+ logger.metric("webhook_http_request_duration_ms", fetchDuration, {
861
+ component: "WebhookHandler",
862
+ success: response.ok,
863
+ status: response.status,
864
+ event,
865
+ });
866
+
867
+ if (!response.ok) {
868
+ const duration = Date.now() - startTime;
869
+
870
+ logErrorWithMetrics(
871
+ "Webhook delivery failed",
872
+ {
873
+ ...baseContext,
874
+ duration,
875
+ status: response.status,
876
+ statusText: response.statusText,
877
+ webhookUrl: formattedWebhookUrl
878
+ },
879
+ new Error(`HTTP ${response.status}: ${response.statusText}`),
880
+ "webhook_delivery_error",
881
+ { error_type: "http_error", status: response.status }
882
+ );
883
+
884
+ // Emit metrics for dashboards
885
+ logger.metric("webhook_delivery_duration_ms", duration, {
886
+ component: "WebhookHandler",
887
+ success: false,
888
+ event,
889
+ error: "HTTP_ERROR",
890
+ status: response.status,
891
+ });
892
+ logger.metric("webhook_delivery_failures_total", 1, {
893
+ component: "WebhookHandler",
894
+ reason: "HTTP_ERROR",
895
+ status: response.status,
896
+ event,
897
+ });
898
+
899
+ throw new Error(`HTTP error! status: ${response.status}`);
900
+ }
901
+
902
+ await response.text();
903
+
904
+ const duration = Date.now() - startTime;
905
+ logger.infoWithContext("Webhook delivered successfully", {
906
+ ...baseContext,
907
+ duration,
908
+ webhookUrl: formattedWebhookUrl,
909
+ status: response.status
910
+ });
911
+
912
+ // Emit metrics for dashboards
913
+ logger.metric("webhook_delivery_duration_ms", duration, {
914
+ component: "WebhookHandler",
915
+ success: true,
916
+ event,
917
+ });
918
+ logger.metric("successful_webhook_deliveries_total", 1, {
919
+ component: "WebhookHandler",
920
+ event,
921
+ });
922
+
923
+ // Removed test-mode DB recording on success
924
+
925
+ // Log success with infoWithContext pattern
926
+ logger.infoWithContext("Webhook sent successfully", {
927
+ ...webhookContext,
928
+ status: "success"
929
+ });
930
+
931
+ // Return success result with requestId for tracing
932
+ return {
933
+ isValid: true,
934
+ message: "Webhook sent successfully",
935
+ requestId,
936
+ duration,
937
+ };
938
+ } catch (error) {
939
+ const duration = Date.now() - startTime;
940
+
941
+ logErrorWithMetrics(
942
+ "Webhook send failed",
943
+ {
944
+ ...baseContext,
945
+ duration,
946
+ errorCode: error.code || "UNKNOWN_ERROR",
947
+ isError,
948
+ operation: "webhook",
949
+ status: "failed"
950
+ },
951
+ error,
952
+ "webhook_delivery_error",
953
+ { error_type: "network_error" }
954
+ );
955
+
956
+ // Emit metrics for dashboards
957
+ logger.metric("webhook_delivery_duration_ms", duration, {
958
+ component: "WebhookHandler",
959
+ success: false,
960
+ event,
961
+ error: error.constructor.name,
962
+ });
963
+ logger.metric("webhook_delivery_errors_total", 1, {
964
+ component: "WebhookHandler",
965
+ error: error.constructor.name,
966
+ event,
967
+ });
968
+
969
+ // Log error with errorWithContext pattern
970
+ logger.errorWithContext && logger.errorWithContext(
971
+ "Webhook send failed",
972
+ {
973
+ ...webhookContext,
974
+ status: "failed",
975
+ errorMessage: error.message
976
+ },
977
+ error
978
+ );
979
+
980
+ // Return error result with requestId for tracing
981
+ return {
982
+ isValid: false,
983
+ error: {
984
+ code: "WEBHOOK_SEND_ERROR",
985
+ message: `Failed to send webhook: ${error.message}`,
986
+ requestId,
987
+ },
988
+ };
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Base class for webhook event handlers
994
+ */
995
+ class WebhookEventHandler {
996
+ /**
997
+ * Create a new webhook event handler
998
+ * @param {Object} configuration - Configuration configuration
999
+ */
1000
+ constructor(configuration = {}) {
1001
+ this.configuration = configuration;
1002
+ }
1003
+
1004
+ /**
1005
+ * Handle a webhook event
1006
+ * @param {Object} event - Event data
1007
+ * @param {Object} req - Express request object
1008
+ * @param {Object} res - Express response object
1009
+ * @returns {Promise<Object>} Response data
1010
+ */
1011
+ async handleEvent(event, req, res) {
1012
+ throw new Error("Method not implemented");
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * Handler for test configuration update events
1018
+ */
1019
+ class TestConfigUpdateHandler extends WebhookEventHandler {
1020
+ /**
1021
+ * Create a new test configuration update handler
1022
+ * @param {Object} configManager - Configuration manager
1023
+ * @param {Object} configuration - Configuration configuration
1024
+ */
1025
+ constructor(configManager, configuration = {}) {
1026
+ super(configuration);
1027
+ this.configManager = configManager;
1028
+ }
1029
+
1030
+ /**
1031
+ * Handle a test configuration update event
1032
+ * @param {Object} event - Event data
1033
+ * @param {Object} req - Express request object
1034
+ * @param {Object} res - Express response object
1035
+ * @returns {Promise<Object>} Response data
1036
+ */
1037
+ async handleEvent(event, req, res) {
1038
+ const logContext = createLogContext({
1039
+ component: "TestConfigUpdateHandler",
1040
+ event: "handleEvent",
1041
+ requestId: req.requestId || ulid(),
1042
+ eventType: event.type,
1043
+ });
1044
+
1045
+ try {
1046
+ if (!this.configManager) {
1047
+ const error = new Error("Config manager is required but not provided");
1048
+ logger.errorWithContext(error.message, logContext, error);
1049
+ return {
1050
+ success: false,
1051
+ error: error.message,
1052
+ };
1053
+ }
1054
+
1055
+ // Update configuration
1056
+ await this.configManager.updateConfig(event.data);
1057
+
1058
+ logger.infoWithContext("Test configuration updated successfully", logContext);
1059
+ return {
1060
+ success: true,
1061
+ message: "Test configuration updated successfully",
1062
+ };
1063
+ } catch (error) {
1064
+ logger.errorWithContext(error.message, logContext, error);
1065
+ return {
1066
+ success: false,
1067
+ error: error.message,
1068
+ };
1069
+ }
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * Handler for test suite execution events
1075
+ */
1076
+ class TestSuiteHandler extends WebhookEventHandler {
1077
+ /**
1078
+ * Create a new test suite handler
1079
+ * @param {Function} runTestSuite - Function to run a test suite
1080
+ * @param {Object} configuration - Configuration configuration
1081
+ */
1082
+ constructor(runTestSuite, configuration = {}) {
1083
+ super(configuration);
1084
+ this.runTestSuite = runTestSuite;
1085
+ }
1086
+
1087
+ /**
1088
+ * Handle a test suite execution event
1089
+ * @param {Object} event - Event data
1090
+ * @param {Object} req - Express request object
1091
+ * @param {Object} res - Express response object
1092
+ * @returns {Promise<Object>} Response data
1093
+ */
1094
+ async handleEvent(event, req, res) {
1095
+ const logContext = createLogContext({
1096
+ component: "TestSuiteHandler",
1097
+ event: "handleEvent",
1098
+ requestId: req.requestId || ulid(),
1099
+ eventType: event.type,
1100
+ });
1101
+
1102
+ try {
1103
+ if (!this.runTestSuite) {
1104
+ const error = new Error("runTestSuite function is required but not provided");
1105
+ logger.errorWithContext(error.message, logContext, error);
1106
+ return {
1107
+ success: false,
1108
+ error: error.message,
1109
+ };
1110
+ }
1111
+
1112
+ // Extract test configuration from event data
1113
+ const testOptions = event.data || {};
1114
+
1115
+ // Run the test suite
1116
+ const testResults = await this.runTestSuite(testOptions);
1117
+
1118
+ logger.infoWithContext("Test suite executed successfully", logContext);
1119
+ return {
1120
+ success: true,
1121
+ message: "Test suite executed successfully",
1122
+ results: testResults,
1123
+ };
1124
+ } catch (error) {
1125
+ logger.errorWithContext(error.message, logContext, error);
1126
+ return {
1127
+ success: false,
1128
+ error: error.message,
1129
+ };
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+ /**
1135
+ * Handler for single test execution events
1136
+ */
1137
+ class SingleTestHandler extends WebhookEventHandler {
1138
+ /**
1139
+ * Create a new single test handler
1140
+ * @param {Function} runSingleTest - Function to run a single test
1141
+ * @param {Object} configuration - Configuration configuration
1142
+ */
1143
+ constructor(runSingleTest, configuration = {}) {
1144
+ super(configuration);
1145
+ this.runSingleTest = runSingleTest;
1146
+ }
1147
+
1148
+ /**
1149
+ * Handle a single test execution event
1150
+ * @param {Object} event - Event data
1151
+ * @param {Object} req - Express request object
1152
+ * @param {Object} res - Express response object
1153
+ * @returns {Promise<Object>} Response data
1154
+ */
1155
+ async handleEvent(event, req, res) {
1156
+ const logContext = createLogContext({
1157
+ component: "SingleTestHandler",
1158
+ event: "handleEvent",
1159
+ requestId: req.requestId || ulid(),
1160
+ eventType: event.type,
1161
+ });
1162
+
1163
+ try {
1164
+ if (!this.runSingleTest) {
1165
+ const error = new Error("runSingleTest function is required but not provided");
1166
+ logger.errorWithContext(error.message, logContext, error);
1167
+ return {
1168
+ success: false,
1169
+ error: error.message,
1170
+ };
1171
+ }
1172
+
1173
+ // Extract test configuration from event data
1174
+ const testOptions = event.data || {};
1175
+ const testName = testOptions.testName;
1176
+
1177
+ if (!testName) {
1178
+ const error = new Error("testName is required but not provided");
1179
+ logger.errorWithContext(error.message, logContext, error);
1180
+ return {
1181
+ success: false,
1182
+ error: error.message,
1183
+ };
1184
+ }
1185
+
1186
+ // Run the single test
1187
+ const testResults = await this.runSingleTest(testName, testOptions);
1188
+
1189
+ logger.infoWithContext("Single test executed successfully", logContext);
1190
+ return {
1191
+ success: true,
1192
+ message: `Test '${testName}' executed successfully`,
1193
+ results: testResults,
1194
+ };
1195
+ } catch (error) {
1196
+ logger.errorWithContext(error.message, logContext, error);
1197
+ return {
1198
+ success: false,
1199
+ error: error.message,
1200
+ };
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ /**
1206
+ * Handler for comment events
1207
+ */
1208
+ class CommentEventHandler extends WebhookEventHandler {
1209
+ /**
1210
+ * Create a new comment event handler
1211
+ * @param {Object} configuration - Configuration configuration
1212
+ */
1213
+ constructor(configuration = {}) {
1214
+ super(configuration);
1215
+ }
1216
+
1217
+ /**
1218
+ * Handle a comment event
1219
+ * @param {Object} event - Event data
1220
+ * @param {Object} req - Express request object
1221
+ * @param {Object} res - Express response object
1222
+ * @returns {Promise<Object>} Response data
1223
+ */
1224
+ async handleEvent(event, req, res) {
1225
+ const logContext = createLogContext({
1226
+ component: "CommentEventHandler",
1227
+ event: "handleEvent",
1228
+ requestId: req.requestId || ulid(),
1229
+ eventType: event.type,
1230
+ });
1231
+
1232
+ try {
1233
+ // Log the comment event
1234
+ logger.infoWithContext("Comment event received", {
1235
+ ...logContext,
1236
+ eventType: event.type,
1237
+ commentId: event.data?.commentId,
1238
+ userId: event.data?.userId,
1239
+ testId: event.data?.testId,
1240
+ });
1241
+
1242
+ // For now, we just acknowledge receipt of the event
1243
+ // In the future, this could store comments in a database or trigger other actions
1244
+ return {
1245
+ success: true,
1246
+ message: `Comment event '${event.type}' processed successfully`,
1247
+ eventType: event.type,
1248
+ timestamp: new Date().toISOString(),
1249
+ };
1250
+ } catch (error) {
1251
+ logger.errorWithContext(error.message, logContext, error);
1252
+ return {
1253
+ success: false,
1254
+ error: error.message,
1255
+ };
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ /**
1261
+ * Factory for creating webhook event handlers
1262
+ */
1263
+ class WebhookEventHandlerFactory {
1264
+ /**
1265
+ * Create a new webhook event handler factory
1266
+ * @param {Object} dependencies - Dependencies for handlers
1267
+ * @param {Object} configuration - Configuration configuration
1268
+ */
1269
+ constructor(dependencies = {}, configuration = {}) {
1270
+ this.dependencies = dependencies;
1271
+ this.configuration = configuration;
1272
+ this.handlers = new Map();
1273
+
1274
+ // Register default handlers if dependencies are provided
1275
+ if (dependencies.configManager) {
1276
+ this.registerHandler("test_config_update", new TestConfigUpdateHandler(dependencies.configManager, configuration));
1277
+ }
1278
+
1279
+ if (dependencies.runTestSuite) {
1280
+ this.registerHandler("run_test_suite", new TestSuiteHandler(dependencies.runTestSuite, configuration));
1281
+ }
1282
+
1283
+ if (dependencies.runSingleTest) {
1284
+ this.registerHandler("run_single_test", new SingleTestHandler(dependencies.runSingleTest, configuration));
1285
+ }
1286
+
1287
+ // Register comment event handlers
1288
+ const commentHandler = new CommentEventHandler(configuration);
1289
+ this.registerHandler("comment_created", commentHandler);
1290
+ this.registerHandler("comment_updated", commentHandler);
1291
+ this.registerHandler("comment_deleted", commentHandler);
1292
+ this.registerHandler("comments_listed", commentHandler);
1293
+ this.registerHandler("create_comment_error", commentHandler);
1294
+ this.registerHandler("update_comment_error", commentHandler);
1295
+ this.registerHandler("delete_comment_error", commentHandler);
1296
+ this.registerHandler("read_comment_error", commentHandler);
1297
+ }
1298
+
1299
+ /**
1300
+ * Register a handler for an event type
1301
+ * @param {string} eventType - Event type
1302
+ * @param {WebhookEventHandler} handler - Event handler
1303
+ */
1304
+ registerHandler(eventType, handler) {
1305
+ this.handlers.set(eventType, handler);
1306
+ }
1307
+
1308
+ /**
1309
+ * Get a handler for an event type
1310
+ * @param {string} eventType - Event type
1311
+ * @returns {WebhookEventHandler|null} Event handler or null if not found
1312
+ */
1313
+ getHandler(eventType) {
1314
+ return this.handlers.get(eventType) || null;
1315
+ }
1316
+
1317
+ /**
1318
+ * Handle a webhook event
1319
+ * @param {Object} event - Event data
1320
+ * @param {Object} req - Express request object
1321
+ * @param {Object} res - Express response object
1322
+ * @returns {Promise<Object>} Response data
1323
+ */
1324
+ async handleEvent(event, req, res) {
1325
+ const logContext = createLogContext({
1326
+ component: "WebhookEventHandlerFactory",
1327
+ event: "handleEvent",
1328
+ requestId: req.requestId || ulid(),
1329
+ eventType: event.type,
1330
+ });
1331
+
1332
+ try {
1333
+ const eventType = event.type;
1334
+
1335
+ if (!eventType) {
1336
+ const error = new Error("Event type is required but not provided");
1337
+ logger.errorWithContext(error.message, logContext, error);
1338
+ return {
1339
+ success: false,
1340
+ error: error.message,
1341
+ };
1342
+ }
1343
+
1344
+ const handler = this.getHandler(eventType);
1345
+
1346
+ if (!handler) {
1347
+ logger.infoWithContext("No handler registered for webhook event type; acknowledging event", {
1348
+ ...logContext,
1349
+ eventType,
1350
+ mode: "noop-ack"
1351
+ });
1352
+ return {
1353
+ success: true,
1354
+ ignored: true,
1355
+ message: `No handler registered for event type: ${eventType}`,
1356
+ eventType
1357
+ };
1358
+ }
1359
+
1360
+ return await handler.handleEvent(event, req, res);
1361
+ } catch (error) {
1362
+ logger.errorWithContext(error.message, logContext, error);
1363
+ return {
1364
+ success: false,
1365
+ error: error.message,
1366
+ };
1367
+ }
1368
+ }
1369
+ }
1370
+
1371
+ module.exports = {
1372
+ // Original exports from webhookhandlermw.js
1373
+ createRawBodyParser,
1374
+ createWebhookProcessingMiddleware,
1375
+ createPublicKeyMiddleware,
1376
+ createWebhookAuthenticationMiddleware,
1377
+ processWebhookEvent,
1378
+ createWebhookHandler,
1379
+ send_webhook,
1380
+
1381
+ // Added exports from eventhandler.js
1382
+ WebhookEventHandler,
1383
+ TestConfigUpdateHandler,
1384
+ TestSuiteHandler,
1385
+ SingleTestHandler,
1386
+ CommentEventHandler,
1387
+ WebhookEventHandlerFactory
1388
+ };