@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,449 @@
1
+ /**
2
+ * ProfileManager - Chrome Profile Isolation Manager
3
+ *
4
+ * Manages isolated Chrome profiles for true session separation.
5
+ * Each session gets its own completely isolated profile directory
6
+ * with separate storage, cookies, cache, and settings.
7
+ *
8
+ * Key Features:
9
+ * - UUID-based profile isolation (no data leakage between sessions)
10
+ * - Secure profile directory validation and cleanup
11
+ * - Storage quotas and limits to prevent DoS
12
+ * - Automatic cleanup of orphaned profiles
13
+ * - Profile lifecycle management with proper resource tracking
14
+ */
15
+
16
+ import { promises as fs } from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+ import { randomUUID } from 'crypto';
20
+
21
+ export class ProfileManager {
22
+ constructor(options = {}) {
23
+ this.profilesDir = options.profilesDir || path.join(os.tmpdir(), 'chrome-pilot-profiles');
24
+ this.activeProfiles = new Map(); // sessionId -> { profilePath, claudeInstanceId, createdAt, size }
25
+ this.initialized = false;
26
+
27
+ // Security and resource limits
28
+ this.config = {
29
+ maxProfileSizeMB: options.maxProfileSizeMB || 500, // 500MB per profile
30
+ maxTotalProfilesMB: options.maxTotalProfilesMB || 5000, // 5GB total
31
+ profileCleanupAgeMs: options.profileCleanupAgeMs || 24 * 60 * 60 * 1000, // 24 hours
32
+ ...options
33
+ };
34
+
35
+ console.log(`[ProfileManager] Initialized with profiles directory: ${this.profilesDir}`);
36
+ }
37
+
38
+ /**
39
+ * Initializes the profile manager
40
+ */
41
+ async initialize() {
42
+ if (this.initialized) return;
43
+
44
+ try {
45
+ // Create profiles directory if it doesn't exist
46
+ await fs.mkdir(this.profilesDir, { recursive: true });
47
+
48
+ // Clean up any orphaned profiles from previous runs
49
+ await this.cleanupOrphanedProfiles();
50
+
51
+ this.initialized = true;
52
+ console.log('[ProfileManager] Initialized successfully');
53
+ } catch (error) {
54
+ console.error('[ProfileManager] Initialization failed:', error);
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Creates an isolated Chrome profile for a session
61
+ * @param {string} sessionId - Unique session identifier
62
+ * @param {string} claudeInstanceId - Claude instance identifier
63
+ * @returns {Promise<string>} Path to the created profile directory
64
+ */
65
+ async createSessionProfile(sessionId, claudeInstanceId) {
66
+ if (!this.initialized) {
67
+ await this.initialize();
68
+ }
69
+
70
+ // Validate inputs
71
+ if (!sessionId || typeof sessionId !== 'string') {
72
+ throw new Error('Valid sessionId is required');
73
+ }
74
+ if (!claudeInstanceId || typeof claudeInstanceId !== 'string') {
75
+ throw new Error('Valid claudeInstanceId is required');
76
+ }
77
+
78
+ // Security: Validate session ID doesn't contain path traversal attempts
79
+ // Decode URL-encoded attempts first
80
+ let decodedSessionId = sessionId;
81
+ try {
82
+ decodedSessionId = decodeURIComponent(sessionId);
83
+ } catch (e) {
84
+ // If decoding fails, use original
85
+ }
86
+
87
+ if (sessionId.includes('..') || sessionId.includes('/') || sessionId.includes('\\') ||
88
+ decodedSessionId.includes('..') || decodedSessionId.includes('/') || decodedSessionId.includes('\\') ||
89
+ sessionId.includes('%2e') || sessionId.includes('%2f') || sessionId.includes('%5c')) {
90
+ throw new Error('Security: Session ID contains invalid characters');
91
+ }
92
+
93
+ // Check if profile already exists
94
+ if (this.activeProfiles.has(sessionId)) {
95
+ throw new Error(`Profile already exists for session: ${sessionId}`);
96
+ }
97
+
98
+ try {
99
+ // Create unique profile directory
100
+ const profileName = `session-${sessionId}`;
101
+ const profilePath = path.join(this.profilesDir, profileName);
102
+
103
+ // Security: Ensure the profile path is within our profiles directory
104
+ const resolvedProfilePath = path.resolve(profilePath);
105
+ const resolvedProfilesDir = path.resolve(this.profilesDir);
106
+
107
+ if (!resolvedProfilePath.startsWith(resolvedProfilesDir)) {
108
+ throw new Error('Security: Profile path outside allowed directory');
109
+ }
110
+
111
+ // Check total space usage before creating new profile
112
+ await this.checkSpaceQuota();
113
+
114
+ // Create profile directory structure
115
+ await fs.mkdir(profilePath, { recursive: true });
116
+
117
+ // Create Chrome profile subdirectories
118
+ const chromeProfileDirs = [
119
+ 'Default',
120
+ 'Default/Cache',
121
+ 'Default/Code Cache',
122
+ 'Default/GPUCache',
123
+ 'Default/Storage',
124
+ 'Default/databases',
125
+ 'Default/Local Storage',
126
+ 'Default/Session Storage'
127
+ ];
128
+
129
+ for (const dir of chromeProfileDirs) {
130
+ await fs.mkdir(path.join(profilePath, dir), { recursive: true });
131
+ }
132
+
133
+ // Create profile configuration
134
+ await this.createProfileConfig(profilePath, sessionId, claudeInstanceId);
135
+
136
+ // Track the profile
137
+ const profileInfo = {
138
+ profilePath: resolvedProfilePath,
139
+ claudeInstanceId,
140
+ createdAt: new Date(),
141
+ size: 0 // Will be updated during cleanup
142
+ };
143
+
144
+ this.activeProfiles.set(sessionId, profileInfo);
145
+
146
+ console.log(`[ProfileManager] Created profile for session ${sessionId}: ${resolvedProfilePath}`);
147
+ return resolvedProfilePath;
148
+ } catch (error) {
149
+ console.error(`[ProfileManager] Failed to create profile for session ${sessionId}:`, error);
150
+ // Cleanup partial profile creation
151
+ try {
152
+ const profilePath = path.join(this.profilesDir, `session-${sessionId}`);
153
+ await fs.rm(profilePath, { recursive: true, force: true });
154
+ } catch (cleanupError) {
155
+ console.warn('[ProfileManager] Failed to cleanup partial profile:', cleanupError);
156
+ }
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Creates a basic Chrome profile configuration
163
+ * @param {string} profilePath - Path to the profile directory
164
+ * @param {string} sessionId - Session ID
165
+ * @param {string} claudeInstanceId - Claude instance ID
166
+ */
167
+ async createProfileConfig(profilePath, sessionId, claudeInstanceId) {
168
+ const defaultDir = path.join(profilePath, 'Default');
169
+
170
+ // Create Preferences file with security settings
171
+ const preferences = {
172
+ profile: {
173
+ name: `Chrome Pilot Session ${sessionId.substring(0, 8)}`,
174
+ managed_user_id: sessionId,
175
+ is_supervised: false
176
+ },
177
+ browser: {
178
+ check_default_browser: false,
179
+ show_home_button: false
180
+ },
181
+ security: {
182
+ // Enhanced security for isolated sessions
183
+ allow_cross_origin_auth_prompts: false,
184
+ mixed_content: {
185
+ auto_upgrade_insecure_requests: true
186
+ }
187
+ },
188
+ privacy: {
189
+ // Privacy settings for session isolation
190
+ dns_prefetching_enabled: false,
191
+ network_prediction_enabled: false,
192
+ background_mode_enabled: false
193
+ },
194
+ session: {
195
+ restore_on_startup: 1, // New Tab Page
196
+ startup_urls: []
197
+ },
198
+ // Session metadata for debugging
199
+ chrome_pilot: {
200
+ session_id: sessionId,
201
+ claude_instance_id: claudeInstanceId,
202
+ created_at: new Date().toISOString(),
203
+ profile_version: '1.0'
204
+ }
205
+ };
206
+
207
+ const preferencesPath = path.join(defaultDir, 'Preferences');
208
+ await fs.writeFile(preferencesPath, JSON.stringify(preferences, null, 2));
209
+
210
+ // Create Local State file
211
+ const localState = {
212
+ profile: {
213
+ info_cache: {
214
+ Default: {
215
+ name: `Chrome Pilot Session ${sessionId.substring(0, 8)}`,
216
+ user_name: `session-${sessionId}`,
217
+ is_supervised: false,
218
+ is_child: false,
219
+ is_force_signin_enabled: false
220
+ }
221
+ },
222
+ last_used: 'Default',
223
+ last_active_profiles: ['Default']
224
+ }
225
+ };
226
+
227
+ const localStatePath = path.join(profilePath, 'Local State');
228
+ await fs.writeFile(localStatePath, JSON.stringify(localState, null, 2));
229
+
230
+ console.log(`[ProfileManager] Created profile configuration for session ${sessionId}`);
231
+ }
232
+
233
+ /**
234
+ * Cleans up a session profile
235
+ * @param {string} sessionId - Session ID to cleanup
236
+ * @returns {Promise<boolean>} True if cleanup was successful
237
+ */
238
+ async cleanupProfile(sessionId) {
239
+ if (!sessionId || !this.activeProfiles.has(sessionId)) {
240
+ console.warn(`[ProfileManager] Attempted to cleanup non-existent profile: ${sessionId}`);
241
+ return false;
242
+ }
243
+
244
+ try {
245
+ const profileInfo = this.activeProfiles.get(sessionId);
246
+ const profilePath = profileInfo.profilePath;
247
+
248
+ // Security: Validate profile path before deletion
249
+ const resolvedProfilePath = path.resolve(profilePath);
250
+ const resolvedProfilesDir = path.resolve(this.profilesDir);
251
+
252
+ if (!resolvedProfilePath.startsWith(resolvedProfilesDir)) {
253
+ throw new Error('Security: Refusing to delete path outside profiles directory');
254
+ }
255
+
256
+ // Calculate profile size for logging
257
+ try {
258
+ const stats = await this.calculateDirectorySize(profilePath);
259
+ console.log(`[ProfileManager] Profile ${sessionId} size: ${(stats.size / 1024 / 1024).toFixed(2)}MB (${stats.files} files)`);
260
+ } catch (sizeError) {
261
+ console.warn('[ProfileManager] Could not calculate profile size:', sizeError);
262
+ }
263
+
264
+ // Remove profile directory
265
+ await fs.rm(profilePath, { recursive: true, force: true });
266
+
267
+ // Remove from active profiles
268
+ this.activeProfiles.delete(sessionId);
269
+
270
+ console.log(`[ProfileManager] Cleaned up profile for session ${sessionId}`);
271
+ return true;
272
+ } catch (error) {
273
+ console.error(`[ProfileManager] Error cleaning up profile for session ${sessionId}:`, error);
274
+ return false;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Checks space quota before creating new profiles
280
+ */
281
+ async checkSpaceQuota() {
282
+ try {
283
+ const totalSize = await this.calculateTotalProfilesSize();
284
+ const totalSizeMB = totalSize / 1024 / 1024;
285
+
286
+ if (totalSizeMB > this.config.maxTotalProfilesMB) {
287
+ throw new Error(`Total profiles size (${totalSizeMB.toFixed(2)}MB) exceeds limit (${this.config.maxTotalProfilesMB}MB)`);
288
+ }
289
+
290
+ console.log(`[ProfileManager] Current profiles size: ${totalSizeMB.toFixed(2)}MB / ${this.config.maxTotalProfilesMB}MB`);
291
+ } catch (error) {
292
+ if (error.message.includes('exceeds limit')) {
293
+ throw error;
294
+ }
295
+ console.warn('[ProfileManager] Could not check space quota:', error);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Calculates total size of all profiles
301
+ * @returns {Promise<number>} Total size in bytes
302
+ */
303
+ async calculateTotalProfilesSize() {
304
+ try {
305
+ const entries = await fs.readdir(this.profilesDir);
306
+ let totalSize = 0;
307
+
308
+ for (const entry of entries) {
309
+ const entryPath = path.join(this.profilesDir, entry);
310
+ const stats = await fs.stat(entryPath);
311
+
312
+ if (stats.isDirectory()) {
313
+ const dirStats = await this.calculateDirectorySize(entryPath);
314
+ totalSize += dirStats.size;
315
+ }
316
+ }
317
+
318
+ return totalSize;
319
+ } catch (error) {
320
+ console.warn('[ProfileManager] Error calculating total profiles size:', error);
321
+ return 0;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Calculates size of a directory recursively
327
+ * @param {string} dirPath - Directory path
328
+ * @returns {Promise<{size: number, files: number}>} Size in bytes and file count
329
+ */
330
+ async calculateDirectorySize(dirPath) {
331
+ let totalSize = 0;
332
+ let fileCount = 0;
333
+
334
+ async function traverse(currentPath) {
335
+ try {
336
+ const entries = await fs.readdir(currentPath);
337
+
338
+ for (const entry of entries) {
339
+ const entryPath = path.join(currentPath, entry);
340
+ const stats = await fs.stat(entryPath);
341
+
342
+ if (stats.isDirectory()) {
343
+ await traverse(entryPath);
344
+ } else {
345
+ totalSize += stats.size;
346
+ fileCount++;
347
+ }
348
+ }
349
+ } catch (error) {
350
+ // Skip directories/files we can't access
351
+ console.debug(`[ProfileManager] Skipping inaccessible path: ${currentPath}`);
352
+ }
353
+ }
354
+
355
+ await traverse(dirPath);
356
+ return { size: totalSize, files: fileCount };
357
+ }
358
+
359
+ /**
360
+ * Cleans up orphaned profiles from previous runs
361
+ */
362
+ async cleanupOrphanedProfiles() {
363
+ try {
364
+ const entries = await fs.readdir(this.profilesDir);
365
+ const sessionDirs = entries.filter(entry => entry.startsWith('session-'));
366
+
367
+ let cleanedCount = 0;
368
+
369
+ for (const sessionDir of sessionDirs) {
370
+ const sessionDirPath = path.join(this.profilesDir, sessionDir);
371
+
372
+ try {
373
+ const stats = await fs.stat(sessionDirPath);
374
+ const age = Date.now() - stats.mtime.getTime();
375
+
376
+ // Clean up profiles older than configured age
377
+ if (age > this.config.profileCleanupAgeMs) {
378
+ await fs.rm(sessionDirPath, { recursive: true, force: true });
379
+ cleanedCount++;
380
+ console.log(`[ProfileManager] Cleaned up orphaned profile: ${sessionDir}`);
381
+ }
382
+ } catch (error) {
383
+ console.warn(`[ProfileManager] Error processing orphaned profile ${sessionDir}:`, error);
384
+ }
385
+ }
386
+
387
+ if (cleanedCount > 0) {
388
+ console.log(`[ProfileManager] Cleaned up ${cleanedCount} orphaned profiles`);
389
+ }
390
+ } catch (error) {
391
+ console.warn('[ProfileManager] Error during orphaned profile cleanup:', error);
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Gets information about a specific profile
397
+ * @param {string} sessionId - Session ID
398
+ * @returns {Object|null} Profile information or null if not found
399
+ */
400
+ getProfileInfo(sessionId) {
401
+ if (this.activeProfiles.has(sessionId)) {
402
+ return { ...this.activeProfiles.get(sessionId) };
403
+ }
404
+ return null;
405
+ }
406
+
407
+ /**
408
+ * Lists all active profiles
409
+ * @returns {Array} Array of profile information objects
410
+ */
411
+ listActiveProfiles() {
412
+ const profiles = [];
413
+ for (const [sessionId, profileInfo] of this.activeProfiles) {
414
+ profiles.push({
415
+ sessionId,
416
+ ...profileInfo
417
+ });
418
+ }
419
+ return profiles;
420
+ }
421
+
422
+ /**
423
+ * Gets manager status and statistics
424
+ * @returns {Object} Status information
425
+ */
426
+ getStatus() {
427
+ return {
428
+ initialized: this.initialized,
429
+ profilesDir: this.profilesDir,
430
+ activeProfiles: this.activeProfiles.size,
431
+ config: this.config
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Cleanup method for graceful shutdown
437
+ */
438
+ async cleanup() {
439
+ console.log('[ProfileManager] Cleaning up active profiles...');
440
+
441
+ // Clean up all active profiles
442
+ const sessionIds = Array.from(this.activeProfiles.keys());
443
+ for (const sessionId of sessionIds) {
444
+ await this.cleanupProfile(sessionId);
445
+ }
446
+
447
+ console.log('[ProfileManager] Cleanup completed');
448
+ }
449
+ }