@dynamicu/chromedebug-mcp 2.3.0 → 2.4.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/README.md CHANGED
@@ -123,6 +123,56 @@ Chrome Debug includes a Chrome extension that enables visual element selection f
123
123
  - Console logs can be included in the recording
124
124
  - Workflows can be replayed later for debugging
125
125
 
126
+ ### Managing License Activations
127
+
128
+ Chrome Debug uses a license activation system to prevent activation limit exhaustion when reinstalling the extension.
129
+
130
+ #### The Problem
131
+
132
+ When you uninstall and reinstall the Chrome extension, it clears local storage and generates a new activation instance. With a typical 3-activation limit, frequent reinstalls can exhaust your available slots.
133
+
134
+ #### The Solution: Activation Manager
135
+
136
+ Chrome Debug now includes an **Activation Manager** that automatically handles this scenario:
137
+
138
+ **How It Works:**
139
+ 1. When activation limit is reached, the Activation Manager opens automatically
140
+ 2. Shows all your current activations with device info (e.g., "macOS 14.0 • Chrome 121")
141
+ 3. Identifies your current device (highlighted in green)
142
+ 4. You select which activation to deactivate
143
+ 5. Extension automatically retries activation on current device
144
+
145
+ **Example: Reinstalling on Same Machine**
146
+
147
+ ```
148
+ You reinstall the extension on your laptop:
149
+ → Try to activate license
150
+ → Activation Manager shows:
151
+ • Activation 1: "macOS 14.0 • Chrome 120" (Oct 10) ← OLD instance
152
+ • Activation 2: "Windows 11 • Chrome 121" (Oct 12)
153
+ • Activation 3: "Linux • Firefox 119" (Oct 14)
154
+ → You recognize #1 is this same laptop
155
+ → Click "Deactivate" on #1
156
+ → Extension automatically activates new instance
157
+ → Result: Still 3/3 slots, seamless experience
158
+ ```
159
+
160
+ **Best Practices:**
161
+ - Before uninstalling: No action needed - Activation Manager handles it
162
+ - Multiple devices: Activation Manager helps track which is which
163
+ - Unused devices: Periodically deactivate machines you no longer use
164
+
165
+ #### FAQ
166
+
167
+ **Q: What happens if I forget which activation is which?**
168
+ A: The Activation Manager shows device information (OS, browser version) and activation dates. Your current device is highlighted in green.
169
+
170
+ **Q: Can I deactivate my current device?**
171
+ A: No, the button is disabled for your current device to prevent accidental lockout.
172
+
173
+ **Q: Does reinstalling the browser consume an activation?**
174
+ A: Yes, because local storage is cleared. Use the Activation Manager to deactivate the old instance first or after reinstalling.
175
+
126
176
 
127
177
  ## Development
128
178
 
