@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.
- package/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- 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
|
+
}
|