@dynamicu/chromedebug-mcp 2.2.0

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.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,1516 @@
1
+ // HTTP server for Chrome extension workflow recording endpoints
2
+ import express from 'express';
3
+ import cors from 'cors';
4
+ import multer from 'multer';
5
+ import { ChromeController } from './chrome-controller.js';
6
+ import { WebSocketServer } from 'ws';
7
+ import { findAvailablePort } from './utils.js';
8
+ import { writePortFile, removePortFile } from './port-discovery.js';
9
+ import { configLoader } from './config-loader.js';
10
+ import { setupProcessTracking } from './services/process-tracker.js';
11
+
12
+ // Security imports
13
+ import { authenticate, authorize, PERMISSIONS } from './middleware/auth.js';
14
+ import {
15
+ corsOptions,
16
+ securityHeaders,
17
+ rateLimits,
18
+ requestSizeLimits,
19
+ securityLogger,
20
+ securityErrorHandler
21
+ } from './middleware/security.js';
22
+ import { createValidator } from './validation/schemas.js';
23
+ import {
24
+ workflowRecordingSchema,
25
+ frameBatchSchema,
26
+ associateLogsSchema,
27
+ streamLogsSchema,
28
+ domIntentSchema,
29
+ navigateSchema,
30
+ evaluateSchema,
31
+ screenInteractionsSchema,
32
+ sessionIdParam,
33
+ recordingIdParam,
34
+ frameParams,
35
+ pageContentQuery
36
+ } from './validation/schemas.js';
37
+ import { transformAssociateLogsRequest, analyzeLogFieldCompliance } from './validation/log-transformer.js';
38
+ import adminRouter from './routes/admin.js';
39
+ import { getLicenseManager } from './firebase-license-manager.js';
40
+
41
+ // Global Chrome controller instance (will be overridden by parameter if provided)
42
+ let globalChromeController = new ChromeController();
43
+
44
+ // Store WebSocket clients
45
+ const wsClients = new Set();
46
+
47
+ // Store selected element info from extension
48
+ let lastSelectedElement = null;
49
+
50
+ // Configure multer for handling FormData with security limits
51
+ const upload = multer({
52
+ storage: multer.memoryStorage(),
53
+ limits: {
54
+ fileSize: 50 * 1024 * 1024, // 50MB limit
55
+ files: 10 // Maximum 10 files
56
+ },
57
+ fileFilter: (req, file, cb) => {
58
+ // Only allow specific file types for security
59
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/json'];
60
+ if (allowedTypes.includes(file.mimetype)) {
61
+ cb(null, true);
62
+ } else {
63
+ cb(new Error(`File type ${file.mimetype} not allowed`));
64
+ }
65
+ }
66
+ });
67
+
68
+ // HTTP server setup for workflow recording endpoints
69
+ async function startHttpServer(chromeController = null, targetPort = null) {
70
+ // Use provided Chrome controller or default to global instance
71
+ const activeController = chromeController || globalChromeController;
72
+
73
+ const app = express();
74
+
75
+ // Trust proxy for accurate client IPs
76
+ app.set('trust proxy', 1);
77
+
78
+ // RAW REQUEST LOGGING - Capture ALL requests before any middleware processing
79
+ app.use('*', (req, res, next) => {
80
+ const timestamp = new Date().toISOString();
81
+ const ip = req.ip || req.connection?.remoteAddress || 'unknown';
82
+ const userAgent = req.get('user-agent') || 'unknown';
83
+ const contentType = req.get('content-type') || 'none';
84
+ const contentLength = req.get('content-length') || '0';
85
+
86
+ console.log(`[RAW-REQUEST] ${timestamp} - ${req.method} ${req.originalUrl}`);
87
+ console.log(`[RAW-REQUEST] IP: ${ip} | UA: ${userAgent}`);
88
+ console.log(`[RAW-REQUEST] Content-Type: ${contentType} | Length: ${contentLength}bytes`);
89
+ console.log(`[RAW-REQUEST] Headers:`, Object.keys(req.headers).map(h => `${h}=${req.headers[h]}`).join(', '));
90
+
91
+ // Capture body parsing errors
92
+ const originalJson = express.json();
93
+ let bodyParsingError = null;
94
+
95
+ // Override res.status to capture 400 errors before they're sent
96
+ const originalStatus = res.status;
97
+ res.status = function(code) {
98
+ if (code === 400) {
99
+ console.log(`[RAW-REQUEST] HTTP 400 ERROR for ${req.method} ${req.originalUrl}`);
100
+ console.log(`[RAW-REQUEST] This request will be rejected with 400`);
101
+ }
102
+ return originalStatus.call(this, code);
103
+ };
104
+
105
+ next();
106
+ });
107
+
108
+ // ENHANCED BODY PARSING - For debugging body parsing issues (only for screen-interactions)
109
+ app.use('/chromedebug/screen-interactions/*', (req, res, next) => {
110
+ if (req.method === 'POST') {
111
+ console.log(`[BODY-DEBUG] Processing screen-interactions request for ${req.originalUrl}`);
112
+ console.log(`[BODY-DEBUG] Content-Type: ${req.get('content-type')}`);
113
+ console.log(`[BODY-DEBUG] Content-Length: ${req.get('content-length')}`);
114
+ console.log(`[BODY-DEBUG] User-Agent: ${req.get('user-agent')}`);
115
+
116
+ // Store original data handler to capture raw body
117
+ const originalData = req.on;
118
+ const chunks = [];
119
+
120
+ req.on = function(event, handler) {
121
+ if (event === 'data') {
122
+ const originalHandler = handler;
123
+ const wrappedHandler = (chunk) => {
124
+ chunks.push(chunk);
125
+ return originalHandler(chunk);
126
+ };
127
+ return originalData.call(this, event, wrappedHandler);
128
+ } else if (event === 'end') {
129
+ const originalHandler = handler;
130
+ const wrappedHandler = () => {
131
+ if (chunks.length > 0) {
132
+ const rawBody = Buffer.concat(chunks).toString();
133
+ console.log(`[BODY-DEBUG] Raw body captured (${rawBody.length} chars):`, rawBody);
134
+ req.rawBody = rawBody;
135
+
136
+ // Try to parse manually
137
+ try {
138
+ const parsedBody = JSON.parse(rawBody);
139
+ console.log(`[BODY-DEBUG] Manual JSON parse successful`);
140
+ console.log(`[BODY-DEBUG] Parsed data:`, JSON.stringify(parsedBody, null, 2));
141
+ } catch (parseError) {
142
+ console.log(`[BODY-DEBUG] Manual JSON parse FAILED:`, parseError.message);
143
+ }
144
+ }
145
+ return originalHandler();
146
+ };
147
+ return originalData.call(this, event, wrappedHandler);
148
+ }
149
+ return originalData.call(this, event, handler);
150
+ };
151
+ }
152
+ next();
153
+ });
154
+
155
+ // JSON PARSING ERROR HANDLER - Catch malformed JSON before it reaches our handlers
156
+ app.use('/chromedebug/screen-interactions/*', (err, req, res, next) => {
157
+ if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
158
+ console.log(`[JSON-PARSE-ERROR] Malformed JSON in screen-interactions request:`, err.message);
159
+ console.log(`[JSON-PARSE-ERROR] Raw body:`, req.rawBody || 'not captured');
160
+ console.log(`[JSON-PARSE-ERROR] Request body:`, req.body);
161
+ return res.status(400).json({
162
+ error: 'Invalid JSON format',
163
+ details: err.message
164
+ });
165
+ }
166
+ next(err);
167
+ });
168
+
169
+ // ENHANCED VALIDATION ERROR HANDLER - Catch and log detailed validation errors
170
+ app.use('/chromedebug/screen-interactions/*', (err, req, res, next) => {
171
+ if (err && err.status === 400 && err.details) {
172
+ console.log(`[VALIDATION-ERROR] Screen interactions validation failed:`);
173
+ console.log(`[VALIDATION-ERROR] Request path:`, req.originalUrl);
174
+ console.log(`[VALIDATION-ERROR] User agent:`, req.get('user-agent'));
175
+ console.log(`[VALIDATION-ERROR] Content type:`, req.get('content-type'));
176
+ console.log(`[VALIDATION-ERROR] Raw body:`, req.rawBody || 'not captured');
177
+ console.log(`[VALIDATION-ERROR] Parsed body:`, JSON.stringify(req.body, null, 2));
178
+ console.log(`[VALIDATION-ERROR] Validation errors:`, JSON.stringify(err.details, null, 2));
179
+ }
180
+ next(err);
181
+ });
182
+
183
+ // Security middleware (applied first)
184
+ app.use(securityHeaders);
185
+ app.use(cors(corsOptions));
186
+ // Note: securityLogger moved to after authentication for protected routes
187
+
188
+ // Rate limiting
189
+ app.use('/chromedebug/admin', rateLimits.auth);
190
+ app.use('/chromedebug/frame-batch', rateLimits.upload);
191
+ app.use('/chromedebug/workflow-recording', rateLimits.upload);
192
+ app.use('/chromedebug/launch', rateLimits.chromeControl);
193
+ app.use('/chromedebug/navigate', rateLimits.chromeControl);
194
+ app.use('/chromedebug/evaluate', rateLimits.chromeControl);
195
+ app.use('/chromedebug/status', rateLimits.status);
196
+ app.use(rateLimits.general);
197
+
198
+ // Body parsing with security limits
199
+ app.use('/chromedebug/frame-batch', express.json({ limit: requestSizeLimits.largeUpload }));
200
+ app.use('/chromedebug/frame-batch', express.urlencoded({ limit: requestSizeLimits.largeUpload, extended: true }));
201
+ app.use('/chromedebug/upload/*', express.raw({
202
+ type: 'application/octet-stream',
203
+ limit: requestSizeLimits.largeUpload
204
+ }));
205
+ app.use(express.json({ limit: requestSizeLimits.json }));
206
+ app.use(express.urlencoded({ limit: requestSizeLimits.urlencoded, extended: true }));
207
+
208
+ // JSON error handling middleware
209
+ app.use((err, req, res, next) => {
210
+ if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
211
+ console.error('[JSON Parse Error]', {
212
+ url: req.url,
213
+ method: req.method,
214
+ contentType: req.get('content-type'),
215
+ error: err.message,
216
+ userAgent: req.get('user-agent'),
217
+ timestamp: new Date().toISOString()
218
+ });
219
+ return res.status(400).json({
220
+ error: 'Invalid JSON',
221
+ details: err.message,
222
+ position: err.message.match(/position (\d+)/)?.[1],
223
+ help: 'Check for escaped characters or malformed JSON in request body'
224
+ });
225
+ }
226
+ next(err);
227
+ });
228
+
229
+ // Admin routes (protected)
230
+ app.use('/chromedebug/admin', authenticate, adminRouter);
231
+
232
+ // Public endpoints (no authentication required)
233
+ app.get('/test', securityLogger, (req, res) => {
234
+ res.json({
235
+ message: 'ChromeDebug MCP HTTP server is running',
236
+ timestamp: new Date().toISOString(),
237
+ security: 'enabled'
238
+ });
239
+ });
240
+
241
+ // Status endpoint for Chrome extension (no auth required)
242
+ app.get('/chromedebug/status', securityLogger, (req, res) => {
243
+ res.json({
244
+ status: 'online',
245
+ version: '1.0.0',
246
+ security: 'enabled',
247
+ timestamp: new Date().toISOString()
248
+ });
249
+ });
250
+
251
+ // Health check endpoint for extension discovery (no auth required)
252
+ app.get('/health', (req, res) => {
253
+ res.json({
254
+ status: 'healthy',
255
+ service: 'chrome-pilot',
256
+ timestamp: new Date().toISOString()
257
+ });
258
+ });
259
+
260
+ // Port discovery endpoint (no auth required)
261
+ app.get('/chromedebug/port', (req, res) => {
262
+ res.json({
263
+ port: process.env.PORT || 3000,
264
+ message: 'ChromeDebug MCP server port',
265
+ timestamp: new Date().toISOString()
266
+ });
267
+ });
268
+
269
+ // Authentication test endpoint
270
+ app.get('/chromedebug/auth-test', authenticate, (req, res) => {
271
+ res.json({
272
+ success: true,
273
+ message: 'Authentication successful',
274
+ user: {
275
+ id: req.user.id,
276
+ name: req.user.name,
277
+ role: req.user.role,
278
+ authMethod: req.user.authMethod
279
+ },
280
+ timestamp: new Date().toISOString()
281
+ });
282
+ });
283
+
284
+ // Protected workflow recording endpoints
285
+ app.post('/chromedebug/workflow-recording',
286
+ authenticate,
287
+ authorize(PERMISSIONS.WORKFLOW_WRITE),
288
+ createValidator(workflowRecordingSchema),
289
+ async (req, res) => {
290
+ const { sessionId, url, title, includeLogs, actions, logs, functionTraces, name, screenshotSettings } = req.body;
291
+ console.log('[HTTP Server] Received workflow recording with name:', name);
292
+ console.log('[HTTP Server] Function traces count:', functionTraces ? functionTraces.length : 0);
293
+ if (!sessionId || !actions) {
294
+ return res.status(400).json({ error: 'sessionId and actions are required' });
295
+ }
296
+
297
+ try {
298
+ const result = await activeController.storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name, screenshotSettings, functionTraces);
299
+ res.json(result);
300
+ } catch (error) {
301
+ console.error('Error storing workflow recording:', error);
302
+ res.status(500).json({ error: 'Failed to store workflow recording', details: error.message });
303
+ }
304
+ });
305
+
306
+ app.get('/chromedebug/workflow-recording/:sessionId',
307
+ authenticate,
308
+ authorize(PERMISSIONS.WORKFLOW_READ),
309
+ createValidator(sessionIdParam, 'params'),
310
+ async (req, res) => {
311
+ const { sessionId } = req.params;
312
+ try {
313
+ const result = await activeController.getWorkflowRecording(sessionId);
314
+ if (result.error) {
315
+ return res.status(404).json(result);
316
+ }
317
+ res.json(result);
318
+ } catch (error) {
319
+ console.error('Error retrieving workflow recording:', error);
320
+ res.status(500).json({ error: 'Failed to retrieve workflow recording', details: error.message });
321
+ }
322
+ });
323
+
324
+ app.get('/chromedebug/workflow-recordings',
325
+ authenticate,
326
+ authorize(PERMISSIONS.WORKFLOW_READ),
327
+ async (req, res) => {
328
+ try {
329
+ const result = await activeController.listWorkflowRecordings();
330
+ res.json(result);
331
+ } catch (error) {
332
+ console.error('Error listing workflow recordings:', error);
333
+ res.status(500).json({ error: 'Failed to list workflow recordings', details: error.message });
334
+ }
335
+ });
336
+
337
+ // New streaming upload endpoints for full data recording
338
+ app.post('/chromedebug/upload/batch',
339
+ authenticate,
340
+ securityLogger,
341
+ authorize(PERMISSIONS.FRAME_WRITE),
342
+ async (req, res) => {
343
+ try {
344
+ const recordingId = req.headers['x-recording-id'];
345
+ const batchId = req.headers['x-batch-id'];
346
+ const isCompressed = req.headers['x-compressed'] === 'true';
347
+
348
+ let events;
349
+
350
+ if (isCompressed && req.body) {
351
+ // Decompress binary data using pako
352
+ const pako = await import('pako');
353
+ const decompressed = pako.default.ungzip(req.body, { to: 'string' });
354
+ events = JSON.parse(decompressed);
355
+ } else if (req.body) {
356
+ // Parse JSON data
357
+ const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
358
+ events = data.events || [];
359
+ } else {
360
+ return res.status(400).json({ error: 'No data provided' });
361
+ }
362
+
363
+ console.log(`[Upload] Received batch ${batchId} for recording ${recordingId} with ${events.length} events`);
364
+
365
+ // Ensure recording exists (auto-create if needed)
366
+ try {
367
+ const existingRecording = activeController.database.getRecording(recordingId);
368
+ if (!existingRecording) {
369
+ console.log(`[Upload] Auto-creating recording ${recordingId} for full data upload`);
370
+ activeController.database.storeRecording(recordingId, 'full_data_recording');
371
+ }
372
+ } catch (error) {
373
+ // Recording doesn't exist, create it
374
+ console.log(`[Upload] Auto-creating recording ${recordingId} for full data upload (error case)`);
375
+ activeController.database.storeRecording(recordingId, 'full_data_recording');
376
+ }
377
+
378
+ // Process events based on type
379
+ for (const event of events) {
380
+ switch (event.type) {
381
+ case 'execution_trace':
382
+ await activeController.database.storeExecutionTrace(recordingId, event);
383
+ break;
384
+ case 'variable_state':
385
+ await activeController.database.storeVariableState(recordingId, event);
386
+ break;
387
+ case 'dom_mutation':
388
+ await activeController.database.storeDomMutation(recordingId, event);
389
+ break;
390
+ case 'network_request':
391
+ await activeController.database.storeNetworkRequest(recordingId, event);
392
+ break;
393
+ case 'performance_metric':
394
+ await activeController.database.storePerformanceMetric(recordingId, event);
395
+ break;
396
+ default:
397
+ console.warn(`[Upload] Unknown event type: ${event.type}`);
398
+ }
399
+ }
400
+
401
+ res.json({
402
+ success: true,
403
+ batchId,
404
+ eventsProcessed: events.length
405
+ });
406
+ } catch (error) {
407
+ console.error('[Upload] Error processing batch:', error);
408
+ res.status(500).json({
409
+ error: 'Failed to process batch',
410
+ details: error.message
411
+ });
412
+ }
413
+ }
414
+ );
415
+
416
+ // Specialized endpoints for specific data types
417
+ app.post('/chromedebug/upload/:dataType',
418
+ authenticate,
419
+ securityLogger,
420
+ authorize(PERMISSIONS.FRAME_WRITE),
421
+ async (req, res) => {
422
+ try {
423
+ const dataType = req.params.dataType;
424
+ const recordingId = req.headers['x-recording-id'];
425
+ const isCompressed = req.headers['x-compressed'] === 'true';
426
+
427
+ let data;
428
+
429
+ if (isCompressed && req.body) {
430
+ const pako = await import('pako');
431
+ const decompressed = pako.default.ungzip(req.body, { to: 'string' });
432
+ data = JSON.parse(decompressed);
433
+ } else {
434
+ data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
435
+ }
436
+
437
+ console.log(`[Upload] Received ${dataType} data for recording ${recordingId}`);
438
+
439
+ // Store data based on type
440
+ const storeMethod = `store${dataType.charAt(0).toUpperCase() + dataType.slice(1).replace(/-/g, '')}`;
441
+ if (typeof activeController.database[storeMethod] === 'function') {
442
+ await activeController.database[storeMethod](recordingId, data);
443
+ res.json({ success: true, dataType, recordingId });
444
+ } else {
445
+ res.status(400).json({ error: `Unknown data type: ${dataType}` });
446
+ }
447
+ } catch (error) {
448
+ console.error(`[Upload] Error processing ${req.params.dataType}:`, error);
449
+ res.status(500).json({
450
+ error: 'Failed to process data',
451
+ details: error.message
452
+ });
453
+ }
454
+ }
455
+ );
456
+
457
+ // Protected frame recording endpoints
458
+ app.post('/chromedebug/frame-batch',
459
+ authenticate,
460
+ securityLogger,
461
+ authorize(PERMISSIONS.FRAME_WRITE),
462
+ upload.none(),
463
+ async (req, res) => {
464
+ console.log('[FORMDATA-DEBUG] ==========================================');
465
+ console.log('[FORMDATA-DEBUG] Frame batch request received');
466
+ console.log('[FORMDATA-DEBUG] Content-Type:', req.headers['content-type']);
467
+ console.log('[FORMDATA-DEBUG] Request method:', req.method);
468
+ console.log('[FORMDATA-DEBUG] User:', req.user?.name || 'unknown');
469
+ console.log('[FORMDATA-DEBUG] Raw body keys:', Object.keys(req.body || {}));
470
+ console.log('[FORMDATA-DEBUG] Raw body:', req.body);
471
+
472
+ // Handle both JSON and FormData
473
+ let sessionId, frames, sessionName;
474
+
475
+ if (req.headers['content-type']?.includes('application/json')) {
476
+ console.log('[FORMDATA-DEBUG] Processing as JSON request');
477
+ // JSON request - validate with schema
478
+ const { error, value } = frameBatchSchema.validate(req.body);
479
+ if (error) {
480
+ console.log('[FORMDATA-DEBUG] JSON validation failed:', error.details);
481
+ return res.status(400).json({
482
+ error: 'Validation failed',
483
+ details: error.details.map(detail => ({
484
+ field: detail.path.join('.'),
485
+ message: detail.message
486
+ }))
487
+ });
488
+ }
489
+ sessionId = value.sessionId;
490
+ frames = value.frames;
491
+ sessionName = value.sessionName || null;
492
+
493
+ console.log('[FORMDATA-DEBUG] JSON parsed successfully:', { sessionId, frameCount: frames?.length, sessionName });
494
+ } else {
495
+ console.log('[FORMDATA-DEBUG] Processing as FormData request');
496
+
497
+ // FormData request - manual validation
498
+ sessionId = req.body.sessionId;
499
+ sessionName = req.body.sessionName || null;
500
+
501
+ console.log('[FORMDATA-DEBUG] SessionId from FormData:', sessionId);
502
+ console.log('[FORMDATA-DEBUG] SessionName from FormData:', sessionName);
503
+ console.log('[FORMDATA-DEBUG] Frames field type:', typeof req.body.frames);
504
+ console.log('[FORMDATA-DEBUG] Frames field length:', req.body.frames?.length);
505
+ console.log('[FORMDATA-DEBUG] Frames field preview:', req.body.frames?.substring(0, 200));
506
+
507
+ try {
508
+ if (!req.body.frames) {
509
+ console.log('[FORMDATA-DEBUG] ❌ No frames field in FormData');
510
+ frames = null;
511
+ } else {
512
+ console.log('[FORMDATA-DEBUG] 🔄 Attempting to parse frames JSON...');
513
+ frames = JSON.parse(req.body.frames);
514
+ console.log('[FORMDATA-DEBUG] ✅ Frames JSON parsed successfully');
515
+ console.log('[FORMDATA-DEBUG] Parsed frames count:', frames?.length);
516
+ console.log('[FORMDATA-DEBUG] First frame structure:', frames?.[0] ? Object.keys(frames[0]) : 'no frames');
517
+ }
518
+
519
+ console.log('[FORMDATA-DEBUG] 🔄 Validating parsed FormData...');
520
+ // Validate the parsed data
521
+ const { error, value } = frameBatchSchema.validate({ sessionId, frames, sessionName });
522
+ if (error) {
523
+ console.log('[FORMDATA-DEBUG] ❌ FormData validation failed:', error.details);
524
+ return res.status(400).json({
525
+ error: 'Validation failed',
526
+ details: error.details.map(detail => ({
527
+ field: detail.path.join('.'),
528
+ message: detail.message
529
+ }))
530
+ });
531
+ }
532
+ sessionId = value.sessionId;
533
+ frames = value.frames;
534
+ sessionName = value.sessionName;
535
+
536
+ console.log('[FORMDATA-DEBUG] ✅ FormData validation successful');
537
+ } catch (parseError) {
538
+ console.error('[FORMDATA-DEBUG] ❌ Failed to parse frames JSON:', parseError);
539
+ console.error('[FORMDATA-DEBUG] Raw frames data:', req.body.frames);
540
+ return res.status(400).json({ error: 'Invalid frames JSON format' });
541
+ }
542
+ }
543
+
544
+ if (!sessionId || !frames) {
545
+ console.log('[FORMDATA-DEBUG] ❌ Missing required data');
546
+ console.log('[FORMDATA-DEBUG] SessionId:', sessionId);
547
+ console.log('[FORMDATA-DEBUG] Frames:', frames ? 'present' : 'null');
548
+ return res.status(400).json({ error: 'sessionId and frames are required' });
549
+ }
550
+
551
+ console.log('[FORMDATA-DEBUG] ✅ All data validated, proceeding to storage');
552
+ console.log('[FORMDATA-DEBUG] Session ID:', sessionId);
553
+ console.log('[FORMDATA-DEBUG] Frame count:', frames?.length);
554
+
555
+ try {
556
+ console.log('[FORMDATA-DEBUG] 🔄 Calling activeController.storeFrameBatch...');
557
+ if (frames && frames.length > 0) {
558
+ console.log('[FORMDATA-DEBUG] First frame structure:', Object.keys(frames[0]));
559
+ console.log('[FORMDATA-DEBUG] First frame has imageData:', !!frames[0].imageData);
560
+ console.log('[FORMDATA-DEBUG] ImageData length:', frames[0].imageData ? frames[0].imageData.length : 'N/A');
561
+ }
562
+
563
+ console.log('[FORMDATA-DEBUG] Session name:', sessionName || 'none');
564
+
565
+ const result = await activeController.storeFrameBatch(sessionId, frames, sessionName);
566
+ console.log('[FORMDATA-DEBUG] ✅ Frame batch storage result:', result);
567
+
568
+ console.log('[FORMDATA-DEBUG] Result recording ID:', result?.id);
569
+ console.log('[FORMDATA-DEBUG] Result frame count:', result?.total_frames);
570
+ console.log('[FORMDATA-DEBUG] ==========================================');
571
+
572
+ res.json(result);
573
+ } catch (error) {
574
+ console.error('[FORMDATA-DEBUG] ❌ Storage error:', error);
575
+ console.error('[FORMDATA-DEBUG] Error stack:', error.stack);
576
+ console.log('[FORMDATA-DEBUG] ==========================================');
577
+ res.status(500).json({ error: 'Failed to store frame batch', details: error.message });
578
+ }
579
+ });
580
+
581
+ app.get('/chromedebug/frame-session/:sessionId',
582
+ authenticate,
583
+ authorize(PERMISSIONS.FRAME_READ),
584
+ createValidator(sessionIdParam, 'params'),
585
+ async (req, res) => {
586
+ const { sessionId } = req.params;
587
+ try {
588
+ const result = await activeController.getFrameSession(sessionId);
589
+ if (!result) {
590
+ return res.status(404).json({ error: 'Frame session not found' });
591
+ }
592
+
593
+ // Debug: Verify logs are properly included and formatted
594
+ if (result.frames?.length > 0 && result.frames[0]?.logs?.length > 0) {
595
+ console.log(`[INFO] Frame session ${sessionId}: Loaded ${result.frames.length} frames with console logs`);
596
+ }
597
+
598
+ res.json(result);
599
+ } catch (error) {
600
+ console.error('Error retrieving frame session:', error);
601
+ res.status(500).json({ error: 'Failed to retrieve frame session', details: error.message });
602
+ }
603
+ });
604
+
605
+ /*
606
+ * SNAPSHOT FEATURE DISABLED (2025-10-01)
607
+ *
608
+ * Snapshots endpoint disabled - see SNAPSHOT_FEATURE_DISABLED.md
609
+ *
610
+ * WHY DISABLED:
611
+ * - Snapshots without console logs are just screenshots (users can do this natively)
612
+ * - Console log capture requires always-on monitoring (privacy concern)
613
+ * - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
614
+ *
615
+ * TO RE-ENABLE:
616
+ * 1. Implement privacy-conscious always-on log monitoring
617
+ * 2. Uncomment this endpoint
618
+ * 3. Re-enable chrome-controller.js getSnapshots() method
619
+ * 4. Re-enable extension UI and handlers
620
+ */
621
+ /*
622
+ app.get('/chromedebug/snapshots',
623
+ authenticate,
624
+ authorize(PERMISSIONS.FRAME_READ),
625
+ async (req, res) => {
626
+ try {
627
+ const limit = parseInt(req.query.limit) || 50;
628
+ const snapshots = await activeController.getSnapshots(limit);
629
+
630
+ res.json({
631
+ snapshots: snapshots || [],
632
+ count: snapshots ? snapshots.length : 0,
633
+ limit: limit
634
+ });
635
+ } catch (error) {
636
+ console.error('Error retrieving snapshots:', error);
637
+ res.status(500).json({ error: 'Failed to retrieve snapshots', details: error.message });
638
+ }
639
+ });
640
+ */
641
+
642
+ app.post('/chromedebug/associate-logs',
643
+ authenticate,
644
+ authorize(PERMISSIONS.FRAME_WRITE),
645
+ upload.none(),
646
+ async (req, res) => {
647
+ // Handle both JSON and FormData
648
+ let sessionId, logs;
649
+
650
+ if (req.headers['content-type']?.includes('application/json')) {
651
+ // JSON request - transform logs to match schema format first
652
+ const transformedBody = transformAssociateLogsRequest(req.body);
653
+
654
+ // Log transformation diagnostics for debugging
655
+ if (req.body.logs) {
656
+ const compliance = analyzeLogFieldCompliance(req.body.logs);
657
+ if (!compliance.isCompliant) {
658
+ console.log('[LogTransform] Applied field filtering:', {
659
+ originalLogs: req.body.logs.length,
660
+ nonSchemaFields: compliance.nonSchemaFields,
661
+ missingFields: compliance.missingRequiredFields
662
+ });
663
+ }
664
+ }
665
+
666
+ // Validate transformed data with schema
667
+ const { error, value } = associateLogsSchema.validate(transformedBody);
668
+ if (error) {
669
+ return res.status(400).json({
670
+ error: 'Validation failed',
671
+ details: error.details.map(detail => ({
672
+ field: detail.path.join('.'),
673
+ message: detail.message
674
+ }))
675
+ });
676
+ }
677
+ sessionId = value.sessionId;
678
+ logs = value.logs;
679
+ } else {
680
+ // FormData request
681
+ sessionId = req.body.sessionId;
682
+ try {
683
+ logs = req.body.logs ? JSON.parse(req.body.logs) : null;
684
+
685
+ // Transform logs to match schema format first
686
+ const requestData = { sessionId, logs };
687
+ const transformedData = transformAssociateLogsRequest(requestData);
688
+
689
+ // Log transformation diagnostics for debugging
690
+ if (logs) {
691
+ const compliance = analyzeLogFieldCompliance(logs);
692
+ if (!compliance.isCompliant) {
693
+ console.log('[LogTransform] Applied field filtering (FormData):', {
694
+ originalLogs: logs.length,
695
+ nonSchemaFields: compliance.nonSchemaFields,
696
+ missingFields: compliance.missingRequiredFields
697
+ });
698
+ }
699
+ }
700
+
701
+ // Validate the transformed data
702
+ const { error, value } = associateLogsSchema.validate(transformedData);
703
+ if (error) {
704
+ return res.status(400).json({
705
+ error: 'Validation failed',
706
+ details: error.details.map(detail => ({
707
+ field: detail.path.join('.'),
708
+ message: detail.message
709
+ }))
710
+ });
711
+ }
712
+ sessionId = value.sessionId;
713
+ logs = value.logs;
714
+ } catch (parseError) {
715
+ return res.status(400).json({ error: 'Invalid logs JSON format' });
716
+ }
717
+ }
718
+
719
+ if (!sessionId || !logs) {
720
+ return res.status(400).json({ error: 'sessionId and logs are required' });
721
+ }
722
+
723
+ try {
724
+ const result = await activeController.associateLogsWithFrames(sessionId, logs);
725
+ res.json(result);
726
+ } catch (error) {
727
+ console.error('Error associating logs with frames:', error);
728
+ res.status(500).json({ error: 'Failed to associate logs', details: error.message });
729
+ }
730
+ });
731
+
732
+ // Stream logs endpoint for real-time batched log streaming
733
+ app.post('/chromedebug/logs/stream',
734
+ authenticate,
735
+ authorize(PERMISSIONS.FRAME_WRITE),
736
+ upload.none(),
737
+ async (req, res) => {
738
+ // Handle both JSON and FormData
739
+ let sessionId, logs;
740
+
741
+ if (req.headers['content-type']?.includes('application/json')) {
742
+ // JSON request - validate with schema
743
+ const { error, value } = streamLogsSchema.validate(req.body);
744
+ if (error) {
745
+ return res.status(400).json({
746
+ error: 'Validation failed',
747
+ details: error.details.map(detail => ({
748
+ field: detail.path.join('.'),
749
+ message: detail.message
750
+ }))
751
+ });
752
+ }
753
+ sessionId = value.sessionId;
754
+ logs = value.logs;
755
+ } else {
756
+ // FormData request
757
+ sessionId = req.body.sessionId;
758
+ try {
759
+ logs = req.body.logs ? JSON.parse(req.body.logs) : null;
760
+
761
+ // Validate the parsed data
762
+ const { error, value } = streamLogsSchema.validate({ sessionId, logs });
763
+ if (error) {
764
+ return res.status(400).json({
765
+ error: 'Validation failed',
766
+ details: error.details.map(detail => ({
767
+ field: detail.path.join('.'),
768
+ message: detail.message
769
+ }))
770
+ });
771
+ }
772
+ sessionId = value.sessionId;
773
+ logs = value.logs;
774
+ } catch (parseError) {
775
+ return res.status(400).json({ error: 'Invalid logs JSON format' });
776
+ }
777
+ }
778
+
779
+ if (!sessionId || !logs) {
780
+ return res.status(400).json({ error: 'sessionId and logs are required' });
781
+ }
782
+
783
+ try {
784
+ const result = await activeController.streamLogsToFrames(sessionId, logs);
785
+ res.json(result);
786
+ } catch (error) {
787
+ console.error('Error streaming logs to frames:', error);
788
+ res.status(500).json({ error: 'Failed to stream logs', details: error.message });
789
+ }
790
+ });
791
+
792
+ // DOM intent endpoint for Chrome extension element selection
793
+ app.post('/chromedebug/dom-intent',
794
+ authenticate,
795
+ authorize(PERMISSIONS.CHROME_CONTROL),
796
+ createValidator(domIntentSchema),
797
+ async (req, res) => {
798
+ try {
799
+ const { selector, instruction, elementInfo } = req.body;
800
+
801
+ if (!selector) {
802
+ return res.status(400).json({ error: 'Selector is required' });
803
+ }
804
+
805
+ // Store the selected element info
806
+ lastSelectedElement = {
807
+ selector,
808
+ instruction: instruction || '',
809
+ elementInfo: elementInfo || {}
810
+ };
811
+
812
+ // If Chrome is connected, forward to Chrome controller
813
+ const status = await activeController.getConnectionStatus();
814
+ if (status.connected) {
815
+ await activeController.selectElement(selector, instruction, elementInfo);
816
+ }
817
+
818
+ // Notify WebSocket clients about the selection
819
+ const wsMessage = JSON.stringify({
820
+ type: 'element_selected',
821
+ data: lastSelectedElement
822
+ });
823
+
824
+ wsClients.forEach(client => {
825
+ if (client.readyState === 1) { // OPEN state
826
+ client.send(wsMessage);
827
+ }
828
+ });
829
+
830
+ res.json({
831
+ success: true,
832
+ message: 'Element selection received',
833
+ selector,
834
+ chromeConnected: status.connected
835
+ });
836
+ } catch (error) {
837
+ console.error('Error handling DOM intent:', error);
838
+ res.status(500).json({
839
+ error: 'Failed to process DOM intent',
840
+ details: error.message
841
+ });
842
+ }
843
+ });
844
+
845
+
846
+ // Protected Chrome control endpoints
847
+ app.post('/chromedebug/launch',
848
+ authenticate,
849
+ authorize(PERMISSIONS.CHROME_CONTROL),
850
+ async (req, res) => {
851
+ try {
852
+ const result = await activeController.launch();
853
+ res.json(result);
854
+ } catch (error) {
855
+ res.status(500).json({ error: error.message });
856
+ }
857
+ });
858
+
859
+ app.post('/chromedebug/navigate',
860
+ authenticate,
861
+ authorize(PERMISSIONS.CHROME_CONTROL),
862
+ createValidator(navigateSchema),
863
+ async (req, res) => {
864
+ try {
865
+ const { url } = req.body;
866
+ const result = await activeController.navigateTo(url);
867
+ res.json(result);
868
+ } catch (error) {
869
+ res.status(500).json({ error: error.message });
870
+ }
871
+ });
872
+
873
+ app.get('/chromedebug/screenshot',
874
+ authenticate,
875
+ authorize(PERMISSIONS.CHROME_CONTROL),
876
+ async (req, res) => {
877
+ try {
878
+ const result = await activeController.takeScreenshot();
879
+ res.json(result);
880
+ } catch (error) {
881
+ res.status(500).json({ error: error.message });
882
+ }
883
+ });
884
+
885
+ app.get('/chromedebug/page-content',
886
+ authenticate,
887
+ authorize(PERMISSIONS.CHROME_CONTROL),
888
+ createValidator(pageContentQuery, 'query'),
889
+ async (req, res) => {
890
+ try {
891
+ // Extract query parameters for options
892
+ const options = {
893
+ includeText: req.query.includeText !== 'false',
894
+ includeHtml: req.query.includeHtml === 'true',
895
+ includeStructure: req.query.includeStructure !== 'false'
896
+ };
897
+
898
+ const result = await activeController.getPageContent(options);
899
+ res.json(result);
900
+ } catch (error) {
901
+ res.status(500).json({ error: error.message });
902
+ }
903
+ });
904
+
905
+ app.post('/chromedebug/evaluate',
906
+ authenticate,
907
+ authorize(PERMISSIONS.CHROME_CONTROL),
908
+ createValidator(evaluateSchema),
909
+ async (req, res) => {
910
+ try {
911
+ const { expression } = req.body;
912
+ const result = await activeController.evaluateExpression(expression);
913
+ res.json(result);
914
+ } catch (error) {
915
+ res.status(500).json({ error: error.message });
916
+ }
917
+ });
918
+
919
+ // Kept for backward compatibility
920
+ app.get('/chromedebug/frame-session-info/:sessionId',
921
+ authenticate,
922
+ authorize(PERMISSIONS.FRAME_READ),
923
+ createValidator(sessionIdParam, 'params'),
924
+ async (req, res) => {
925
+ try {
926
+ const result = await activeController.getFrameSessionInfo(req.params.sessionId);
927
+ if (!result) {
928
+ return res.status(404).json({ error: 'Frame session not found' });
929
+ }
930
+ res.json(result);
931
+ } catch (error) {
932
+ res.status(500).json({ error: error.message });
933
+ }
934
+ });
935
+
936
+ app.get('/chromedebug/frame-sessions',
937
+ authenticate,
938
+ authorize(PERMISSIONS.FRAME_READ),
939
+ async (req, res) => {
940
+ try {
941
+ const result = await activeController.listFrameSessions();
942
+ res.json(result);
943
+ } catch (error) {
944
+ res.status(500).json({ error: error.message });
945
+ }
946
+ });
947
+
948
+ app.get('/chromedebug/frame/:sessionId/:frameIndex',
949
+ authenticate,
950
+ authorize(PERMISSIONS.FRAME_READ),
951
+ createValidator(frameParams, 'params'),
952
+ async (req, res) => {
953
+ try {
954
+ const { sessionId, frameIndex } = req.params;
955
+ const result = await activeController.getFrame(sessionId, parseInt(frameIndex));
956
+ if (!result) {
957
+ return res.status(404).json({ error: 'Frame not found' });
958
+ }
959
+ res.json(result);
960
+ } catch (error) {
961
+ res.status(500).json({ error: error.message });
962
+ }
963
+ });
964
+
965
+ // Get frame screenshot
966
+ app.get('/chromedebug/frame-screenshot/:sessionId/:frameIndex',
967
+ authenticate,
968
+ authorize(PERMISSIONS.FRAME_READ),
969
+ createValidator(frameParams, 'params'),
970
+ async (req, res) => {
971
+ try {
972
+ const { sessionId, frameIndex } = req.params;
973
+ const includeMetadata = req.query.includeMetadata === 'true';
974
+
975
+ const result = await activeController.getFrameScreenshot(sessionId, parseInt(frameIndex), includeMetadata);
976
+ if (!result) {
977
+ return res.status(404).json({ error: 'Frame screenshot not found' });
978
+ }
979
+
980
+ // Add cache headers for browser optimization
981
+ res.set({
982
+ 'Cache-Control': 'public, max-age=3600',
983
+ 'Content-Type': 'application/json'
984
+ });
985
+
986
+ res.json(result);
987
+ } catch (error) {
988
+ res.status(500).json({ error: error.message });
989
+ }
990
+ });
991
+
992
+ // Search frame logs
993
+ app.get('/chromedebug/frame-logs/search/:sessionId',
994
+ authenticate,
995
+ authorize(PERMISSIONS.FRAME_READ),
996
+ createValidator(sessionIdParam, 'params'),
997
+ async (req, res) => {
998
+ try {
999
+ const { sessionId } = req.params;
1000
+ const { searchText, logLevel = 'all', maxResults = 50 } = req.query;
1001
+
1002
+ if (!searchText) {
1003
+ return res.status(400).json({ error: 'searchText query parameter is required' });
1004
+ }
1005
+
1006
+ const result = await activeController.searchFrameLogs(
1007
+ sessionId,
1008
+ searchText,
1009
+ logLevel,
1010
+ parseInt(maxResults)
1011
+ );
1012
+ res.json(result);
1013
+ } catch (error) {
1014
+ res.status(500).json({ error: error.message });
1015
+ }
1016
+ });
1017
+
1018
+ // Get frame logs paginated
1019
+ app.get('/chromedebug/frame-logs/:sessionId/:frameIndex',
1020
+ authenticate,
1021
+ authorize(PERMISSIONS.FRAME_READ),
1022
+ createValidator(frameParams, 'params'),
1023
+ async (req, res) => {
1024
+ try {
1025
+ const { sessionId, frameIndex } = req.params;
1026
+ const {
1027
+ offset = 0,
1028
+ limit = 100,
1029
+ logLevel = 'all',
1030
+ searchText
1031
+ } = req.query;
1032
+
1033
+ const result = await activeController.getFrameLogsPaginated(
1034
+ sessionId,
1035
+ parseInt(frameIndex),
1036
+ parseInt(offset),
1037
+ parseInt(limit),
1038
+ logLevel,
1039
+ searchText
1040
+ );
1041
+ res.json(result);
1042
+ } catch (error) {
1043
+ res.status(500).json({ error: error.message });
1044
+ }
1045
+ });
1046
+
1047
+
1048
+ app.delete('/chromedebug/recording/:recordingId',
1049
+ authenticate,
1050
+ authorize(PERMISSIONS.WORKFLOW_DELETE),
1051
+ createValidator(recordingIdParam, 'params'),
1052
+ async (req, res) => {
1053
+ try {
1054
+ const { recordingId } = req.params;
1055
+ const result = await activeController.deleteRecording(recordingId);
1056
+
1057
+ if (result.success) {
1058
+ res.json(result);
1059
+ } else {
1060
+ res.status(404).json(result);
1061
+ }
1062
+ } catch (error) {
1063
+ console.error('Error deleting recording:', error);
1064
+ res.status(500).json({ error: 'Failed to delete recording', details: error.message });
1065
+ }
1066
+ });
1067
+
1068
+ app.delete('/chromedebug/workflow-recording/:recordingId',
1069
+ authenticate,
1070
+ authorize(PERMISSIONS.WORKFLOW_DELETE),
1071
+ createValidator(recordingIdParam, 'params'),
1072
+ async (req, res) => {
1073
+ try {
1074
+ const { recordingId } = req.params;
1075
+ const result = await activeController.deleteWorkflowRecording(recordingId);
1076
+
1077
+ if (result.success) {
1078
+ res.json(result);
1079
+ } else {
1080
+ res.status(404).json(result);
1081
+ }
1082
+ } catch (error) {
1083
+ console.error('Error deleting workflow recording:', error);
1084
+ res.status(500).json({ error: 'Failed to delete workflow recording', details: error.message });
1085
+ }
1086
+ });
1087
+
1088
+ // Save screen interactions for a recording
1089
+ app.post('/chromedebug/screen-interactions/:sessionId',
1090
+ authenticate,
1091
+ authorize(PERMISSIONS.WORKFLOW_WRITE),
1092
+ createValidator(sessionIdParam, 'params'),
1093
+ createValidator(screenInteractionsSchema),
1094
+ async (req, res) => {
1095
+ const { sessionId } = req.params;
1096
+ const { interactions } = req.body;
1097
+
1098
+ // Debug logging to identify validation issues
1099
+ console.log('[SCREEN-INTERACTIONS-DEBUG] ==========================================');
1100
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Request received for session:', sessionId);
1101
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Content-Type:', req.get('content-type'));
1102
+ console.log('[SCREEN-INTERACTIONS-DEBUG] User agent:', req.get('user-agent'));
1103
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Auth user:', req.user?.name || 'unknown');
1104
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Request body keys:', Object.keys(req.body || {}));
1105
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Interactions field present:', !!interactions);
1106
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Interactions type:', typeof interactions);
1107
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Interactions length:', Array.isArray(interactions) ? interactions.length : 'N/A');
1108
+ if (Array.isArray(interactions) && interactions.length > 0) {
1109
+ console.log('[SCREEN-INTERACTIONS-DEBUG] First interaction:', JSON.stringify(interactions[0], null, 2));
1110
+ }
1111
+ console.log('[SCREEN-INTERACTIONS-DEBUG] Raw body:', JSON.stringify(req.body, null, 2));
1112
+ console.log('[SCREEN-INTERACTIONS-DEBUG] ==========================================');
1113
+
1114
+ try {
1115
+ const { database } = await import('./database.js');
1116
+ const result = database.storeScreenInteractions(sessionId, interactions);
1117
+
1118
+ if (result.success) {
1119
+ console.log(`[HTTP] Stored ${result.count} screen interactions for recording ${sessionId}`);
1120
+ res.json({ success: true, count: result.count });
1121
+ } else {
1122
+ res.status(500).json({ success: false, error: result.error });
1123
+ }
1124
+ } catch (error) {
1125
+ console.error('Error storing screen interactions:', error);
1126
+ res.status(500).json({ error: error.message });
1127
+ }
1128
+ });
1129
+
1130
+ // TEMPORARY DEBUG: Test authentication bypass and see raw data
1131
+ app.post('/chromedebug/debug-interactions/:sessionId',
1132
+ authenticate,
1133
+ (req, res) => {
1134
+ console.log('[DEBUG-ENDPOINT] ==========================================');
1135
+ console.log('[DEBUG-ENDPOINT] Auth bypass test successful!');
1136
+ console.log('[DEBUG-ENDPOINT] User:', JSON.stringify(req.user, null, 2));
1137
+ console.log('[DEBUG-ENDPOINT] Session ID:', req.params.sessionId);
1138
+ console.log('[DEBUG-ENDPOINT] Content-Type:', req.get('content-type'));
1139
+ console.log('[DEBUG-ENDPOINT] Request headers:', Object.keys(req.headers));
1140
+ console.log('[DEBUG-ENDPOINT] Request body:', JSON.stringify(req.body, null, 2));
1141
+ console.log('[DEBUG-ENDPOINT] ==========================================');
1142
+
1143
+ res.json({
1144
+ debug: true,
1145
+ message: 'Debug endpoint working',
1146
+ user: req.user,
1147
+ received: req.body,
1148
+ sessionId: req.params.sessionId
1149
+ });
1150
+ }
1151
+ );
1152
+
1153
+ // Get screen interactions for a recording
1154
+ app.get('/chromedebug/screen-interactions/:sessionId',
1155
+ authenticate,
1156
+ authorize(PERMISSIONS.WORKFLOW_READ),
1157
+ createValidator(sessionIdParam, 'params'),
1158
+ async (req, res) => {
1159
+ const { sessionId } = req.params;
1160
+
1161
+ try {
1162
+ const { database } = await import('./database.js');
1163
+ const interactions = database.getScreenInteractions(sessionId);
1164
+ res.json({ interactions });
1165
+ } catch (error) {
1166
+ console.error('Error retrieving screen interactions:', error);
1167
+ res.status(500).json({ error: error.message });
1168
+ }
1169
+ });
1170
+
1171
+ // License Management Endpoints
1172
+
1173
+ // Validate license key
1174
+ app.post('/chromedebug/license/validate',
1175
+ async (req, res) => {
1176
+ try {
1177
+ const { licenseKey } = req.body;
1178
+
1179
+ if (!licenseKey) {
1180
+ return res.status(400).json({ error: 'License key required' });
1181
+ }
1182
+
1183
+ const licenseManager = getLicenseManager();
1184
+ const result = await licenseManager.validateLicense(licenseKey);
1185
+
1186
+ res.json(result);
1187
+ } catch (error) {
1188
+ console.error('License validation error:', error);
1189
+ res.status(500).json({ error: 'License validation failed' });
1190
+ }
1191
+ });
1192
+
1193
+ // Check usage limit
1194
+ app.post('/chromedebug/license/check-usage',
1195
+ authenticate,
1196
+ async (req, res) => {
1197
+ try {
1198
+ const userId = req.user.id;
1199
+
1200
+ const licenseManager = getLicenseManager();
1201
+ const result = await licenseManager.checkUsageLimit(userId);
1202
+
1203
+ res.json(result);
1204
+ } catch (error) {
1205
+ console.error('Usage check error:', error);
1206
+ res.status(500).json({ error: 'Usage check failed' });
1207
+ }
1208
+ });
1209
+
1210
+ // Increment usage
1211
+ app.post('/chromedebug/license/increment-usage',
1212
+ authenticate,
1213
+ async (req, res) => {
1214
+ try {
1215
+ const userId = req.user.id;
1216
+ const { count = 1 } = req.body;
1217
+
1218
+ const licenseManager = getLicenseManager();
1219
+ const result = await licenseManager.incrementUsage(userId, count);
1220
+
1221
+ res.json(result);
1222
+ } catch (error) {
1223
+ console.error('Usage increment error:', error);
1224
+ res.status(500).json({ error: 'Usage increment failed' });
1225
+ }
1226
+ });
1227
+
1228
+ // Get user license info
1229
+ app.get('/chromedebug/license/info',
1230
+ authenticate,
1231
+ async (req, res) => {
1232
+ try {
1233
+ const userId = req.user.id;
1234
+
1235
+ const licenseManager = getLicenseManager();
1236
+ const result = await licenseManager.getUserLicense(userId);
1237
+
1238
+ res.json(result);
1239
+ } catch (error) {
1240
+ console.error('License info error:', error);
1241
+ res.status(500).json({ error: 'Failed to get license info' });
1242
+ }
1243
+ });
1244
+
1245
+ // List active instances
1246
+ app.get('/chromedebug/license/instances',
1247
+ authenticate,
1248
+ async (req, res) => {
1249
+ try {
1250
+ const { licenseKey } = req.query;
1251
+
1252
+ if (!licenseKey) {
1253
+ return res.status(400).json({ error: 'License key required' });
1254
+ }
1255
+
1256
+ const licenseManager = getLicenseManager();
1257
+ const instances = await licenseManager.listInstances(licenseKey);
1258
+
1259
+ res.json({ instances });
1260
+ } catch (error) {
1261
+ console.error('List instances error:', error);
1262
+ res.status(500).json({ error: 'Failed to list instances' });
1263
+ }
1264
+ });
1265
+
1266
+ // Deactivate instance
1267
+ app.post('/chromedebug/license/deactivate-instance',
1268
+ authenticate,
1269
+ async (req, res) => {
1270
+ try {
1271
+ const { instanceId } = req.body;
1272
+
1273
+ if (!instanceId) {
1274
+ return res.status(400).json({ error: 'Instance ID required' });
1275
+ }
1276
+
1277
+ const licenseManager = getLicenseManager();
1278
+ const result = await licenseManager.deactivateInstance(instanceId);
1279
+
1280
+ res.json(result);
1281
+ } catch (error) {
1282
+ console.error('Deactivate instance error:', error);
1283
+ res.status(500).json({ error: 'Failed to deactivate instance' });
1284
+ }
1285
+ });
1286
+
1287
+ // Get cache status
1288
+ app.get('/chromedebug/license/cache-status',
1289
+ authenticate,
1290
+ async (req, res) => {
1291
+ try {
1292
+ const licenseManager = getLicenseManager();
1293
+ const status = licenseManager.getCacheStatus();
1294
+
1295
+ res.json(status);
1296
+ } catch (error) {
1297
+ console.error('Cache status error:', error);
1298
+ res.status(500).json({ error: 'Failed to get cache status' });
1299
+ }
1300
+ });
1301
+
1302
+ // Clear license cache
1303
+ app.post('/chromedebug/license/clear-cache',
1304
+ authenticate,
1305
+ async (req, res) => {
1306
+ try {
1307
+ const licenseManager = getLicenseManager();
1308
+ licenseManager.clearCache();
1309
+
1310
+ res.json({ success: true, message: 'License cache cleared' });
1311
+ } catch (error) {
1312
+ console.error('Clear cache error:', error);
1313
+ res.status(500).json({ error: 'Failed to clear cache' });
1314
+ }
1315
+ });
1316
+
1317
+ // Add a catch-all route for debugging
1318
+ app.use('*', (req, res, next) => {
1319
+ console.log(`[HTTP] ${req.method} ${req.originalUrl}`);
1320
+ next();
1321
+ });
1322
+
1323
+ // Security error handler
1324
+ app.use(securityErrorHandler);
1325
+
1326
+ // 404 handler - must be last
1327
+ app.use((req, res) => {
1328
+ console.log(`[HTTP] 404 - Route not found: ${req.method} ${req.originalUrl}`);
1329
+ res.status(404).json({
1330
+ success: false,
1331
+ error: 'Endpoint not found',
1332
+ availableEndpoints: [
1333
+ 'GET /chromedebug/status',
1334
+ 'GET /chromedebug/port',
1335
+ 'POST /chromedebug/workflow-recording',
1336
+ 'GET /chromedebug/workflow-recordings',
1337
+ 'POST /chromedebug/frame-batch',
1338
+ 'POST /chromedebug/dom-intent',
1339
+ 'POST /chromedebug/launch',
1340
+ 'POST /chromedebug/navigate',
1341
+ 'GET /chromedebug/screenshot',
1342
+ 'GET /chromedebug/page-content',
1343
+ 'POST /chromedebug/evaluate'
1344
+ ]
1345
+ });
1346
+ });
1347
+
1348
+ // Start server with port finding
1349
+ let server;
1350
+ let actualPort;
1351
+ let portsToTry;
1352
+
1353
+ if (targetPort) {
1354
+ // Use the target port provided by session manager
1355
+ portsToTry = [targetPort];
1356
+ console.log(`Attempting to start HTTP server on session manager allocated port: ${targetPort}`);
1357
+ } else {
1358
+ // Fall back to configured ports
1359
+ const configuredPorts = configLoader.getHttpServerPorts();
1360
+ const startPort = process.env.PORT ? parseInt(process.env.PORT) : configuredPorts[0];
1361
+
1362
+ // If PORT env var is set, try it first, then fall back to configured ports
1363
+ portsToTry = process.env.PORT
1364
+ ? [startPort, ...configuredPorts.filter(p => p !== startPort)]
1365
+ : configuredPorts;
1366
+
1367
+ console.log('Attempting to start HTTP server on configured ports:', portsToTry);
1368
+ }
1369
+
1370
+ // Try configured ports in order
1371
+ for (const port of portsToTry) {
1372
+ try {
1373
+ await new Promise((resolve, reject) => {
1374
+ server = app.listen(port, () => {
1375
+ actualPort = port;
1376
+ console.error(`HTTP server listening on port ${actualPort}`);
1377
+ resolve();
1378
+ }).on('error', reject);
1379
+ });
1380
+ break;
1381
+ } catch (error) {
1382
+ if (error.code === 'EADDRINUSE') {
1383
+ console.error(`Port ${port} is in use, trying next...`);
1384
+ continue;
1385
+ }
1386
+ throw error;
1387
+ }
1388
+ }
1389
+
1390
+ if (!server || !actualPort) {
1391
+ console.error('ERROR: All configured ports are in use:', portsToTry);
1392
+ console.error('Please free up one of these ports or configure different ports');
1393
+ throw new Error(`Could not find available port for HTTP server. All ports in use: ${portsToTry.join(', ')}`);
1394
+ }
1395
+
1396
+ console.log(`HTTP server started on port ${actualPort}`);
1397
+
1398
+ // Write port file for discovery
1399
+ writePortFile(actualPort);
1400
+
1401
+ // Return backward-compatible object with both server and port
1402
+ const result = {
1403
+ httpServer: server,
1404
+ serverPort: actualPort
1405
+ };
1406
+
1407
+ // Make object coercible to number for backward compatibility
1408
+ result.valueOf = () => actualPort;
1409
+ result.toString = () => String(actualPort);
1410
+
1411
+ return result;
1412
+ }
1413
+
1414
+ // WebSocket server setup
1415
+ async function startWebSocketServer() {
1416
+ const startPort = process.env.WS_PORT ? parseInt(process.env.WS_PORT) : 3001;
1417
+ const wsPort = await findAvailablePort(startPort);
1418
+
1419
+ const wss = new WebSocketServer({ port: wsPort });
1420
+
1421
+ wss.on('connection', async (ws) => {
1422
+ console.error(`WebSocket client connected`);
1423
+ wsClients.add(ws);
1424
+
1425
+ // Send initial status
1426
+ const status = await activeController.getConnectionStatus();
1427
+ ws.send(JSON.stringify({
1428
+ type: 'status',
1429
+ data: status
1430
+ }));
1431
+
1432
+ ws.on('message', async (message) => {
1433
+ try {
1434
+ const msg = JSON.parse(message.toString());
1435
+
1436
+ switch (msg.type) {
1437
+ case 'element_selected':
1438
+ // Store the selected element info from extension
1439
+ lastSelectedElement = msg.data;
1440
+ await activeController.selectElement(
1441
+ msg.data.selector,
1442
+ msg.data.instruction,
1443
+ msg.data.elementInfo
1444
+ );
1445
+
1446
+ // Send confirmation
1447
+ ws.send(JSON.stringify({
1448
+ type: 'element_selection_confirmed',
1449
+ data: { success: true }
1450
+ }));
1451
+ break;
1452
+
1453
+ case 'get_status':
1454
+ const status = await activeController.getConnectionStatus();
1455
+ ws.send(JSON.stringify({
1456
+ type: 'status',
1457
+ data: status
1458
+ }));
1459
+ break;
1460
+
1461
+ default:
1462
+ console.error(`Unknown WebSocket message type: ${msg.type}`);
1463
+ }
1464
+ } catch (error) {
1465
+ console.error('WebSocket message error:', error);
1466
+ ws.send(JSON.stringify({
1467
+ type: 'error',
1468
+ data: { message: error.message }
1469
+ }));
1470
+ }
1471
+ });
1472
+
1473
+ ws.on('close', () => {
1474
+ console.error('WebSocket client disconnected');
1475
+ wsClients.delete(ws);
1476
+ });
1477
+
1478
+ ws.on('error', (error) => {
1479
+ console.error('WebSocket error:', error);
1480
+ wsClients.delete(ws);
1481
+ });
1482
+ });
1483
+
1484
+ console.error(`WebSocket server listening on port ${wsPort}`);
1485
+ }
1486
+
1487
+ // Export for use by MCP server
1488
+ export { startHttpServer, startWebSocketServer };
1489
+
1490
+ // Only run main if this is the main module
1491
+ if (import.meta.url === `file://${process.argv[1]}`) {
1492
+ async function main() {
1493
+ // Register this process for safe tracking
1494
+ setupProcessTracking('http-server');
1495
+
1496
+ // Start HTTP server for workflow recording endpoints
1497
+ await startHttpServer();
1498
+
1499
+ // Start WebSocket server for Chrome extension
1500
+ await startWebSocketServer();
1501
+
1502
+ console.error('ChromeDebug MCP HTTP server running with WebSocket support');
1503
+ }
1504
+
1505
+ // Clean shutdown
1506
+ const cleanup = async (signal) => {
1507
+ console.error(`Received ${signal}, shutting down HTTP server...`);
1508
+ await activeController.close();
1509
+ process.exit(0);
1510
+ };
1511
+
1512
+ process.on('SIGINT', () => cleanup('SIGINT'));
1513
+ process.on('SIGTERM', () => cleanup('SIGTERM'));
1514
+
1515
+ main().catch(console.error);
1516
+ }