@@ -0,0 +1,208 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Manage License Activations</title>
6
+ <style>
7
+ body {
8
+ width: 450px;
9
+ padding: 20px;
10
+ font-family: Arial, sans-serif;
11
+ background: #f5f5f5;
12
+ }
13
+
14
+ h2 {
15
+ margin: 0 0 10px 0;
16
+ font-size: 20px;
17
+ color: #333;
18
+ }
19
+
20
+ .subtitle {
21
+ font-size: 14px;
22
+ color: #666;
23
+ margin-bottom: 20px;
24
+ }
25
+
26
+ .limit-info {
27
+ background: #fff3cd;
28
+ border: 1px solid #ffc107;
29
+ padding: 15px;
30
+ border-radius: 4px;
31
+ margin-bottom: 20px;
32
+ font-size: 14px;
33
+ color: #856404;
34
+ }
35
+
36
+ .limit-info strong {
37
+ color: #333;
38
+ }
39
+
40
+ .activations-list {
41
+ background: white;
42
+ border-radius: 4px;
43
+ border: 1px solid #e0e0e0;
44
+ max-height: 400px;
45
+ overflow-y: auto;
46
+ }
47
+
48
+ .activation-item {
49
+ padding: 15px;
50
+ border-bottom: 1px solid #e0e0e0;
51
+ transition: background 0.2s;
52
+ }
53
+
54
+ .activation-item:last-child {
55
+ border-bottom: none;
56
+ }
57
+
58
+ .activation-item:hover {
59
+ background: #f9f9f9;
60
+ }
61
+
62
+ .activation-item.current-device {
63
+ background: #e8f5e9;
64
+ border-left: 4px solid #4caf50;
65
+ }
66
+
67
+ .activation-header {
68
+ display: flex;
69
+ justify-content: space-between;
70
+ align-items: center;
71
+ margin-bottom: 10px;
72
+ }
73
+
74
+ .activation-number {
75
+ font-weight: bold;
76
+ font-size: 16px;
77
+ color: #333;
78
+ }
79
+
80
+ .current-badge {
81
+ background: #4caf50;
82
+ color: white;
83
+ padding: 2px 8px;
84
+ border-radius: 12px;
85
+ font-size: 11px;
86
+ font-weight: bold;
87
+ }
88
+
89
+ .device-info {
90
+ font-size: 14px;
91
+ color: #666;
92
+ margin-bottom: 8px;
93
+ }
94
+
95
+ .device-info strong {
96
+ color: #333;
97
+ }
98
+
99
+ .activation-date {
100
+ font-size: 12px;
101
+ color: #999;
102
+ margin-bottom: 10px;
103
+ }
104
+
105
+ .deactivate-btn {
106
+ padding: 8px 16px;
107
+ background: #f44336;
108
+ color: white;
109
+ border: none;
110
+ border-radius: 4px;
111
+ cursor: pointer;
112
+ font-size: 13px;
113
+ font-weight: 500;
114
+ transition: background 0.2s;
115
+ }
116
+
117
+ .deactivate-btn:hover {
118
+ background: #d32f2f;
119
+ }
120
+
121
+ .deactivate-btn:disabled {
122
+ background: #ccc;
123
+ cursor: not-allowed;
124
+ }
125
+
126
+ .actions {
127
+ margin-top: 20px;
128
+ display: flex;
129
+ gap: 10px;
130
+ }
131
+
132
+ .cancel-btn {
133
+ flex: 1;
134
+ padding: 10px;
135
+ background: #9e9e9e;
136
+ color: white;
137
+ border: none;
138
+ border-radius: 4px;
139
+ cursor: pointer;
140
+ font-size: 14px;
141
+ font-weight: 500;
142
+ }
143
+
144
+ .cancel-btn:hover {
145
+ background: #757575;
146
+ }
147
+
148
+ .loading {
149
+ text-align: center;
150
+ padding: 40px;
151
+ font-size: 14px;
152
+ color: #666;
153
+ }
154
+
155
+ .error {
156
+ background: #ffebee;
157
+ border: 1px solid #f44336;
158
+ padding: 15px;
159
+ border-radius: 4px;
160
+ color: #c62828;
161
+ font-size: 14px;
162
+ margin-bottom: 20px;
163
+ }
164
+
165
+ .spinner {
166
+ border: 3px solid #f3f3f3;
167
+ border-top: 3px solid #2196F3;
168
+ border-radius: 50%;
169
+ width: 30px;
170
+ height: 30px;
171
+ animation: spin 1s linear infinite;
172
+ margin: 20px auto;
173
+ }
174
+
175
+ @keyframes spin {
176
+ 0% { transform: rotate(0deg); }
177
+ 100% { transform: rotate(360deg); }
178
+ }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ <h2>⚠️ Activation Limit Reached</h2>
183
+ <p class="subtitle">Please deactivate an existing activation to continue.</p>
184
+
185
+ <div id="error-message" class="error" style="display: none;"></div>
186
+
187
+ <div id="limit-info" class="limit-info" style="display: none;">
188
+ <strong>License Status:</strong> <span id="activation-status">Loading...</span>
189
+ </div>
190
+
191
+ <div id="loading" class="loading">
192
+ <div class="spinner"></div>
193
+ <p>Loading activations...</p>
194
+ </div>
195
+
196
+ <div id="activations-container" style="display: none;">
197
+ <div class="activations-list" id="activations-list">
198
+ <!-- Activations will be populated here -->
199
+ </div>
200
+
201
+ <div class="actions">
202
+ <button class="cancel-btn" id="cancel-btn">Cancel</button>
203
+ </div>
204
+ </div>
205
+
206
+ <script type="module" src="activation-manager.js"></script>
207
+ </body>
208
+ </html>
@@ -0,0 +1,187 @@
1
+ // Activation Manager for Chrome Debug Extension
2
+ import { FirebaseLicenseClient } from './firebase-client.js';
3
+
4
+ const licenseClient = new FirebaseLicenseClient();
5
+
6
+ let licenseKey = '';
7
+ let currentInstanceId = '';
8
+ let activations = [];
9
+
10
+ // Initialize on page load
11
+ document.addEventListener('DOMContentLoaded', async () => {
12
+ await loadActivations();
13
+
14
+ // Set up event listeners
15
+ document.getElementById('cancel-btn').addEventListener('click', () => {
16
+ window.close();
17
+ });
18
+ });
19
+
20
+ /**
21
+ * Load activations from storage and Firebase
22
+ */
23
+ async function loadActivations() {
24
+ try {
25
+ // Get license key and instance ID from storage
26
+ const stored = await chrome.storage.local.get(['ls_license_key', 'ls_instance_id', 'chromedebug_instance_id']);
27
+ licenseKey = stored.ls_license_key;
28
+ currentInstanceId = stored.chromedebug_instance_id;
29
+
30
+ if (!licenseKey) {
31
+ showError('License key not found. Please activate your license first.');
32
+ return;
33
+ }
34
+
35
+ // Fetch activations from Firebase
36
+ const data = await licenseClient.listActivations(licenseKey);
37
+ activations = data.activations || [];
38
+
39
+ // Show activation status
40
+ document.getElementById('activation-status').textContent =
41
+ `${data.activationUsage} of ${data.activationLimit} activations used`;
42
+ document.getElementById('limit-info').style.display = 'block';
43
+
44
+ // Hide loading, show activations
45
+ document.getElementById('loading').style.display = 'none';
46
+ document.getElementById('activations-container').style.display = 'block';
47
+
48
+ // Render activations list
49
+ renderActivations();
50
+
51
+ } catch (error) {
52
+ console.error('[Activation Manager] Error loading activations:', error);
53
+ showError(`Failed to load activations: ${error.message}`);
54
+ document.getElementById('loading').style.display = 'none';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Render the list of activations
60
+ */
61
+ function renderActivations() {
62
+ const container = document.getElementById('activations-list');
63
+ container.innerHTML = '';
64
+
65
+ if (activations.length === 0) {
66
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No activations found.</div>';
67
+ return;
68
+ }
69
+
70
+ activations.forEach((activation, index) => {
71
+ const item = document.createElement('div');
72
+ item.className = 'activation-item';
73
+
74
+ // Check if this is the current device
75
+ const isCurrentDevice = activation.name === currentInstanceId ||
76
+ activation.identifier === currentInstanceId;
77
+
78
+ if (isCurrentDevice) {
79
+ item.classList.add('current-device');
80
+ }
81
+
82
+ // Format date
83
+ const date = new Date(activation.createdAt);
84
+ const formattedDate = date.toLocaleString('en-US', {
85
+ month: 'short',
86
+ day: 'numeric',
87
+ year: 'numeric',
88
+ hour: '2-digit',
89
+ minute: '2-digit'
90
+ });
91
+
92
+ // Build device info
93
+ let deviceDisplay = 'Unknown Device';
94
+ if (activation.deviceInfo) {
95
+ deviceDisplay = activation.deviceInfo.deviceName ||
96
+ `${activation.deviceInfo.platform || 'Unknown'} • ${activation.deviceInfo.browser || 'Unknown'}`;
97
+ } else if (activation.name && activation.name !== currentInstanceId) {
98
+ deviceDisplay = activation.name;
99
+ }
100
+
101
+ item.innerHTML = `
102
+ <div class="activation-header">
103
+ <span class="activation-number">Activation ${index + 1}</span>
104
+ ${isCurrentDevice ? '<span class="current-badge">CURRENT DEVICE</span>' : ''}
105
+ </div>
106
+ <div class="device-info">
107
+ <strong>${deviceDisplay}</strong>
108
+ </div>
109
+ <div class="activation-date">
110
+ Activated on ${formattedDate}
111
+ </div>
112
+ <button
113
+ class="deactivate-btn"
114
+ data-instance-id="${activation.identifier}"
115
+ ${isCurrentDevice ? 'disabled title="Cannot deactivate current device"' : ''}
116
+ >
117
+ ${isCurrentDevice ? '✓ Current Device' : 'Deactivate'}
118
+ </button>
119
+ `;
120
+
121
+ // Add click handler for deactivate button
122
+ if (!isCurrentDevice) {
123
+ const deactivateBtn = item.querySelector('.deactivate-btn');
124
+ deactivateBtn.addEventListener('click', () => {
125
+ handleDeactivate(activation.identifier, index + 1);
126
+ });
127
+ }
128
+
129
+ container.appendChild(item);
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Handle deactivation of an instance
135
+ */
136
+ async function handleDeactivate(instanceId, activationNumber) {
137
+ const confirmed = confirm(
138
+ `Are you sure you want to deactivate Activation ${activationNumber}?\n\n` +
139
+ `This will free up an activation slot, allowing you to activate on this device.`
140
+ );
141
+
142
+ if (!confirmed) {
143
+ return;
144
+ }
145
+
146
+ try {
147
+ // Disable all deactivate buttons immediately
148
+ const buttons = document.querySelectorAll('.deactivate-btn');
149
+ buttons.forEach(btn => btn.disabled = true);
150
+
151
+ // Call deactivate API
152
+ const result = await licenseClient.deactivateInstance(licenseKey, instanceId);
153
+
154
+ if (result.deactivated) {
155
+ // Show success message
156
+ alert('Instance deactivated successfully! Click OK to retry activation.');
157
+
158
+ // Close this window and retry activation
159
+ // Send message to popup to retry activation
160
+ chrome.runtime.sendMessage({
161
+ type: 'RETRY_ACTIVATION',
162
+ licenseKey: licenseKey
163
+ });
164
+
165
+ window.close();
166
+ } else {
167
+ throw new Error('Deactivation failed');
168
+ }
169
+
170
+ } catch (error) {
171
+ console.error('[Activation Manager] Deactivation error:', error);
172
+ showError(`Failed to deactivate: ${error.message}`);
173
+
174
+ // Re-enable buttons
175
+ const buttons = document.querySelectorAll('.deactivate-btn:not([data-current])');
176
+ buttons.forEach(btn => btn.disabled = false);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Show error message
182
+ */
183
+ function showError(message) {
184
+ const errorDiv = document.getElementById('error-message');
185
+ errorDiv.textContent = message;
186
+ errorDiv.style.display = 'block';
187
+ }
@@ -1537,7 +1537,7 @@ async function createUserNotification(priority, message) {
1537
1537
 
1538
1538
  const notificationOptions = {
1539
1539
  type: 'basic',
1540
- iconUrl: 'icons/icon48.png',
1540
+ iconUrl: chrome.runtime.getURL('icon48.png'),
1541
1541
  title: 'Chrome Debug Error',
1542
1542
  message: message,
1543
1543
  priority: priority === 'CRITICAL' ? 2 : 1,
@@ -2428,7 +2428,7 @@ async function startRecording(tabId, settings = {}) {
2428
2428
  // Show notification to user
2429
2429
  chrome.notifications.create({
2430
2430
  type: 'basic',
2431
- iconUrl: 'icon.png',
2431
+ iconUrl: chrome.runtime.getURL('icon48.png'),
2432
2432
  title: 'Recording Limit Reached',
2433
2433
  message: licenseCheck.message || 'Upgrade to Pro for unlimited recordings.',
2434
2434
  priority: 2
@@ -2938,7 +2938,7 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
2938
2938
  // Show notification to user (same pattern as screen recording)
2939
2939
  chrome.notifications.create({
2940
2940
  type: 'basic',
2941
- iconUrl: 'icon128.png',
2941
+ iconUrl: chrome.runtime.getURL('icon128.png'),
2942
2942
  title: 'Recording Limit Reached',
2943
2943
  message: licenseCheck.message || 'Daily limit reached. Upgrade to Pro for unlimited workflow recordings.',
2944
2944
  buttons: [{ title: 'Upgrade to Pro' }],
@@ -3749,7 +3749,7 @@ async function handleFrameCaptureComplete(sessionData) {
3749
3749
  // Show user notification about log association failure
3750
3750
  chrome.notifications.create({
3751
3751
  type: 'basic',
3752
- iconUrl: 'icon.png',
3752
+ iconUrl: chrome.runtime.getURL('icon48.png'),
3753
3753
  title: 'Chrome Debug - Log Association Failed',
3754
3754
  message: `${errorDetails} Recording saved but logs may be missing. Check console for details.`,
3755
3755
  priority: 1
@@ -3766,7 +3766,7 @@ async function handleFrameCaptureComplete(sessionData) {
3766
3766
  // Show user notification about no logs captured
3767
3767
  chrome.notifications.create({
3768
3768
  type: 'basic',
3769
- iconUrl: 'icon.png',
3769
+ iconUrl: chrome.runtime.getURL('icon48.png'),
3770
3770
  title: 'Chrome Debug - No Console Logs Captured',
3771
3771
  message: 'Recording completed but no console logs were captured. This may be expected if the page had no console activity.',
3772
3772
  priority: 0
@@ -29,6 +29,68 @@ class FirebaseLicenseClient {
29
29
  return instanceId;
30
30
  }
31
31
 
32
+ /**
33
+ * Collect device context information for activation tracking
34
+ * @returns {Object} Device information
35
+ */
36
+ getDeviceInfo() {
37
+ const ua = navigator.userAgent;
38
+ let browser = "Unknown";
39
+ let browserVersion = "Unknown";
40
+ let platform = "Unknown";
41
+
42
+ // Detect browser
43
+ if (ua.indexOf("Chrome") > -1 && ua.indexOf("Edg") === -1) {
44
+ browser = "Chrome";
45
+ const match = ua.match(/Chrome\/(\d+)/);
46
+ browserVersion = match ? match[1] : "Unknown";
47
+ } else if (ua.indexOf("Edg") > -1) {
48
+ browser = "Edge";
49
+ const match = ua.match(/Edg\/(\d+)/);
50
+ browserVersion = match ? match[1] : "Unknown";
51
+ } else if (ua.indexOf("Firefox") > -1) {
52
+ browser = "Firefox";
53
+ const match = ua.match(/Firefox\/(\d+)/);
54
+ browserVersion = match ? match[1] : "Unknown";
55
+ } else if (ua.indexOf("Safari") > -1) {
56
+ browser = "Safari";
57
+ const match = ua.match(/Version\/(\d+)/);
58
+ browserVersion = match ? match[1] : "Unknown";
59
+ }
60
+
61
+ // Detect platform
62
+ if (ua.indexOf("Win") > -1) {
63
+ platform = "Windows";
64
+ // Try to extract Windows version
65
+ if (ua.indexOf("Windows NT 10.0") > -1) platform = "Windows 10/11";
66
+ else if (ua.indexOf("Windows NT 6.3") > -1) platform = "Windows 8.1";
67
+ else if (ua.indexOf("Windows NT 6.2") > -1) platform = "Windows 8";
68
+ else if (ua.indexOf("Windows NT 6.1") > -1) platform = "Windows 7";
69
+ } else if (ua.indexOf("Mac") > -1) {
70
+ platform = "macOS";
71
+ // Try to extract macOS version from user agent
72
+ const match = ua.match(/Mac OS X (\d+[._]\d+)/);
73
+ if (match) {
74
+ const version = match[1].replace('_', '.');
75
+ platform = `macOS ${version}`;
76
+ }
77
+ } else if (ua.indexOf("Linux") > -1) {
78
+ platform = "Linux";
79
+ } else if (ua.indexOf("CrOS") > -1) {
80
+ platform = "Chrome OS";
81
+ }
82
+
83
+ return {
84
+ platform,
85
+ browser,
86
+ browserVersion,
87
+ userAgent: ua,
88
+ screenResolution: `${screen.width}x${screen.height}`,
89
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
90
+ language: navigator.language
91
+ };
92
+ }
93
+
32
94
  /**
33
95
  * Activate license with LemonSqueezy (first-time activation)
34
96
  * @param {string} licenseKey - License key to activate
@@ -36,16 +98,24 @@ class FirebaseLicenseClient {
36
98
  */
37
99
  async activateLicense(licenseKey) {
38
100
  const deviceId = await this.generateInstanceId();
101
+ const deviceInfo = this.getDeviceInfo();
39
102
 
40
103
  try {
41
104
  console.log(`[License] Activating license ${licenseKey} with device ${deviceId}`);
105
+ console.log('[License] Device info:', deviceInfo);
42
106
 
43
107
  const response = await fetch(`${this.functionsUrl}/activateLicense`, {
44
108
  method: 'POST',
45
109
  headers: {'Content-Type': 'application/json'},
46
110
  body: JSON.stringify({
47
111
  licenseKey: licenseKey.trim(),
48
- instanceName: deviceId
112
+ instanceName: deviceId,
113
+ deviceInfo: {
114
+ platform: deviceInfo.platform,
115
+ browser: deviceInfo.browser,
116
+ browserVersion: deviceInfo.browserVersion,
117
+ deviceName: `${deviceInfo.platform} • ${deviceInfo.browser} ${deviceInfo.browserVersion}`
118
+ }
49
119
  })
50
120
  });
51
121
 
@@ -273,6 +343,70 @@ class FirebaseLicenseClient {
273
343
  async clearLicenseCache() {
274
344
  await chrome.storage.local.remove(this.cacheKey);
275
345
  }
346
+
347
+ /**
348
+ * List all activations for a license key
349
+ * @param {string} licenseKey - License key
350
+ * @returns {Promise<{activations: Array, activationLimit: number, activationUsage: number, availableSlots: number}>}
351
+ */
352
+ async listActivations(licenseKey) {
353
+ try {
354
+ console.log(`[License] Listing activations for ${licenseKey}`);
355
+
356
+ const response = await fetch(`${this.functionsUrl}/listActivations`, {
357
+ method: 'POST',
358
+ headers: {'Content-Type': 'application/json'},
359
+ body: JSON.stringify({ licenseKey: licenseKey.trim() })
360
+ });
361
+
362
+ if (!response.ok) {
363
+ const errorData = await response.json();
364
+ throw new Error(errorData.error || `HTTP ${response.status}`);
365
+ }
366
+
367
+ const data = await response.json();
368
+ console.log('[License] Activations:', data);
369
+
370
+ return data;
371
+ } catch (error) {
372
+ console.error('[License] Failed to list activations:', error);
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Deactivate a specific license instance
379
+ * @param {string} licenseKey - License key
380
+ * @param {string} instanceId - Instance ID to deactivate
381
+ * @returns {Promise<{deactivated: boolean, message?: string}>}
382
+ */
383
+ async deactivateInstance(licenseKey, instanceId) {
384
+ try {
385
+ console.log(`[License] Deactivating instance ${instanceId}`);
386
+
387
+ const response = await fetch(`${this.functionsUrl}/deactivateInstance`, {
388
+ method: 'POST',
389
+ headers: {'Content-Type': 'application/json'},
390
+ body: JSON.stringify({
391
+ licenseKey: licenseKey.trim(),
392
+ instanceId
393
+ })
394
+ });
395
+
396
+ if (!response.ok) {
397
+ const errorData = await response.json();
398
+ throw new Error(errorData.error || `HTTP ${response.status}`);
399
+ }
400
+
401
+ const data = await response.json();
402
+ console.log('[License] Deactivation result:', data);
403
+
404
+ return data;
405
+ } catch (error) {
406
+ console.error('[License] Failed to deactivate instance:', error);
407
+ throw error;
408
+ }
409
+ }
276
410
  }
277
411
 
278
412
  export { FirebaseLicenseClient };