@vforsh/argus 0.1.18 → 0.1.20
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/dist/.tsbuildinfo +1 -1
- package/dist/argus.js +3008 -572
- package/dist/cli/plugins/pluginHost.d.ts.map +1 -1
- package/dist/cli/plugins/pluginHost.js +1 -0
- package/dist/cli/plugins/pluginHost.js.map +1 -1
- package/dist/cli/register/extensionCommands.d.ts.map +1 -1
- package/dist/cli/register/extensionCommands.js +131 -6
- package/dist/cli/register/extensionCommands.js.map +1 -1
- package/dist/cli/register/index.d.ts.map +1 -1
- package/dist/cli/register/index.js +6 -0
- package/dist/cli/register/index.js.map +1 -1
- package/dist/cli/register/recordCommands.d.ts +3 -0
- package/dist/cli/register/recordCommands.d.ts.map +1 -0
- package/dist/cli/register/recordCommands.js +90 -0
- package/dist/cli/register/recordCommands.js.map +1 -0
- package/dist/cli/register/skillCommands.d.ts +3 -0
- package/dist/cli/register/skillCommands.d.ts.map +1 -0
- package/dist/cli/register/skillCommands.js +13 -0
- package/dist/cli/register/skillCommands.js.map +1 -0
- package/dist/commands/domDrag.d.ts +21 -0
- package/dist/commands/domDrag.d.ts.map +1 -0
- package/dist/commands/domDrag.js +179 -0
- package/dist/commands/domDrag.js.map +1 -0
- package/dist/commands/extension/attach.d.ts +2 -0
- package/dist/commands/extension/attach.d.ts.map +1 -1
- package/dist/commands/extension/attach.js +31 -6
- package/dist/commands/extension/attach.js.map +1 -1
- package/dist/commands/extension/doctor.d.ts +6 -0
- package/dist/commands/extension/doctor.d.ts.map +1 -0
- package/dist/commands/extension/doctor.js +185 -0
- package/dist/commands/extension/doctor.js.map +1 -0
- package/dist/commands/extension/extensionId.d.ts +27 -0
- package/dist/commands/extension/extensionId.d.ts.map +1 -0
- package/dist/commands/extension/extensionId.js +37 -0
- package/dist/commands/extension/extensionId.js.map +1 -0
- package/dist/commands/extension/extensionPath.d.ts +15 -0
- package/dist/commands/extension/extensionPath.d.ts.map +1 -0
- package/dist/commands/extension/extensionPath.js +61 -0
- package/dist/commands/extension/extensionPath.js.map +1 -0
- package/dist/commands/extension/install.d.ts +16 -0
- package/dist/commands/extension/install.d.ts.map +1 -0
- package/dist/commands/extension/install.js +154 -0
- package/dist/commands/extension/install.js.map +1 -0
- package/dist/commands/extension/select.d.ts +7 -0
- package/dist/commands/extension/select.d.ts.map +1 -0
- package/dist/commands/extension/select.js +73 -0
- package/dist/commands/extension/select.js.map +1 -0
- package/dist/commands/extension/setup.d.ts +2 -1
- package/dist/commands/extension/setup.d.ts.map +1 -1
- package/dist/commands/extension/setup.js +5 -2
- package/dist/commands/extension/setup.js.map +1 -1
- package/dist/commands/extension/show.d.ts +4 -0
- package/dist/commands/extension/show.d.ts.map +1 -1
- package/dist/commands/extension/show.js +12 -88
- package/dist/commands/extension/show.js.map +1 -1
- package/dist/commands/extension/tabAttach.d.ts +28 -0
- package/dist/commands/extension/tabAttach.d.ts.map +1 -0
- package/dist/commands/extension/tabAttach.js +106 -0
- package/dist/commands/extension/tabAttach.js.map +1 -0
- package/dist/commands/extension/tabWatcher.d.ts +22 -0
- package/dist/commands/extension/tabWatcher.d.ts.map +1 -0
- package/dist/commands/extension/tabWatcher.js +89 -0
- package/dist/commands/extension/tabWatcher.js.map +1 -0
- package/dist/commands/extension/targetSelection.d.ts +62 -0
- package/dist/commands/extension/targetSelection.d.ts.map +1 -0
- package/dist/commands/extension/targetSelection.js +210 -0
- package/dist/commands/extension/targetSelection.js.map +1 -0
- package/dist/commands/extension/targets.d.ts +7 -0
- package/dist/commands/extension/targets.d.ts.map +1 -0
- package/dist/commands/extension/targets.js +73 -0
- package/dist/commands/extension/targets.js.map +1 -0
- package/dist/commands/extension/use.d.ts +13 -0
- package/dist/commands/extension/use.d.ts.map +1 -0
- package/dist/commands/extension/use.js +66 -0
- package/dist/commands/extension/use.js.map +1 -0
- package/dist/commands/record.d.ts +35 -0
- package/dist/commands/record.d.ts.map +1 -0
- package/dist/commands/record.js +210 -0
- package/dist/commands/record.js.map +1 -0
- package/dist/commands/skill.d.ts +6 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +42 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/watcherNativeHost.js +48 -1
- package/dist/commands/watcherNativeHost.js.map +1 -1
- package/dist/extension/dist/background/auth-header-monitor.js +77 -0
- package/dist/extension/dist/background/bridge-client.js +120 -0
- package/dist/extension/dist/background/cdp-proxy.js +201 -0
- package/dist/extension/dist/background/debugger-manager.js +296 -0
- package/dist/extension/dist/background/service-worker.js +2004 -0
- package/dist/extension/dist/background/service-worker.js.map +7 -0
- package/dist/extension/dist/popup/popup.js +730 -0
- package/dist/extension/dist/popup/popup.js.map +7 -0
- package/dist/extension/dist/popup/tabMarkup.js +104 -0
- package/dist/extension/dist/popup/viewState.js +60 -0
- package/dist/extension/dist/types/messages.js +5 -0
- package/dist/extension/icons/icon-128.png +0 -0
- package/dist/extension/icons/icon-16.png +0 -0
- package/dist/extension/icons/icon-48.png +0 -0
- package/dist/extension/manifest.json +26 -0
- package/dist/extension/src/popup/popup.html +756 -0
- package/dist/skill/argus/SKILL.md +202 -0
- package/dist/skill/argus/reference/CAPTURE.md +77 -0
- package/dist/skill/argus/reference/DIALOG.md +31 -0
- package/dist/skill/argus/reference/EVAL.md +137 -0
- package/dist/skill/argus/reference/EXTENSION.md +43 -0
- package/dist/skill/argus/reference/EXTENSION_IFRAME_EVAL.md +58 -0
- package/dist/skill/argus/reference/IFRAMES.md +46 -0
- package/dist/skill/argus/reference/INJECT.md +145 -0
- package/dist/skill/argus/reference/INSPECT.md +175 -0
- package/dist/skill/argus/reference/NET.md +108 -0
- package/dist/skill/argus/reference/PLUGINS.md +172 -0
- package/dist/skill/argus/reference/RUNTIME_CODE.md +42 -0
- package/dist/skill/argus/reference/START.md +120 -0
- package/dist/skill/argus/start-watcher.ts +108 -0
- package/package.json +2 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const AUTH_HEADER_NAMES = new Set([
|
|
2
|
+
'authorization',
|
|
3
|
+
'cookie',
|
|
4
|
+
'x-api-key',
|
|
5
|
+
'x-auth-token',
|
|
6
|
+
'x-csrf-token',
|
|
7
|
+
'x-csrftoken',
|
|
8
|
+
'x-xsrf-token',
|
|
9
|
+
'csrf-token',
|
|
10
|
+
'xsrf-token',
|
|
11
|
+
]);
|
|
12
|
+
const WEB_REQUEST_FILTER = { urls: ['<all_urls>'] };
|
|
13
|
+
const WEB_REQUEST_EXTRA_INFO = ['requestHeaders', 'extraHeaders'];
|
|
14
|
+
/**
|
|
15
|
+
* Observe auth-related request headers for currently attached tabs.
|
|
16
|
+
* We keep the extension payload narrow so the native bridge never has to ship full request headers.
|
|
17
|
+
*/
|
|
18
|
+
export class AuthHeaderMonitor {
|
|
19
|
+
debuggerManager;
|
|
20
|
+
bridgeClient;
|
|
21
|
+
constructor(debuggerManager, bridgeClient) {
|
|
22
|
+
this.debuggerManager = debuggerManager;
|
|
23
|
+
this.bridgeClient = bridgeClient;
|
|
24
|
+
chrome.webRequest.onBeforeSendHeaders.addListener((details) => {
|
|
25
|
+
this.handleBeforeSendHeaders(details);
|
|
26
|
+
}, WEB_REQUEST_FILTER, WEB_REQUEST_EXTRA_INFO);
|
|
27
|
+
}
|
|
28
|
+
handleBeforeSendHeaders(details) {
|
|
29
|
+
const message = createObservedHeadersMessage(details, this.debuggerManager);
|
|
30
|
+
if (!message) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.bridgeClient.send(message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const createObservedHeadersMessage = (details, debuggerManager) => {
|
|
37
|
+
if (details.tabId < 0 || !debuggerManager.isAttached(details.tabId)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const requestHeaders = pickObservedAuthHeaders(details.requestHeaders);
|
|
41
|
+
if (!requestHeaders) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: 'auth_headers_observed',
|
|
46
|
+
tabId: details.tabId,
|
|
47
|
+
url: details.url,
|
|
48
|
+
method: details.method,
|
|
49
|
+
ts: typeof details.timeStamp === 'number' ? details.timeStamp : Date.now(),
|
|
50
|
+
requestHeaders,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const pickObservedAuthHeaders = (headers) => {
|
|
54
|
+
if (!headers || headers.length === 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const observed = {};
|
|
58
|
+
for (const header of headers) {
|
|
59
|
+
const name = header.name?.trim().toLowerCase();
|
|
60
|
+
if (!name || !AUTH_HEADER_NAMES.has(name)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const value = normalizeHeaderValue(header.value);
|
|
64
|
+
if (!value) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
observed[name] = value;
|
|
68
|
+
}
|
|
69
|
+
return Object.keys(observed).length > 0 ? observed : undefined;
|
|
70
|
+
};
|
|
71
|
+
const normalizeHeaderValue = (value) => {
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
return trimmed || null;
|
|
77
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Messaging client for communicating with argus-watcher (extension mode).
|
|
3
|
+
* Handles connection lifecycle, message serialization, and reconnection.
|
|
4
|
+
*/
|
|
5
|
+
export class BridgeClient {
|
|
6
|
+
port = null;
|
|
7
|
+
hostName;
|
|
8
|
+
messageHandlers = new Set();
|
|
9
|
+
connectHandler = null;
|
|
10
|
+
disconnectHandler = null;
|
|
11
|
+
reconnectAttempts = 0;
|
|
12
|
+
maxReconnectAttempts = 5;
|
|
13
|
+
reconnectDelay = 1000;
|
|
14
|
+
constructor(hostName = 'com.vforsh.argus.bridge') {
|
|
15
|
+
this.hostName = hostName;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set handler for incoming messages from the host.
|
|
19
|
+
*/
|
|
20
|
+
onMessage(handler) {
|
|
21
|
+
this.messageHandlers.add(handler);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Set handler for successful connection.
|
|
25
|
+
*/
|
|
26
|
+
onConnect(handler) {
|
|
27
|
+
this.connectHandler = handler;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set handler for disconnection.
|
|
31
|
+
*/
|
|
32
|
+
onDisconnect(handler) {
|
|
33
|
+
this.disconnectHandler = handler;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Connect to the Native Messaging host.
|
|
37
|
+
*/
|
|
38
|
+
connect() {
|
|
39
|
+
if (this.port) {
|
|
40
|
+
return true; // Already connected
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
this.port = chrome.runtime.connectNative(this.hostName);
|
|
44
|
+
this.port.onMessage.addListener((message) => {
|
|
45
|
+
this.reconnectAttempts = 0; // Reset on successful message
|
|
46
|
+
for (const handler of this.messageHandlers) {
|
|
47
|
+
handler(message);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.port.onDisconnect.addListener(() => {
|
|
51
|
+
const error = chrome.runtime.lastError;
|
|
52
|
+
console.log('[BridgeClient] Disconnected:', error?.message ?? 'unknown reason');
|
|
53
|
+
this.port = null;
|
|
54
|
+
if (this.disconnectHandler) {
|
|
55
|
+
this.disconnectHandler();
|
|
56
|
+
}
|
|
57
|
+
// Attempt reconnection
|
|
58
|
+
this.scheduleReconnect();
|
|
59
|
+
});
|
|
60
|
+
console.log('[BridgeClient] Connected to', this.hostName);
|
|
61
|
+
this.reconnectAttempts = 0;
|
|
62
|
+
if (this.connectHandler) {
|
|
63
|
+
this.connectHandler();
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('[BridgeClient] Failed to connect:', err);
|
|
69
|
+
this.scheduleReconnect();
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Disconnect from the Native Messaging host.
|
|
75
|
+
*/
|
|
76
|
+
disconnect() {
|
|
77
|
+
if (this.port) {
|
|
78
|
+
this.port.disconnect();
|
|
79
|
+
this.port = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Send a message to the Native Messaging host.
|
|
84
|
+
*/
|
|
85
|
+
send(message) {
|
|
86
|
+
if (!this.port) {
|
|
87
|
+
console.warn('[BridgeClient] Cannot send, not connected');
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
this.port.postMessage(message);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error('[BridgeClient] Send failed:', err);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if connected to the host.
|
|
101
|
+
*/
|
|
102
|
+
isConnected() {
|
|
103
|
+
return this.port !== null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
107
|
+
*/
|
|
108
|
+
scheduleReconnect() {
|
|
109
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
110
|
+
console.log('[BridgeClient] Max reconnection attempts reached');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
|
|
114
|
+
this.reconnectAttempts++;
|
|
115
|
+
console.log(`[BridgeClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
this.connect();
|
|
118
|
+
}, delay);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Proxy - Routes CDP commands and events between the Native Messaging
|
|
3
|
+
* host (argus-watcher in extension mode) and the chrome.debugger API.
|
|
4
|
+
*/
|
|
5
|
+
export class CdpProxy {
|
|
6
|
+
debuggerManager;
|
|
7
|
+
bridgeClient;
|
|
8
|
+
constructor(debuggerManager, bridgeClient) {
|
|
9
|
+
this.debuggerManager = debuggerManager;
|
|
10
|
+
this.bridgeClient = bridgeClient;
|
|
11
|
+
this.setupEventForwarding();
|
|
12
|
+
this.setupMessageHandling();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Forward CDP events from debugger to bridge.
|
|
16
|
+
*/
|
|
17
|
+
setupEventForwarding() {
|
|
18
|
+
this.debuggerManager.onEvent((tabId, method, params, meta) => {
|
|
19
|
+
this.bridgeClient.send({
|
|
20
|
+
type: 'cdp_event',
|
|
21
|
+
tabId,
|
|
22
|
+
method,
|
|
23
|
+
params,
|
|
24
|
+
sessionId: meta?.sessionId ?? undefined,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
this.debuggerManager.onDetach((tabId, reason) => {
|
|
28
|
+
this.bridgeClient.send({
|
|
29
|
+
type: 'tab_detached',
|
|
30
|
+
tabId,
|
|
31
|
+
reason,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Handle messages from the bridge.
|
|
37
|
+
*/
|
|
38
|
+
setupMessageHandling() {
|
|
39
|
+
this.bridgeClient.onMessage((message) => {
|
|
40
|
+
this.handleMessage(message);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Process a message from the bridge.
|
|
45
|
+
*/
|
|
46
|
+
async handleMessage(message) {
|
|
47
|
+
switch (message.type) {
|
|
48
|
+
case 'attach_tab':
|
|
49
|
+
await this.handleAttachTab(message);
|
|
50
|
+
break;
|
|
51
|
+
case 'detach_tab':
|
|
52
|
+
await this.handleDetachTab(message);
|
|
53
|
+
break;
|
|
54
|
+
case 'cdp_command':
|
|
55
|
+
await this.handleCdpCommand(message);
|
|
56
|
+
break;
|
|
57
|
+
case 'list_tabs':
|
|
58
|
+
await this.handleListTabs(message);
|
|
59
|
+
break;
|
|
60
|
+
case 'enable_domain':
|
|
61
|
+
await this.handleEnableDomain(message);
|
|
62
|
+
break;
|
|
63
|
+
case 'host_info':
|
|
64
|
+
case 'target_info':
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
console.warn('[CdpProxy] Unknown message type:', message.type);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Attach to a tab.
|
|
72
|
+
*/
|
|
73
|
+
async handleAttachTab(message) {
|
|
74
|
+
try {
|
|
75
|
+
const target = await this.debuggerManager.attach(message.tabId);
|
|
76
|
+
this.bridgeClient.send({
|
|
77
|
+
type: 'tab_attached',
|
|
78
|
+
tabId: target.tabId,
|
|
79
|
+
url: target.url,
|
|
80
|
+
title: target.title,
|
|
81
|
+
faviconUrl: target.faviconUrl,
|
|
82
|
+
topFrameId: target.topFrameId,
|
|
83
|
+
frames: this.debuggerManager.getFrames(target.tabId),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error('[CdpProxy] Failed to attach:', err);
|
|
88
|
+
this.bridgeClient.send({
|
|
89
|
+
type: 'tab_detached',
|
|
90
|
+
tabId: message.tabId,
|
|
91
|
+
reason: err instanceof Error ? err.message : 'attach_failed',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Detach from a tab.
|
|
97
|
+
*/
|
|
98
|
+
async handleDetachTab(message) {
|
|
99
|
+
await this.debuggerManager.detach(message.tabId);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Execute a CDP command.
|
|
103
|
+
*/
|
|
104
|
+
async handleCdpCommand(message) {
|
|
105
|
+
try {
|
|
106
|
+
const result = await this.debuggerManager.sendCommand(message.tabId, message.method, message.params, message.sessionId);
|
|
107
|
+
this.bridgeClient.send({
|
|
108
|
+
type: 'cdp_response',
|
|
109
|
+
requestId: message.requestId,
|
|
110
|
+
result,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
this.bridgeClient.send({
|
|
115
|
+
type: 'cdp_response',
|
|
116
|
+
requestId: message.requestId,
|
|
117
|
+
error: {
|
|
118
|
+
message: err instanceof Error ? err.message : 'Unknown error',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* List available tabs.
|
|
125
|
+
*/
|
|
126
|
+
async handleListTabs(message) {
|
|
127
|
+
const chromeTabs = await chrome.tabs.query({});
|
|
128
|
+
const attachedTargets = this.debuggerManager.listAttached();
|
|
129
|
+
const attachedTabIds = new Set(attachedTargets.map((t) => t.tabId));
|
|
130
|
+
let tabs = chromeTabs
|
|
131
|
+
.filter((tab) => tab.id !== undefined && tab.url !== undefined)
|
|
132
|
+
.map((tab) => ({
|
|
133
|
+
tabId: tab.id,
|
|
134
|
+
url: tab.url,
|
|
135
|
+
title: tab.title ?? '',
|
|
136
|
+
faviconUrl: tab.favIconUrl,
|
|
137
|
+
attached: attachedTabIds.has(tab.id),
|
|
138
|
+
}));
|
|
139
|
+
// Apply filters if provided
|
|
140
|
+
if (message.filter) {
|
|
141
|
+
if (message.filter.url) {
|
|
142
|
+
const urlFilter = message.filter.url.toLowerCase();
|
|
143
|
+
tabs = tabs.filter((t) => t.url.toLowerCase().includes(urlFilter));
|
|
144
|
+
}
|
|
145
|
+
if (message.filter.title) {
|
|
146
|
+
const titleFilter = message.filter.title.toLowerCase();
|
|
147
|
+
tabs = tabs.filter((t) => t.title.toLowerCase().includes(titleFilter));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
this.bridgeClient.send({
|
|
151
|
+
type: 'list_tabs_response',
|
|
152
|
+
tabs,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Enable a CDP domain for a tab.
|
|
157
|
+
*/
|
|
158
|
+
async handleEnableDomain(message) {
|
|
159
|
+
try {
|
|
160
|
+
await this.debuggerManager.enableDomain(message.tabId, message.domain);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error('[CdpProxy] Failed to enable domain:', err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Manually attach to a tab (called from popup).
|
|
168
|
+
*/
|
|
169
|
+
async attachTab(tabId) {
|
|
170
|
+
await this.handleAttachTab({ type: 'attach_tab', tabId });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Manually detach from a tab (called from popup).
|
|
174
|
+
*/
|
|
175
|
+
async detachTab(tabId) {
|
|
176
|
+
await this.debuggerManager.detach(tabId);
|
|
177
|
+
this.bridgeClient.send({
|
|
178
|
+
type: 'tab_detached',
|
|
179
|
+
tabId,
|
|
180
|
+
reason: 'user_requested',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get list of tabs for popup UI.
|
|
185
|
+
*/
|
|
186
|
+
async getTabsForPopup() {
|
|
187
|
+
const chromeTabs = await chrome.tabs.query({});
|
|
188
|
+
const attachedTargets = this.debuggerManager.listAttached();
|
|
189
|
+
const attachedTabIds = new Set(attachedTargets.map((t) => t.tabId));
|
|
190
|
+
return chromeTabs
|
|
191
|
+
.filter((tab) => tab.id !== undefined && tab.url !== undefined)
|
|
192
|
+
.filter((tab) => !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://'))
|
|
193
|
+
.map((tab) => ({
|
|
194
|
+
tabId: tab.id,
|
|
195
|
+
url: tab.url,
|
|
196
|
+
title: tab.title ?? '',
|
|
197
|
+
faviconUrl: tab.favIconUrl,
|
|
198
|
+
attached: attachedTabIds.has(tab.id),
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages chrome.debugger attachment lifecycle.
|
|
3
|
+
* Handles root tab attachment, recursive child-target auto-attach, frame discovery,
|
|
4
|
+
* and session-aware CDP command routing.
|
|
5
|
+
*/
|
|
6
|
+
export class DebuggerManager {
|
|
7
|
+
attached = new Map();
|
|
8
|
+
globalEventHandler = null;
|
|
9
|
+
detachHandler = null;
|
|
10
|
+
constructor() {
|
|
11
|
+
chrome.debugger.onEvent.addListener((debuggee, method, params) => {
|
|
12
|
+
this.handleCdpEvent(debuggee, method, params);
|
|
13
|
+
});
|
|
14
|
+
chrome.debugger.onDetach.addListener((debuggee, reason) => {
|
|
15
|
+
if (debuggee.tabId) {
|
|
16
|
+
this.handleDetach(debuggee.tabId, debuggee.sessionId ?? null, reason);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
onEvent(handler) {
|
|
21
|
+
this.globalEventHandler = handler;
|
|
22
|
+
}
|
|
23
|
+
onDetach(handler) {
|
|
24
|
+
this.detachHandler = handler;
|
|
25
|
+
}
|
|
26
|
+
async attach(tabId) {
|
|
27
|
+
if (this.attached.has(tabId)) {
|
|
28
|
+
return this.attached.get(tabId);
|
|
29
|
+
}
|
|
30
|
+
const debuggee = { tabId };
|
|
31
|
+
await chrome.debugger.attach(debuggee, '1.3');
|
|
32
|
+
const tab = await chrome.tabs.get(tabId);
|
|
33
|
+
const target = {
|
|
34
|
+
tabId,
|
|
35
|
+
debuggeeId: debuggee,
|
|
36
|
+
url: tab.url ?? '',
|
|
37
|
+
title: tab.title ?? '',
|
|
38
|
+
faviconUrl: tab.favIconUrl,
|
|
39
|
+
attachedAt: Date.now(),
|
|
40
|
+
enabledDomains: new Set(),
|
|
41
|
+
childSessions: new Map(),
|
|
42
|
+
frames: new Map(),
|
|
43
|
+
topFrameId: null,
|
|
44
|
+
};
|
|
45
|
+
this.attached.set(tabId, target);
|
|
46
|
+
await this.configureAutoAttach(tabId);
|
|
47
|
+
await this.refreshFrameTree(tabId, null);
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
50
|
+
async detach(tabId) {
|
|
51
|
+
const target = this.attached.get(tabId);
|
|
52
|
+
if (!target)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
await chrome.debugger.detach(target.debuggeeId);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Tab may already be closed
|
|
59
|
+
}
|
|
60
|
+
this.attached.delete(tabId);
|
|
61
|
+
}
|
|
62
|
+
async sendCommand(tabId, method, params, sessionId) {
|
|
63
|
+
const target = this.getRequiredTarget(tabId);
|
|
64
|
+
const debuggee = this.toDebuggee(target, sessionId);
|
|
65
|
+
const result = await chrome.debugger.sendCommand(debuggee, method, params);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
async enableDomain(tabId, domain, sessionId) {
|
|
69
|
+
const enabledDomains = this.getRequiredEnabledDomains(tabId, sessionId);
|
|
70
|
+
if (enabledDomains.has(domain)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await this.sendCommand(tabId, `${domain}.enable`, undefined, sessionId);
|
|
74
|
+
enabledDomains.add(domain);
|
|
75
|
+
}
|
|
76
|
+
isAttached(tabId) {
|
|
77
|
+
return this.attached.has(tabId);
|
|
78
|
+
}
|
|
79
|
+
listAttached() {
|
|
80
|
+
return [...this.attached.values()];
|
|
81
|
+
}
|
|
82
|
+
getTarget(tabId) {
|
|
83
|
+
return this.attached.get(tabId);
|
|
84
|
+
}
|
|
85
|
+
getFrames(tabId) {
|
|
86
|
+
const target = this.attached.get(tabId);
|
|
87
|
+
if (!target) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return [...target.frames.values()];
|
|
91
|
+
}
|
|
92
|
+
async configureAutoAttach(tabId, sessionId) {
|
|
93
|
+
await this.sendCommand(tabId, 'Target.setAutoAttach', {
|
|
94
|
+
autoAttach: true,
|
|
95
|
+
waitForDebuggerOnStart: false,
|
|
96
|
+
flatten: true,
|
|
97
|
+
filter: [{ type: 'iframe', exclude: false }],
|
|
98
|
+
}, sessionId);
|
|
99
|
+
}
|
|
100
|
+
getEnabledDomains(tabId, sessionId) {
|
|
101
|
+
const target = this.attached.get(tabId);
|
|
102
|
+
if (!target) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (!sessionId) {
|
|
106
|
+
return target.enabledDomains;
|
|
107
|
+
}
|
|
108
|
+
return target.childSessions.get(sessionId)?.enabledDomains ?? null;
|
|
109
|
+
}
|
|
110
|
+
getRequiredTarget(tabId) {
|
|
111
|
+
const target = this.attached.get(tabId);
|
|
112
|
+
if (!target) {
|
|
113
|
+
throw new Error(`Tab ${tabId} is not attached`);
|
|
114
|
+
}
|
|
115
|
+
return target;
|
|
116
|
+
}
|
|
117
|
+
getRequiredEnabledDomains(tabId, sessionId) {
|
|
118
|
+
const enabledDomains = this.getEnabledDomains(tabId, sessionId);
|
|
119
|
+
if (!enabledDomains) {
|
|
120
|
+
throw new Error(`Tab ${tabId} is not attached`);
|
|
121
|
+
}
|
|
122
|
+
return enabledDomains;
|
|
123
|
+
}
|
|
124
|
+
toDebuggee(target, sessionId) {
|
|
125
|
+
if (!sessionId) {
|
|
126
|
+
return target.debuggeeId;
|
|
127
|
+
}
|
|
128
|
+
return { tabId: target.tabId, sessionId };
|
|
129
|
+
}
|
|
130
|
+
handleCdpEvent(debuggee, method, params) {
|
|
131
|
+
const tabId = debuggee.tabId;
|
|
132
|
+
if (!tabId || !this.attached.has(tabId))
|
|
133
|
+
return;
|
|
134
|
+
const sessionId = debuggee.sessionId ?? null;
|
|
135
|
+
if (method === 'Target.attachedToTarget' && params) {
|
|
136
|
+
void this.handleAttachedToTarget(tabId, params);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (method === 'Target.detachedFromTarget' && params) {
|
|
140
|
+
this.handleDetachedFromTarget(tabId, params);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.updateStateFromEvent(tabId, sessionId, method, params);
|
|
144
|
+
if (this.globalEventHandler) {
|
|
145
|
+
this.globalEventHandler(tabId, method, params ?? {}, { sessionId });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async handleAttachedToTarget(tabId, params) {
|
|
149
|
+
const target = this.attached.get(tabId);
|
|
150
|
+
if (!target) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const record = params;
|
|
154
|
+
if (!record.sessionId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
target.childSessions.set(record.sessionId, {
|
|
158
|
+
sessionId: record.sessionId,
|
|
159
|
+
targetId: record.targetInfo?.targetId ?? record.sessionId,
|
|
160
|
+
type: record.targetInfo?.type ?? 'unknown',
|
|
161
|
+
url: record.targetInfo?.url ?? '',
|
|
162
|
+
title: record.targetInfo?.title ?? '',
|
|
163
|
+
attachedAt: Date.now(),
|
|
164
|
+
enabledDomains: new Set(),
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
await this.configureAutoAttach(tabId, record.sessionId);
|
|
168
|
+
await this.enableChildSessionDomains(tabId, record.sessionId);
|
|
169
|
+
await this.refreshFrameTree(tabId, record.sessionId);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.warn('[DebuggerManager] Failed to bootstrap child target:', error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async enableChildSessionDomains(tabId, sessionId) {
|
|
176
|
+
// Keep child sessions aligned with the root tab so watcher features can observe iframe traffic too.
|
|
177
|
+
for (const domain of ['Runtime', 'Page', 'Network']) {
|
|
178
|
+
await this.enableDomain(tabId, domain, sessionId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
handleDetachedFromTarget(tabId, params) {
|
|
182
|
+
const record = params;
|
|
183
|
+
if (!record.sessionId) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.dropChildSession(tabId, record.sessionId);
|
|
187
|
+
}
|
|
188
|
+
async refreshFrameTree(tabId, sessionId) {
|
|
189
|
+
const frameTree = (await this.sendCommand(tabId, 'Page.getFrameTree', undefined, sessionId));
|
|
190
|
+
this.mergeFrameTree(tabId, sessionId, frameTree.frameTree);
|
|
191
|
+
}
|
|
192
|
+
mergeFrameTree(tabId, sessionId, node) {
|
|
193
|
+
if (!node?.frame?.id) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const target = this.attached.get(tabId);
|
|
197
|
+
if (!target) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const frameId = node.frame.id;
|
|
201
|
+
target.frames.set(frameId, this.toFrameRecord(node.frame, sessionId));
|
|
202
|
+
if (!node.frame.parentId && !sessionId) {
|
|
203
|
+
target.topFrameId = frameId;
|
|
204
|
+
}
|
|
205
|
+
if (this.globalEventHandler) {
|
|
206
|
+
this.globalEventHandler(tabId, 'Page.frameNavigated', {
|
|
207
|
+
frame: {
|
|
208
|
+
id: frameId,
|
|
209
|
+
parentId: node.frame.parentId ?? null,
|
|
210
|
+
url: node.frame.url ?? '',
|
|
211
|
+
name: node.frame.name ?? null,
|
|
212
|
+
},
|
|
213
|
+
}, { sessionId });
|
|
214
|
+
}
|
|
215
|
+
for (const child of node.childFrames ?? []) {
|
|
216
|
+
this.mergeFrameTree(tabId, sessionId, child);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
updateStateFromEvent(tabId, sessionId, method, params) {
|
|
220
|
+
const target = this.attached.get(tabId);
|
|
221
|
+
if (!target) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (method === 'Page.frameNavigated' && params) {
|
|
225
|
+
const frame = params.frame;
|
|
226
|
+
if (!frame?.id) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
target.frames.set(frame.id, this.toFrameRecord(frame, sessionId));
|
|
230
|
+
if (!frame.parentId && !sessionId && frame.url) {
|
|
231
|
+
target.url = frame.url;
|
|
232
|
+
target.topFrameId = frame.id;
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (method === 'Page.frameAttached' && params) {
|
|
237
|
+
const frame = params;
|
|
238
|
+
if (!frame.frameId) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const existing = target.frames.get(frame.frameId);
|
|
242
|
+
target.frames.set(frame.frameId, {
|
|
243
|
+
frameId: frame.frameId,
|
|
244
|
+
parentFrameId: frame.parentFrameId ?? target.topFrameId,
|
|
245
|
+
url: existing?.url ?? '',
|
|
246
|
+
title: existing?.title ?? null,
|
|
247
|
+
sessionId: existing?.sessionId ?? sessionId,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (method === 'Page.frameDetached' && params) {
|
|
252
|
+
const frame = params;
|
|
253
|
+
if (!frame.frameId) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
target.frames.delete(frame.frameId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
toFrameRecord(frame, sessionId) {
|
|
260
|
+
return {
|
|
261
|
+
frameId: frame.id,
|
|
262
|
+
parentFrameId: frame.parentId ?? null,
|
|
263
|
+
url: frame.url ?? '',
|
|
264
|
+
title: frame.name ?? null,
|
|
265
|
+
sessionId,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
handleDetach(tabId, sessionId, reason) {
|
|
269
|
+
const target = this.attached.get(tabId);
|
|
270
|
+
if (!target) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (sessionId) {
|
|
274
|
+
this.dropChildSession(tabId, sessionId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.attached.delete(tabId);
|
|
278
|
+
if (this.detachHandler) {
|
|
279
|
+
this.detachHandler(tabId, reason);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
dropChildSession(tabId, sessionId) {
|
|
283
|
+
const target = this.attached.get(tabId);
|
|
284
|
+
if (!target) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
target.childSessions.delete(sessionId);
|
|
288
|
+
for (const [frameId, frame] of target.frames.entries()) {
|
|
289
|
+
if (frame.sessionId !== sessionId) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
target.frames.delete(frameId);
|
|
293
|
+
this.globalEventHandler?.(tabId, 'Page.frameDetached', { frameId }, { sessionId });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|