@vforsh/argus 0.1.19 → 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 +257 -59
- package/dist/cli/register/extensionCommands.d.ts.map +1 -1
- package/dist/cli/register/extensionCommands.js +28 -3
- package/dist/cli/register/extensionCommands.js.map +1 -1
- 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/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/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 +3 -1
- package/dist/skill/argus/reference/EXTENSION.md +13 -8
- package/package.json +2 -2
|
@@ -0,0 +1,2004 @@
|
|
|
1
|
+
// src/background/debugger-manager.ts
|
|
2
|
+
var FRAME_TREE_SYNC_DELAY_MS = 150;
|
|
3
|
+
var DebuggerManager = class {
|
|
4
|
+
attached = /* @__PURE__ */ new Map();
|
|
5
|
+
eventHandlers = /* @__PURE__ */ new Set();
|
|
6
|
+
detachHandlers = /* @__PURE__ */ new Set();
|
|
7
|
+
frameTreeSyncTimers = /* @__PURE__ */ new Map();
|
|
8
|
+
constructor() {
|
|
9
|
+
chrome.debugger.onEvent.addListener((debuggee, method, params) => {
|
|
10
|
+
this.handleCdpEvent(debuggee, method, params);
|
|
11
|
+
});
|
|
12
|
+
chrome.debugger.onDetach.addListener((debuggee, reason) => {
|
|
13
|
+
if (debuggee.tabId) {
|
|
14
|
+
this.handleDetach(debuggee.tabId, debuggee.sessionId ?? null, reason);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
onEvent(handler) {
|
|
19
|
+
this.eventHandlers.add(handler);
|
|
20
|
+
return () => {
|
|
21
|
+
this.eventHandlers.delete(handler);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
onDetach(handler) {
|
|
25
|
+
this.detachHandlers.add(handler);
|
|
26
|
+
return () => {
|
|
27
|
+
this.detachHandlers.delete(handler);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
emitEvent(tabId, method, params, sessionId) {
|
|
31
|
+
for (const handler of this.eventHandlers) {
|
|
32
|
+
handler(tabId, method, params, { sessionId });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
emitDetach(tabId, reason) {
|
|
36
|
+
for (const handler of this.detachHandlers) {
|
|
37
|
+
handler(tabId, reason);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async attach(tabId) {
|
|
41
|
+
if (this.attached.has(tabId)) {
|
|
42
|
+
return this.attached.get(tabId);
|
|
43
|
+
}
|
|
44
|
+
const debuggee = { tabId };
|
|
45
|
+
await chrome.debugger.attach(debuggee, "1.3");
|
|
46
|
+
const tab = await chrome.tabs.get(tabId);
|
|
47
|
+
const target = {
|
|
48
|
+
tabId,
|
|
49
|
+
debuggeeId: debuggee,
|
|
50
|
+
url: tab.url ?? "",
|
|
51
|
+
title: tab.title ?? "",
|
|
52
|
+
faviconUrl: tab.favIconUrl,
|
|
53
|
+
attachedAt: Date.now(),
|
|
54
|
+
enabledDomains: /* @__PURE__ */ new Set(),
|
|
55
|
+
childSessions: /* @__PURE__ */ new Map(),
|
|
56
|
+
frames: /* @__PURE__ */ new Map(),
|
|
57
|
+
topFrameId: null
|
|
58
|
+
};
|
|
59
|
+
this.attached.set(tabId, target);
|
|
60
|
+
await this.configureAutoAttach(tabId);
|
|
61
|
+
await this.refreshFrameTree(tabId, null);
|
|
62
|
+
return target;
|
|
63
|
+
}
|
|
64
|
+
async detach(tabId) {
|
|
65
|
+
const target = this.attached.get(tabId);
|
|
66
|
+
if (!target) return;
|
|
67
|
+
try {
|
|
68
|
+
await chrome.debugger.detach(target.debuggeeId);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
this.attached.delete(tabId);
|
|
72
|
+
}
|
|
73
|
+
async sendCommand(tabId, method, params, sessionId) {
|
|
74
|
+
const target = this.getRequiredTarget(tabId);
|
|
75
|
+
const debuggee = this.toDebuggee(target, sessionId);
|
|
76
|
+
const result = await chrome.debugger.sendCommand(debuggee, method, params);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
async enableDomain(tabId, domain, sessionId) {
|
|
80
|
+
const enabledDomains = this.getRequiredEnabledDomains(tabId, sessionId);
|
|
81
|
+
if (enabledDomains.has(domain)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await this.sendCommand(tabId, `${domain}.enable`, void 0, sessionId);
|
|
85
|
+
enabledDomains.add(domain);
|
|
86
|
+
}
|
|
87
|
+
isAttached(tabId) {
|
|
88
|
+
return this.attached.has(tabId);
|
|
89
|
+
}
|
|
90
|
+
listAttached() {
|
|
91
|
+
return [...this.attached.values()];
|
|
92
|
+
}
|
|
93
|
+
getTarget(tabId) {
|
|
94
|
+
return this.attached.get(tabId);
|
|
95
|
+
}
|
|
96
|
+
getFrames(tabId) {
|
|
97
|
+
const target = this.attached.get(tabId);
|
|
98
|
+
if (!target) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return [...target.frames.values()];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read cookies from the attached tab's cookie store so extension-mode auth export can keep
|
|
105
|
+
* sibling subdomain session cookies such as `auth.example.com`.
|
|
106
|
+
*/
|
|
107
|
+
async getCookies(tabId, query = {}) {
|
|
108
|
+
this.getRequiredTarget(tabId);
|
|
109
|
+
const storeId = await this.findCookieStoreId(tabId);
|
|
110
|
+
const cookies = await chrome.cookies.getAll({
|
|
111
|
+
domain: query.domain,
|
|
112
|
+
storeId: storeId ?? void 0,
|
|
113
|
+
url: query.domain ? void 0 : query.url
|
|
114
|
+
});
|
|
115
|
+
return cookies.map((cookie) => ({
|
|
116
|
+
name: cookie.name,
|
|
117
|
+
value: cookie.value,
|
|
118
|
+
domain: cookie.domain,
|
|
119
|
+
path: cookie.path,
|
|
120
|
+
secure: cookie.secure,
|
|
121
|
+
httpOnly: cookie.httpOnly,
|
|
122
|
+
session: cookie.session,
|
|
123
|
+
expires: typeof cookie.expirationDate === "number" ? cookie.expirationDate : null,
|
|
124
|
+
sameSite: cookie.sameSite ?? null
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
async configureAutoAttach(tabId, sessionId) {
|
|
128
|
+
await this.sendCommand(
|
|
129
|
+
tabId,
|
|
130
|
+
"Target.setAutoAttach",
|
|
131
|
+
{
|
|
132
|
+
autoAttach: true,
|
|
133
|
+
waitForDebuggerOnStart: false,
|
|
134
|
+
flatten: true,
|
|
135
|
+
filter: [{ type: "iframe", exclude: false }]
|
|
136
|
+
},
|
|
137
|
+
sessionId
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
getEnabledDomains(tabId, sessionId) {
|
|
141
|
+
const target = this.attached.get(tabId);
|
|
142
|
+
if (!target) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (!sessionId) {
|
|
146
|
+
return target.enabledDomains;
|
|
147
|
+
}
|
|
148
|
+
return target.childSessions.get(sessionId)?.enabledDomains ?? null;
|
|
149
|
+
}
|
|
150
|
+
getRequiredTarget(tabId) {
|
|
151
|
+
const target = this.attached.get(tabId);
|
|
152
|
+
if (!target) {
|
|
153
|
+
throw new Error(`Tab ${tabId} is not attached`);
|
|
154
|
+
}
|
|
155
|
+
return target;
|
|
156
|
+
}
|
|
157
|
+
getRequiredEnabledDomains(tabId, sessionId) {
|
|
158
|
+
const enabledDomains = this.getEnabledDomains(tabId, sessionId);
|
|
159
|
+
if (!enabledDomains) {
|
|
160
|
+
throw new Error(`Tab ${tabId} is not attached`);
|
|
161
|
+
}
|
|
162
|
+
return enabledDomains;
|
|
163
|
+
}
|
|
164
|
+
toDebuggee(target, sessionId) {
|
|
165
|
+
if (!sessionId) {
|
|
166
|
+
return target.debuggeeId;
|
|
167
|
+
}
|
|
168
|
+
return { tabId: target.tabId, sessionId };
|
|
169
|
+
}
|
|
170
|
+
async findCookieStoreId(tabId) {
|
|
171
|
+
const stores = await chrome.cookies.getAllCookieStores();
|
|
172
|
+
const store = stores.find((candidate) => candidate.tabIds.includes(tabId));
|
|
173
|
+
return store?.id ?? null;
|
|
174
|
+
}
|
|
175
|
+
handleCdpEvent(debuggee, method, params) {
|
|
176
|
+
const tabId = debuggee.tabId;
|
|
177
|
+
if (!tabId || !this.attached.has(tabId)) return;
|
|
178
|
+
const sessionId = debuggee.sessionId ?? null;
|
|
179
|
+
if (method === "Target.attachedToTarget" && params) {
|
|
180
|
+
void this.handleAttachedToTarget(tabId, params);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (method === "Target.detachedFromTarget" && params) {
|
|
184
|
+
this.handleDetachedFromTarget(tabId, params);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.updateStateFromEvent(tabId, sessionId, method, params);
|
|
188
|
+
this.emitEvent(tabId, method, params ?? {}, sessionId);
|
|
189
|
+
this.scheduleFrameTreeSyncIfNeeded(tabId, sessionId, method, params);
|
|
190
|
+
}
|
|
191
|
+
async handleAttachedToTarget(tabId, params) {
|
|
192
|
+
const target = this.attached.get(tabId);
|
|
193
|
+
if (!target) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const record = params;
|
|
197
|
+
if (!record.sessionId) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
target.childSessions.set(record.sessionId, {
|
|
201
|
+
sessionId: record.sessionId,
|
|
202
|
+
targetId: record.targetInfo?.targetId ?? record.sessionId,
|
|
203
|
+
type: record.targetInfo?.type ?? "unknown",
|
|
204
|
+
url: record.targetInfo?.url ?? "",
|
|
205
|
+
title: record.targetInfo?.title ?? "",
|
|
206
|
+
attachedAt: Date.now(),
|
|
207
|
+
enabledDomains: /* @__PURE__ */ new Set()
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
await this.configureAutoAttach(tabId, record.sessionId);
|
|
211
|
+
await this.enableChildSessionDomains(tabId, record.sessionId);
|
|
212
|
+
await this.refreshFrameTree(tabId, record.sessionId);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.warn("[DebuggerManager] Failed to bootstrap child target:", error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async enableChildSessionDomains(tabId, sessionId) {
|
|
218
|
+
for (const domain of ["Runtime", "Page", "Network"]) {
|
|
219
|
+
await this.enableDomain(tabId, domain, sessionId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
handleDetachedFromTarget(tabId, params) {
|
|
223
|
+
const record = params;
|
|
224
|
+
if (!record.sessionId) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.clearFrameTreeSync(record.sessionId, tabId);
|
|
228
|
+
this.dropChildSession(tabId, record.sessionId);
|
|
229
|
+
}
|
|
230
|
+
async refreshFrameTree(tabId, sessionId) {
|
|
231
|
+
const result = await this.sendCommand(tabId, "Page.getFrameTree", void 0, sessionId);
|
|
232
|
+
this.replaceFrameTreeSnapshot(tabId, sessionId, result.frameTree);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Each `Page.getFrameTree` result is a full snapshot for that session, so merge the refreshed
|
|
236
|
+
* nodes and then prune frames from the same session that no longer exist after reload.
|
|
237
|
+
*/
|
|
238
|
+
replaceFrameTreeSnapshot(tabId, sessionId, node) {
|
|
239
|
+
if (!node?.frame?.id) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const target = this.attached.get(tabId);
|
|
243
|
+
if (!target) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const nextFrameIds = /* @__PURE__ */ new Set();
|
|
247
|
+
this.mergeFrameTree(tabId, sessionId, node, nextFrameIds);
|
|
248
|
+
this.pruneMissingSessionFrames(tabId, sessionId, nextFrameIds);
|
|
249
|
+
}
|
|
250
|
+
mergeFrameTree(tabId, sessionId, node, nextFrameIds) {
|
|
251
|
+
if (!node?.frame?.id) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const target = this.attached.get(tabId);
|
|
255
|
+
if (!target) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const frameId = node.frame.id;
|
|
259
|
+
nextFrameIds.add(frameId);
|
|
260
|
+
target.frames.set(frameId, this.toFrameRecord(node.frame, sessionId));
|
|
261
|
+
if (!node.frame.parentId && !sessionId) {
|
|
262
|
+
target.topFrameId = frameId;
|
|
263
|
+
}
|
|
264
|
+
this.emitEvent(
|
|
265
|
+
tabId,
|
|
266
|
+
"Page.frameNavigated",
|
|
267
|
+
{
|
|
268
|
+
frame: {
|
|
269
|
+
id: frameId,
|
|
270
|
+
parentId: node.frame.parentId ?? null,
|
|
271
|
+
url: node.frame.url ?? "",
|
|
272
|
+
name: node.frame.name ?? null
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
sessionId
|
|
276
|
+
);
|
|
277
|
+
for (const child of node.childFrames ?? []) {
|
|
278
|
+
this.mergeFrameTree(tabId, sessionId, child, nextFrameIds);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
pruneMissingSessionFrames(tabId, sessionId, nextFrameIds) {
|
|
282
|
+
const target = this.attached.get(tabId);
|
|
283
|
+
if (!target) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const [frameId, frame] of target.frames.entries()) {
|
|
287
|
+
if (frame.sessionId !== sessionId || nextFrameIds.has(frameId)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
target.frames.delete(frameId);
|
|
291
|
+
this.emitEvent(tabId, "Page.frameDetached", { frameId }, sessionId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
updateStateFromEvent(tabId, sessionId, method, params) {
|
|
295
|
+
const target = this.attached.get(tabId);
|
|
296
|
+
if (!target) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (method === "Page.frameNavigated" && params) {
|
|
300
|
+
const frame = params.frame;
|
|
301
|
+
if (!frame?.id) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
target.frames.set(frame.id, this.toFrameRecord(frame, sessionId));
|
|
305
|
+
if (!frame.parentId && !sessionId && frame.url) {
|
|
306
|
+
target.url = frame.url;
|
|
307
|
+
target.topFrameId = frame.id;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (method === "Page.frameAttached" && params) {
|
|
312
|
+
const frame = params;
|
|
313
|
+
if (!frame.frameId) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const existing = target.frames.get(frame.frameId);
|
|
317
|
+
target.frames.set(frame.frameId, {
|
|
318
|
+
frameId: frame.frameId,
|
|
319
|
+
parentFrameId: frame.parentFrameId ?? target.topFrameId,
|
|
320
|
+
url: existing?.url ?? "",
|
|
321
|
+
title: existing?.title ?? null,
|
|
322
|
+
sessionId: existing?.sessionId ?? sessionId
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (method === "Page.frameDetached" && params) {
|
|
327
|
+
const frame = params;
|
|
328
|
+
if (!frame.frameId) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
target.frames.delete(frame.frameId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Chrome's frame attach/detach events during reload are not always complete. When the root frame
|
|
336
|
+
* for a session navigates, refresh that session's full frame tree shortly afterward and treat it
|
|
337
|
+
* as authoritative so stale iframe records do not linger in extension state.
|
|
338
|
+
*/
|
|
339
|
+
scheduleFrameTreeSyncIfNeeded(tabId, sessionId, method, params) {
|
|
340
|
+
if (!shouldSyncFrameTreeSnapshot(method, params)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const key = getFrameTreeSyncKey(tabId, sessionId);
|
|
344
|
+
if (this.frameTreeSyncTimers.has(key)) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const timer = setTimeout(() => {
|
|
348
|
+
this.frameTreeSyncTimers.delete(key);
|
|
349
|
+
void this.refreshFrameTree(tabId, sessionId).catch((error) => {
|
|
350
|
+
console.warn("[DebuggerManager] Failed to refresh frame tree after navigation:", error);
|
|
351
|
+
});
|
|
352
|
+
}, FRAME_TREE_SYNC_DELAY_MS);
|
|
353
|
+
this.frameTreeSyncTimers.set(key, timer);
|
|
354
|
+
}
|
|
355
|
+
clearFrameTreeSync(sessionId, tabId) {
|
|
356
|
+
const key = getFrameTreeSyncKey(tabId, sessionId);
|
|
357
|
+
const timer = this.frameTreeSyncTimers.get(key);
|
|
358
|
+
if (!timer) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
this.frameTreeSyncTimers.delete(key);
|
|
363
|
+
}
|
|
364
|
+
clearAllFrameTreeSync(tabId) {
|
|
365
|
+
for (const [key, timer] of this.frameTreeSyncTimers.entries()) {
|
|
366
|
+
if (!key.startsWith(`${tabId}:`)) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
this.frameTreeSyncTimers.delete(key);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
toFrameRecord(frame, sessionId) {
|
|
374
|
+
return {
|
|
375
|
+
frameId: frame.id,
|
|
376
|
+
parentFrameId: frame.parentId ?? null,
|
|
377
|
+
url: frame.url ?? "",
|
|
378
|
+
title: frame.name ?? null,
|
|
379
|
+
sessionId
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
handleDetach(tabId, sessionId, reason) {
|
|
383
|
+
const target = this.attached.get(tabId);
|
|
384
|
+
if (!target) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.clearFrameTreeSync(sessionId, tabId);
|
|
388
|
+
if (sessionId) {
|
|
389
|
+
this.dropChildSession(tabId, sessionId);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
this.clearAllFrameTreeSync(tabId);
|
|
393
|
+
this.attached.delete(tabId);
|
|
394
|
+
this.emitDetach(tabId, reason);
|
|
395
|
+
}
|
|
396
|
+
dropChildSession(tabId, sessionId) {
|
|
397
|
+
const target = this.attached.get(tabId);
|
|
398
|
+
if (!target) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
target.childSessions.delete(sessionId);
|
|
402
|
+
for (const [frameId, frame] of target.frames.entries()) {
|
|
403
|
+
if (frame.sessionId !== sessionId) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
target.frames.delete(frameId);
|
|
407
|
+
this.emitEvent(tabId, "Page.frameDetached", { frameId }, sessionId);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
function shouldSyncFrameTreeSnapshot(method, params) {
|
|
412
|
+
if (method !== "Page.frameNavigated" || !params) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
const frame = params.frame;
|
|
416
|
+
return frame?.parentId == null;
|
|
417
|
+
}
|
|
418
|
+
function getFrameTreeSyncKey(tabId, sessionId) {
|
|
419
|
+
return `${tabId}:${sessionId ?? "root"}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/background/bridge-client.ts
|
|
423
|
+
var BridgeClient = class {
|
|
424
|
+
port = null;
|
|
425
|
+
hostName;
|
|
426
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
427
|
+
connectHandler = null;
|
|
428
|
+
disconnectHandler = null;
|
|
429
|
+
reconnectAttempts = 0;
|
|
430
|
+
maxReconnectAttempts = 5;
|
|
431
|
+
reconnectDelay = 1e3;
|
|
432
|
+
autoReconnect;
|
|
433
|
+
reconnectEnabled = true;
|
|
434
|
+
constructor(hostName = "com.vforsh.argus.bridge", options = {}) {
|
|
435
|
+
this.hostName = hostName;
|
|
436
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Set handler for incoming messages from the host.
|
|
440
|
+
*/
|
|
441
|
+
onMessage(handler) {
|
|
442
|
+
this.messageHandlers.add(handler);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Set handler for successful connection.
|
|
446
|
+
*/
|
|
447
|
+
onConnect(handler) {
|
|
448
|
+
this.connectHandler = handler;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Set handler for disconnection.
|
|
452
|
+
*/
|
|
453
|
+
onDisconnect(handler) {
|
|
454
|
+
this.disconnectHandler = handler;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Connect to the Native Messaging host.
|
|
458
|
+
*/
|
|
459
|
+
connect() {
|
|
460
|
+
if (this.port) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
this.reconnectEnabled = true;
|
|
464
|
+
try {
|
|
465
|
+
this.port = chrome.runtime.connectNative(this.hostName);
|
|
466
|
+
this.port.onMessage.addListener((message) => {
|
|
467
|
+
this.reconnectAttempts = 0;
|
|
468
|
+
for (const handler of this.messageHandlers) {
|
|
469
|
+
handler(message);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
this.port.onDisconnect.addListener(() => {
|
|
473
|
+
const error = chrome.runtime.lastError;
|
|
474
|
+
console.log("[BridgeClient] Disconnected:", error?.message ?? "unknown reason");
|
|
475
|
+
this.port = null;
|
|
476
|
+
if (this.disconnectHandler) {
|
|
477
|
+
this.disconnectHandler();
|
|
478
|
+
}
|
|
479
|
+
this.scheduleReconnect();
|
|
480
|
+
});
|
|
481
|
+
console.log("[BridgeClient] Connected to", this.hostName);
|
|
482
|
+
this.reconnectAttempts = 0;
|
|
483
|
+
if (this.connectHandler) {
|
|
484
|
+
this.connectHandler();
|
|
485
|
+
}
|
|
486
|
+
return true;
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error("[BridgeClient] Failed to connect:", err);
|
|
489
|
+
this.scheduleReconnect();
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Disconnect from the Native Messaging host.
|
|
495
|
+
*/
|
|
496
|
+
disconnect() {
|
|
497
|
+
this.reconnectEnabled = false;
|
|
498
|
+
if (this.port) {
|
|
499
|
+
this.port.disconnect();
|
|
500
|
+
this.port = null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Send a message to the Native Messaging host.
|
|
505
|
+
*/
|
|
506
|
+
send(message) {
|
|
507
|
+
if (!this.port) {
|
|
508
|
+
console.warn("[BridgeClient] Cannot send, not connected");
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
this.port.postMessage(message);
|
|
513
|
+
return true;
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.error("[BridgeClient] Send failed:", err);
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if connected to the host.
|
|
521
|
+
*/
|
|
522
|
+
isConnected() {
|
|
523
|
+
return this.port !== null;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
527
|
+
*/
|
|
528
|
+
scheduleReconnect() {
|
|
529
|
+
if (!this.autoReconnect || !this.reconnectEnabled) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
533
|
+
console.log("[BridgeClient] Max reconnection attempts reached");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
|
|
537
|
+
this.reconnectAttempts++;
|
|
538
|
+
console.log(`[BridgeClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
539
|
+
setTimeout(() => {
|
|
540
|
+
this.connect();
|
|
541
|
+
}, delay);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// src/background/tab-list.ts
|
|
546
|
+
var listBrowserTabs = async (debuggerManager2, filter, options = {}) => {
|
|
547
|
+
const chromeTabs = await chrome.tabs.query({});
|
|
548
|
+
const attachedTargets = debuggerManager2.listAttached();
|
|
549
|
+
const attachedTabIds = new Set(attachedTargets.map((target) => target.tabId));
|
|
550
|
+
let tabs = chromeTabs.filter((tab) => tab.id !== void 0 && tab.url !== void 0).filter((tab) => !tab.url.startsWith("chrome://") && !tab.url.startsWith("chrome-extension://")).map((tab) => ({
|
|
551
|
+
tabId: tab.id,
|
|
552
|
+
url: tab.url,
|
|
553
|
+
title: tab.title ?? "",
|
|
554
|
+
faviconUrl: tab.favIconUrl,
|
|
555
|
+
attached: attachedTabIds.has(tab.id),
|
|
556
|
+
watcherId: options.getWatcherIdForTab?.(tab.id) ?? void 0
|
|
557
|
+
}));
|
|
558
|
+
if (filter?.url) {
|
|
559
|
+
const urlFilter = filter.url.toLowerCase();
|
|
560
|
+
tabs = tabs.filter((tab) => tab.url.toLowerCase().includes(urlFilter));
|
|
561
|
+
}
|
|
562
|
+
if (filter?.title) {
|
|
563
|
+
const titleFilter = filter.title.toLowerCase();
|
|
564
|
+
tabs = tabs.filter((tab) => tab.title.toLowerCase().includes(titleFilter));
|
|
565
|
+
}
|
|
566
|
+
tabs.sort((left, right) => Number(right.attached) - Number(left.attached));
|
|
567
|
+
return tabs;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/background/cdp-proxy.ts
|
|
571
|
+
var CdpProxy = class {
|
|
572
|
+
debuggerManager;
|
|
573
|
+
bridgeClient;
|
|
574
|
+
removeDebuggerEventForwarding = null;
|
|
575
|
+
removeDebuggerDetachForwarding = null;
|
|
576
|
+
constructor(debuggerManager2, bridgeClient) {
|
|
577
|
+
this.debuggerManager = debuggerManager2;
|
|
578
|
+
this.bridgeClient = bridgeClient;
|
|
579
|
+
this.setupEventForwarding();
|
|
580
|
+
this.setupMessageHandling();
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Forward CDP events from debugger to bridge.
|
|
584
|
+
*/
|
|
585
|
+
setupEventForwarding() {
|
|
586
|
+
this.removeDebuggerEventForwarding = this.debuggerManager.onEvent((tabId, method, params, meta) => {
|
|
587
|
+
this.bridgeClient.send({
|
|
588
|
+
type: "cdp_event",
|
|
589
|
+
tabId,
|
|
590
|
+
method,
|
|
591
|
+
params,
|
|
592
|
+
sessionId: meta?.sessionId ?? void 0
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
this.removeDebuggerDetachForwarding = this.debuggerManager.onDetach((tabId, reason) => {
|
|
596
|
+
this.bridgeClient.send({
|
|
597
|
+
type: "tab_detached",
|
|
598
|
+
tabId,
|
|
599
|
+
reason
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
dispose() {
|
|
604
|
+
this.removeDebuggerEventForwarding?.();
|
|
605
|
+
this.removeDebuggerEventForwarding = null;
|
|
606
|
+
this.removeDebuggerDetachForwarding?.();
|
|
607
|
+
this.removeDebuggerDetachForwarding = null;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Handle messages from the bridge.
|
|
611
|
+
*/
|
|
612
|
+
setupMessageHandling() {
|
|
613
|
+
this.bridgeClient.onMessage((message) => {
|
|
614
|
+
this.handleMessage(message);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Process a message from the bridge.
|
|
619
|
+
*/
|
|
620
|
+
async handleMessage(message) {
|
|
621
|
+
switch (message.type) {
|
|
622
|
+
case "attach_tab":
|
|
623
|
+
await this.handleAttachTab(message);
|
|
624
|
+
break;
|
|
625
|
+
case "detach_tab":
|
|
626
|
+
await this.handleDetachTab(message);
|
|
627
|
+
break;
|
|
628
|
+
case "cdp_command":
|
|
629
|
+
await this.handleCdpCommand(message);
|
|
630
|
+
break;
|
|
631
|
+
case "cookie_query":
|
|
632
|
+
await this.handleCookieQuery(message);
|
|
633
|
+
break;
|
|
634
|
+
case "enable_domain":
|
|
635
|
+
await this.handleEnableDomain(message);
|
|
636
|
+
break;
|
|
637
|
+
case "host_info":
|
|
638
|
+
case "host_ready":
|
|
639
|
+
case "target_info":
|
|
640
|
+
break;
|
|
641
|
+
default:
|
|
642
|
+
console.warn("[CdpProxy] Unknown message type:", message.type);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Attach to a tab.
|
|
647
|
+
*/
|
|
648
|
+
async handleAttachTab(message) {
|
|
649
|
+
try {
|
|
650
|
+
const target = await this.debuggerManager.attach(message.tabId);
|
|
651
|
+
this.bridgeClient.send({
|
|
652
|
+
type: "tab_attached",
|
|
653
|
+
tabId: target.tabId,
|
|
654
|
+
url: target.url,
|
|
655
|
+
title: target.title,
|
|
656
|
+
faviconUrl: target.faviconUrl,
|
|
657
|
+
topFrameId: target.topFrameId,
|
|
658
|
+
frames: this.debuggerManager.getFrames(target.tabId)
|
|
659
|
+
});
|
|
660
|
+
} catch (err) {
|
|
661
|
+
console.error("[CdpProxy] Failed to attach:", err);
|
|
662
|
+
this.bridgeClient.send({
|
|
663
|
+
type: "tab_detached",
|
|
664
|
+
tabId: message.tabId,
|
|
665
|
+
reason: err instanceof Error ? err.message : "attach_failed"
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Detach from a tab.
|
|
671
|
+
*/
|
|
672
|
+
async handleDetachTab(message) {
|
|
673
|
+
await this.debuggerManager.detach(message.tabId);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Execute a CDP command.
|
|
677
|
+
*/
|
|
678
|
+
async handleCdpCommand(message) {
|
|
679
|
+
try {
|
|
680
|
+
const result = await this.debuggerManager.sendCommand(message.tabId, message.method, message.params, message.sessionId);
|
|
681
|
+
this.bridgeClient.send({
|
|
682
|
+
type: "cdp_response",
|
|
683
|
+
requestId: message.requestId,
|
|
684
|
+
result
|
|
685
|
+
});
|
|
686
|
+
} catch (err) {
|
|
687
|
+
this.bridgeClient.send({
|
|
688
|
+
type: "cdp_response",
|
|
689
|
+
requestId: message.requestId,
|
|
690
|
+
error: {
|
|
691
|
+
message: err instanceof Error ? err.message : "Unknown error"
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Query browser cookies from the attached tab's cookie store.
|
|
698
|
+
*/
|
|
699
|
+
async handleCookieQuery(message) {
|
|
700
|
+
try {
|
|
701
|
+
const cookies = await this.debuggerManager.getCookies(message.tabId, {
|
|
702
|
+
domain: message.domain,
|
|
703
|
+
url: message.url
|
|
704
|
+
});
|
|
705
|
+
this.bridgeClient.send({
|
|
706
|
+
type: "cookie_query_response",
|
|
707
|
+
requestId: message.requestId,
|
|
708
|
+
cookies
|
|
709
|
+
});
|
|
710
|
+
} catch (err) {
|
|
711
|
+
this.bridgeClient.send({
|
|
712
|
+
type: "cookie_query_response",
|
|
713
|
+
requestId: message.requestId,
|
|
714
|
+
error: {
|
|
715
|
+
message: err instanceof Error ? err.message : "Unknown error"
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Enable a CDP domain for a tab.
|
|
722
|
+
*/
|
|
723
|
+
async handleEnableDomain(message) {
|
|
724
|
+
try {
|
|
725
|
+
await this.debuggerManager.enableDomain(message.tabId, message.domain);
|
|
726
|
+
} catch (err) {
|
|
727
|
+
console.error("[CdpProxy] Failed to enable domain:", err);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Manually attach to a tab (called from popup).
|
|
732
|
+
*/
|
|
733
|
+
async attachTab(tabId) {
|
|
734
|
+
await this.handleAttachTab({ type: "attach_tab", tabId });
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Manually detach from a tab (called from popup).
|
|
738
|
+
*/
|
|
739
|
+
async detachTab(tabId) {
|
|
740
|
+
this.bridgeClient.send({
|
|
741
|
+
type: "tab_detached",
|
|
742
|
+
tabId,
|
|
743
|
+
reason: "user_requested"
|
|
744
|
+
});
|
|
745
|
+
await this.debuggerManager.detach(tabId);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Get list of tabs for popup UI.
|
|
749
|
+
*/
|
|
750
|
+
async getTabsForPopup() {
|
|
751
|
+
return await listBrowserTabs(this.debuggerManager);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/background/native-hosts.ts
|
|
756
|
+
var BRIDGE_HOST_NAME = "com.vforsh.argus.bridge";
|
|
757
|
+
var CONTROL_HOST_NAME = "com.vforsh.argus.control";
|
|
758
|
+
|
|
759
|
+
// src/background/tab-bridge-session.ts
|
|
760
|
+
var TabBridgeSession = class {
|
|
761
|
+
tabId;
|
|
762
|
+
bridgeClient;
|
|
763
|
+
cdpProxy;
|
|
764
|
+
events;
|
|
765
|
+
watcherInfo = null;
|
|
766
|
+
targetInfo = null;
|
|
767
|
+
desiredWatcherId;
|
|
768
|
+
lastMessageAt = null;
|
|
769
|
+
ready = false;
|
|
770
|
+
readyWaiters = [];
|
|
771
|
+
watcherInfoWaiters = [];
|
|
772
|
+
disposed = false;
|
|
773
|
+
constructor(tabId, debuggerManager2, events = {}, options = {}) {
|
|
774
|
+
this.tabId = tabId;
|
|
775
|
+
this.events = events;
|
|
776
|
+
this.desiredWatcherId = options.watcherId;
|
|
777
|
+
this.bridgeClient = new BridgeClient(BRIDGE_HOST_NAME, { autoReconnect: false });
|
|
778
|
+
this.cdpProxy = new CdpProxy(debuggerManager2, this.bridgeClient);
|
|
779
|
+
this.bridgeClient.onMessage((message) => {
|
|
780
|
+
this.handleMessage(message);
|
|
781
|
+
});
|
|
782
|
+
this.bridgeClient.onDisconnect(() => {
|
|
783
|
+
if (this.disposed) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
this.events.onDisconnect?.();
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async connectAndAttach() {
|
|
790
|
+
this.assertOpen();
|
|
791
|
+
const connected = this.bridgeClient.connect();
|
|
792
|
+
if (!connected) {
|
|
793
|
+
throw new Error(`Failed to connect native host for tab ${this.tabId}`);
|
|
794
|
+
}
|
|
795
|
+
const sent = this.bridgeClient.send({
|
|
796
|
+
type: "init_tab_watcher",
|
|
797
|
+
watcherId: this.desiredWatcherId
|
|
798
|
+
});
|
|
799
|
+
if (!sent) {
|
|
800
|
+
throw new Error(`Failed to initialize watcher for tab ${this.tabId}`);
|
|
801
|
+
}
|
|
802
|
+
await this.waitUntilReady();
|
|
803
|
+
await this.waitForWatcherInfo();
|
|
804
|
+
await this.cdpProxy.attachTab(this.tabId);
|
|
805
|
+
}
|
|
806
|
+
async waitUntilReady(timeoutMs = 3e3) {
|
|
807
|
+
this.assertOpen();
|
|
808
|
+
if (this.ready) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
await new Promise((resolve, reject) => {
|
|
812
|
+
const timer = setTimeout(() => {
|
|
813
|
+
this.readyWaiters = this.readyWaiters.filter((waiter) => waiter !== onReady);
|
|
814
|
+
reject(new Error(`Native host session for tab ${this.tabId} did not become ready`));
|
|
815
|
+
}, timeoutMs);
|
|
816
|
+
const onReady = () => {
|
|
817
|
+
clearTimeout(timer);
|
|
818
|
+
resolve();
|
|
819
|
+
};
|
|
820
|
+
this.readyWaiters.push(onReady);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async detach() {
|
|
824
|
+
if (this.disposed) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
await this.cdpProxy.detachTab(this.tabId);
|
|
829
|
+
} finally {
|
|
830
|
+
this.dispose();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
selectTarget(frameId) {
|
|
834
|
+
this.assertOpen();
|
|
835
|
+
const sent = this.bridgeClient.send({
|
|
836
|
+
type: "target_selected",
|
|
837
|
+
tabId: this.tabId,
|
|
838
|
+
frameId
|
|
839
|
+
});
|
|
840
|
+
if (!sent) {
|
|
841
|
+
throw new Error(`Failed to select target for tab ${this.tabId}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
dispose() {
|
|
845
|
+
if (this.disposed) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
this.disposed = true;
|
|
849
|
+
this.cdpProxy.dispose();
|
|
850
|
+
this.bridgeClient.disconnect();
|
|
851
|
+
}
|
|
852
|
+
isConnected() {
|
|
853
|
+
return this.bridgeClient.isConnected();
|
|
854
|
+
}
|
|
855
|
+
getWatcherInfo() {
|
|
856
|
+
return this.watcherInfo;
|
|
857
|
+
}
|
|
858
|
+
getTargetInfo() {
|
|
859
|
+
return this.targetInfo;
|
|
860
|
+
}
|
|
861
|
+
getLastMessageAt() {
|
|
862
|
+
return this.lastMessageAt;
|
|
863
|
+
}
|
|
864
|
+
async waitForWatcherInfo(timeoutMs = 3e3) {
|
|
865
|
+
this.assertOpen();
|
|
866
|
+
if (this.watcherInfo) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
await new Promise((resolve, reject) => {
|
|
870
|
+
const timer = setTimeout(() => {
|
|
871
|
+
this.watcherInfoWaiters = this.watcherInfoWaiters.filter((waiter) => waiter !== onWatcherInfo);
|
|
872
|
+
reject(new Error(`Native host session for tab ${this.tabId} did not report watcher info`));
|
|
873
|
+
}, timeoutMs);
|
|
874
|
+
const onWatcherInfo = () => {
|
|
875
|
+
clearTimeout(timer);
|
|
876
|
+
resolve();
|
|
877
|
+
};
|
|
878
|
+
this.watcherInfoWaiters.push(onWatcherInfo);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
handleMessage(message) {
|
|
882
|
+
this.lastMessageAt = Date.now();
|
|
883
|
+
switch (message.type) {
|
|
884
|
+
case "host_info":
|
|
885
|
+
this.watcherInfo = {
|
|
886
|
+
watcherId: message.watcherId,
|
|
887
|
+
watcherHost: message.watcherHost,
|
|
888
|
+
watcherPort: message.watcherPort,
|
|
889
|
+
pid: message.pid
|
|
890
|
+
};
|
|
891
|
+
this.events.onWatcherInfo?.(this.watcherInfo);
|
|
892
|
+
this.resolveWatcherInfoWaiters();
|
|
893
|
+
return;
|
|
894
|
+
case "host_ready":
|
|
895
|
+
this.resolveReadyWaiters();
|
|
896
|
+
return;
|
|
897
|
+
case "target_info":
|
|
898
|
+
this.targetInfo = {
|
|
899
|
+
targetId: message.targetId,
|
|
900
|
+
title: message.title,
|
|
901
|
+
url: message.url,
|
|
902
|
+
attachedAt: message.attachedAt,
|
|
903
|
+
targetReady: message.targetReady ?? null
|
|
904
|
+
};
|
|
905
|
+
this.events.onTargetInfo?.(this.targetInfo);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
resolveReadyWaiters() {
|
|
909
|
+
this.ready = true;
|
|
910
|
+
const waiters = this.readyWaiters;
|
|
911
|
+
this.readyWaiters = [];
|
|
912
|
+
for (const waiter of waiters) {
|
|
913
|
+
waiter();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
resolveWatcherInfoWaiters() {
|
|
917
|
+
const waiters = this.watcherInfoWaiters;
|
|
918
|
+
this.watcherInfoWaiters = [];
|
|
919
|
+
for (const waiter of waiters) {
|
|
920
|
+
waiter();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
assertOpen() {
|
|
924
|
+
if (this.disposed) {
|
|
925
|
+
throw new Error(`Native host session for tab ${this.tabId} is already closed`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// src/background/control-bridge-session.ts
|
|
931
|
+
var ControlBridgeSession = class {
|
|
932
|
+
bridgeClient;
|
|
933
|
+
debuggerManager;
|
|
934
|
+
events;
|
|
935
|
+
watcherInfo = null;
|
|
936
|
+
lastMessageAt = null;
|
|
937
|
+
disposed = false;
|
|
938
|
+
constructor(debuggerManager2, events = {}) {
|
|
939
|
+
this.debuggerManager = debuggerManager2;
|
|
940
|
+
this.events = events;
|
|
941
|
+
this.bridgeClient = new BridgeClient(CONTROL_HOST_NAME, { autoReconnect: true });
|
|
942
|
+
this.bridgeClient.onMessage((message) => {
|
|
943
|
+
void this.handleMessage(message);
|
|
944
|
+
});
|
|
945
|
+
this.bridgeClient.onDisconnect(() => {
|
|
946
|
+
if (this.disposed) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
this.events.onDisconnect?.();
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
connect() {
|
|
953
|
+
this.assertOpen();
|
|
954
|
+
return this.bridgeClient.connect();
|
|
955
|
+
}
|
|
956
|
+
dispose() {
|
|
957
|
+
if (this.disposed) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
this.disposed = true;
|
|
961
|
+
this.bridgeClient.disconnect();
|
|
962
|
+
}
|
|
963
|
+
isConnected() {
|
|
964
|
+
return this.bridgeClient.isConnected();
|
|
965
|
+
}
|
|
966
|
+
getWatcherInfo() {
|
|
967
|
+
return this.watcherInfo;
|
|
968
|
+
}
|
|
969
|
+
getLastMessageAt() {
|
|
970
|
+
return this.lastMessageAt;
|
|
971
|
+
}
|
|
972
|
+
async handleMessage(message) {
|
|
973
|
+
this.lastMessageAt = Date.now();
|
|
974
|
+
switch (message.type) {
|
|
975
|
+
case "host_info":
|
|
976
|
+
this.watcherInfo = {
|
|
977
|
+
watcherId: message.watcherId,
|
|
978
|
+
watcherHost: message.watcherHost,
|
|
979
|
+
watcherPort: message.watcherPort,
|
|
980
|
+
pid: message.pid
|
|
981
|
+
};
|
|
982
|
+
this.events.onWatcherInfo?.(this.watcherInfo);
|
|
983
|
+
return;
|
|
984
|
+
case "attach_tab_watcher":
|
|
985
|
+
await this.handleTabAction(message.requestId, () => this.events.onAttachTabWatcher?.(message.tabId, { watcherId: message.watcherId }));
|
|
986
|
+
return;
|
|
987
|
+
case "detach_tab_watcher":
|
|
988
|
+
await this.handleTabAction(message.requestId, () => this.events.onDetachTabWatcher?.(message.tabId));
|
|
989
|
+
return;
|
|
990
|
+
case "list_tabs":
|
|
991
|
+
await this.handleListTabs(message.requestId, message.filter);
|
|
992
|
+
return;
|
|
993
|
+
case "control_status":
|
|
994
|
+
this.handleControlStatus(message.requestId);
|
|
995
|
+
return;
|
|
996
|
+
case "host_ready":
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async handleTabAction(requestId, action) {
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await action() ?? { ok: false, error: "Extension control action is not available" };
|
|
1003
|
+
if (!result.ok) {
|
|
1004
|
+
this.bridgeClient.send({ type: "tab_action_response", requestId, ok: false, error: { message: result.error } });
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
this.bridgeClient.send({ type: "tab_action_response", requestId, ok: true, tab: result.tab, watcherId: result.watcherId });
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
this.bridgeClient.send({
|
|
1010
|
+
type: "tab_action_response",
|
|
1011
|
+
requestId,
|
|
1012
|
+
ok: false,
|
|
1013
|
+
error: { message: error instanceof Error ? error.message : String(error) }
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async handleListTabs(requestId, filter) {
|
|
1018
|
+
const tabs = await listBrowserTabs(this.debuggerManager, filter, {
|
|
1019
|
+
getWatcherIdForTab: this.events.getWatcherIdForTab
|
|
1020
|
+
});
|
|
1021
|
+
this.bridgeClient.send({
|
|
1022
|
+
type: "list_tabs_response",
|
|
1023
|
+
requestId,
|
|
1024
|
+
tabs
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
handleControlStatus(requestId) {
|
|
1028
|
+
this.bridgeClient.send({
|
|
1029
|
+
type: "control_status_response",
|
|
1030
|
+
requestId,
|
|
1031
|
+
diagnostics: this.events.getDiagnostics?.() ?? this.buildFallbackDiagnostics()
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
buildFallbackDiagnostics() {
|
|
1035
|
+
return {
|
|
1036
|
+
extensionId: null,
|
|
1037
|
+
extensionVersion: null,
|
|
1038
|
+
control: {
|
|
1039
|
+
connected: this.isConnected(),
|
|
1040
|
+
watcherId: this.watcherInfo?.watcherId ?? null,
|
|
1041
|
+
watcherHost: this.watcherInfo?.watcherHost ?? null,
|
|
1042
|
+
watcherPort: this.watcherInfo?.watcherPort ?? null,
|
|
1043
|
+
pid: this.watcherInfo?.pid ?? null,
|
|
1044
|
+
lastMessageAt: this.lastMessageAt
|
|
1045
|
+
},
|
|
1046
|
+
tabWatchers: [],
|
|
1047
|
+
recentEvents: []
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
assertOpen() {
|
|
1051
|
+
if (this.disposed) {
|
|
1052
|
+
throw new Error("Native host control session is already closed");
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// src/background/target-selection-history.ts
|
|
1058
|
+
var DEFAULT_STORAGE_KEY = "targetSelectionHistory";
|
|
1059
|
+
var DEFAULT_MAX_ENTRIES = 20;
|
|
1060
|
+
var normalizeSelectionPageKey = (pageUrl) => {
|
|
1061
|
+
try {
|
|
1062
|
+
const parsed = new URL(pageUrl);
|
|
1063
|
+
return `${parsed.origin}${parsed.pathname || "/"}`;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return pageUrl.split("#", 1)[0] || pageUrl;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
var matchRememberedIframeTarget = (entry, targets) => {
|
|
1069
|
+
if (entry.target.type !== "iframe") {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
const rememberedIframe = entry.target;
|
|
1073
|
+
const iframeTargets = getIframeTargets(targets);
|
|
1074
|
+
if (rememberedIframe.url) {
|
|
1075
|
+
return pickSingleTargetMatch(iframeTargets, (target) => target.url === rememberedIframe.url);
|
|
1076
|
+
}
|
|
1077
|
+
if (!rememberedIframe.title) {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
return pickSingleTargetMatch(iframeTargets, (target) => normalizeTargetTitle(target.title) === rememberedIframe.title);
|
|
1081
|
+
};
|
|
1082
|
+
var TargetSelectionHistoryStore = class {
|
|
1083
|
+
persistence;
|
|
1084
|
+
maxEntries;
|
|
1085
|
+
entries = [];
|
|
1086
|
+
loadPromise = null;
|
|
1087
|
+
saveChain = Promise.resolve();
|
|
1088
|
+
constructor(persistence = createChromeStoragePersistence(), options = {}) {
|
|
1089
|
+
this.persistence = persistence;
|
|
1090
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
1091
|
+
}
|
|
1092
|
+
async getByPageUrl(pageUrl) {
|
|
1093
|
+
await this.ensureLoaded();
|
|
1094
|
+
const pageKey = normalizeSelectionPageKey(pageUrl);
|
|
1095
|
+
return this.entries.find((entry) => entry.pageKey === pageKey) ?? null;
|
|
1096
|
+
}
|
|
1097
|
+
async remember(pageUrl, target) {
|
|
1098
|
+
await this.ensureLoaded();
|
|
1099
|
+
const entry = buildRememberedSelection(pageUrl, target);
|
|
1100
|
+
this.entries = [entry, ...this.entries.filter((candidate) => candidate.pageKey !== entry.pageKey)].slice(0, this.maxEntries);
|
|
1101
|
+
await this.persist();
|
|
1102
|
+
return entry;
|
|
1103
|
+
}
|
|
1104
|
+
async ensureLoaded() {
|
|
1105
|
+
if (!this.loadPromise) {
|
|
1106
|
+
this.loadPromise = this.persistence.load().then((entries) => {
|
|
1107
|
+
this.entries = sanitizeRememberedSelections(entries).slice(0, this.maxEntries);
|
|
1108
|
+
}).catch((error) => {
|
|
1109
|
+
console.error("[TargetSelectionHistoryStore] Failed to load history:", error);
|
|
1110
|
+
this.entries = [];
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
await this.loadPromise;
|
|
1114
|
+
}
|
|
1115
|
+
async persist() {
|
|
1116
|
+
this.saveChain = this.saveChain.catch(() => void 0).then(() => this.persistence.save(this.entries)).catch((error) => {
|
|
1117
|
+
console.error("[TargetSelectionHistoryStore] Failed to save history:", error);
|
|
1118
|
+
});
|
|
1119
|
+
await this.saveChain;
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
var createChromeStoragePersistence = (storageArea = chrome.storage.local, storageKey = DEFAULT_STORAGE_KEY) => ({
|
|
1123
|
+
load: async () => {
|
|
1124
|
+
const stored = await readStorageValue(storageArea, storageKey);
|
|
1125
|
+
return sanitizeRememberedSelections(Array.isArray(stored) ? stored : []);
|
|
1126
|
+
},
|
|
1127
|
+
save: async (entries) => {
|
|
1128
|
+
await writeStorageValue(storageArea, { [storageKey]: entries });
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
var buildRememberedSelection = (pageUrl, target) => {
|
|
1132
|
+
const base = {
|
|
1133
|
+
pageKey: normalizeSelectionPageKey(pageUrl),
|
|
1134
|
+
pageUrl,
|
|
1135
|
+
updatedAt: Date.now()
|
|
1136
|
+
};
|
|
1137
|
+
if (target.type === "page") {
|
|
1138
|
+
return {
|
|
1139
|
+
...base,
|
|
1140
|
+
target: { type: "page" }
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
...base,
|
|
1145
|
+
target: {
|
|
1146
|
+
type: "iframe",
|
|
1147
|
+
url: normalizeTargetUrl(target.url),
|
|
1148
|
+
title: normalizeTargetTitle(target.title)
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
};
|
|
1152
|
+
var pickSingleTargetMatch = (targets, predicate) => {
|
|
1153
|
+
const matches = targets.filter(predicate);
|
|
1154
|
+
return matches.length === 1 ? matches[0] : null;
|
|
1155
|
+
};
|
|
1156
|
+
var getIframeTargets = (targets) => {
|
|
1157
|
+
return targets.filter((target) => {
|
|
1158
|
+
return target.type === "iframe" && typeof target.frameId === "string" && target.frameId.length > 0;
|
|
1159
|
+
});
|
|
1160
|
+
};
|
|
1161
|
+
var sanitizeRememberedSelections = (entries) => {
|
|
1162
|
+
return entries.map(sanitizeRememberedSelection).filter((entry) => entry !== null).sort(sortByUpdatedAtDesc);
|
|
1163
|
+
};
|
|
1164
|
+
var sanitizeRememberedSelection = (entry) => {
|
|
1165
|
+
const candidate = entry;
|
|
1166
|
+
if (typeof candidate?.pageUrl !== "string" || candidate.pageUrl.length === 0) {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
const base = {
|
|
1170
|
+
pageKey: typeof candidate.pageKey === "string" && candidate.pageKey.length > 0 ? candidate.pageKey : normalizeSelectionPageKey(candidate.pageUrl),
|
|
1171
|
+
pageUrl: candidate.pageUrl,
|
|
1172
|
+
updatedAt: typeof candidate.updatedAt === "number" ? candidate.updatedAt : 0
|
|
1173
|
+
};
|
|
1174
|
+
if (candidate.target?.type === "page") {
|
|
1175
|
+
return {
|
|
1176
|
+
...base,
|
|
1177
|
+
target: { type: "page" }
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
if (candidate.target?.type !== "iframe") {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
...base,
|
|
1185
|
+
target: {
|
|
1186
|
+
type: "iframe",
|
|
1187
|
+
url: normalizeTargetUrl(typeof candidate.target.url === "string" ? candidate.target.url : null),
|
|
1188
|
+
title: normalizeTargetTitle(typeof candidate.target.title === "string" ? candidate.target.title : null)
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
function normalizeTargetUrl(url) {
|
|
1193
|
+
return normalizeOptionalText(url);
|
|
1194
|
+
}
|
|
1195
|
+
function normalizeTargetTitle(title) {
|
|
1196
|
+
return normalizeOptionalText(title);
|
|
1197
|
+
}
|
|
1198
|
+
function normalizeOptionalText(value) {
|
|
1199
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1200
|
+
}
|
|
1201
|
+
var sortByUpdatedAtDesc = (left, right) => right.updatedAt - left.updatedAt;
|
|
1202
|
+
var readStorageValue = async (storageArea, key) => {
|
|
1203
|
+
return await new Promise((resolve, reject) => {
|
|
1204
|
+
storageArea.get(key, (items) => {
|
|
1205
|
+
const error = chrome.runtime.lastError;
|
|
1206
|
+
if (error) {
|
|
1207
|
+
reject(new Error(error.message));
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
resolve(items[key]);
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
};
|
|
1214
|
+
var writeStorageValue = async (storageArea, items) => {
|
|
1215
|
+
await new Promise((resolve, reject) => {
|
|
1216
|
+
storageArea.set(items, () => {
|
|
1217
|
+
const error = chrome.runtime.lastError;
|
|
1218
|
+
if (error) {
|
|
1219
|
+
reject(new Error(error.message));
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
resolve();
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
// src/background/target-visibility-history.ts
|
|
1228
|
+
var DEFAULT_STORAGE_KEY2 = "hiddenTargetHistory";
|
|
1229
|
+
var DEFAULT_MAX_PAGE_ENTRIES = 20;
|
|
1230
|
+
var TargetVisibilityHistoryStore = class {
|
|
1231
|
+
persistence;
|
|
1232
|
+
maxPageEntries;
|
|
1233
|
+
entries = [];
|
|
1234
|
+
loadPromise = null;
|
|
1235
|
+
saveChain = Promise.resolve();
|
|
1236
|
+
constructor(persistence = createChromeStoragePersistence2(), options = {}) {
|
|
1237
|
+
this.persistence = persistence;
|
|
1238
|
+
this.maxPageEntries = options.maxPageEntries ?? DEFAULT_MAX_PAGE_ENTRIES;
|
|
1239
|
+
}
|
|
1240
|
+
async getHiddenTargets(pageUrl) {
|
|
1241
|
+
await this.ensureLoaded();
|
|
1242
|
+
return [...this.findEntry(pageUrl)?.targets ?? []];
|
|
1243
|
+
}
|
|
1244
|
+
async hide(pageUrl, target) {
|
|
1245
|
+
await this.ensureLoaded();
|
|
1246
|
+
const hiddenTarget = toHiddenTarget(target);
|
|
1247
|
+
if (!hiddenTarget) {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
const pageKey = normalizeSelectionPageKey(pageUrl);
|
|
1251
|
+
const existing = this.findEntry(pageUrl);
|
|
1252
|
+
const targets = uniqueHiddenTargets([hiddenTarget, ...existing?.targets ?? []]);
|
|
1253
|
+
const entry = { pageKey, pageUrl, updatedAt: Date.now(), targets };
|
|
1254
|
+
this.upsertEntry(entry);
|
|
1255
|
+
await this.persist();
|
|
1256
|
+
return entry;
|
|
1257
|
+
}
|
|
1258
|
+
async show(pageUrl, target) {
|
|
1259
|
+
await this.ensureLoaded();
|
|
1260
|
+
const hiddenTarget = toHiddenTarget(target);
|
|
1261
|
+
if (!hiddenTarget) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const existing = this.findEntry(pageUrl);
|
|
1265
|
+
if (!existing) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const targets = existing.targets.filter((candidate) => !isSameHiddenTarget(candidate, hiddenTarget));
|
|
1269
|
+
if (targets.length === 0) {
|
|
1270
|
+
this.entries = this.entries.filter((entry) => entry.pageKey !== existing.pageKey);
|
|
1271
|
+
await this.persist();
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
this.entries = this.entries.map((entry) => entry.pageKey === existing.pageKey ? { ...entry, targets, updatedAt: Date.now() } : entry);
|
|
1275
|
+
await this.persist();
|
|
1276
|
+
}
|
|
1277
|
+
async isHidden(pageUrl, target) {
|
|
1278
|
+
const hiddenTargets = await this.getHiddenTargets(pageUrl);
|
|
1279
|
+
return hiddenTargets.some((hiddenTarget) => matchesHiddenTarget(hiddenTarget, target));
|
|
1280
|
+
}
|
|
1281
|
+
findEntry(pageUrl) {
|
|
1282
|
+
const pageKey = normalizeSelectionPageKey(pageUrl);
|
|
1283
|
+
return this.entries.find((entry) => entry.pageKey === pageKey) ?? null;
|
|
1284
|
+
}
|
|
1285
|
+
upsertEntry(entry) {
|
|
1286
|
+
this.entries = [entry, ...this.entries.filter((candidate) => candidate.pageKey !== entry.pageKey)].slice(0, this.maxPageEntries);
|
|
1287
|
+
}
|
|
1288
|
+
async ensureLoaded() {
|
|
1289
|
+
if (!this.loadPromise) {
|
|
1290
|
+
this.loadPromise = this.persistence.load().then((entries) => {
|
|
1291
|
+
this.entries = sanitizeHiddenTargetEntries(entries).slice(0, this.maxPageEntries);
|
|
1292
|
+
}).catch((error) => {
|
|
1293
|
+
console.error("[TargetVisibilityHistoryStore] Failed to load history:", error);
|
|
1294
|
+
this.entries = [];
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
await this.loadPromise;
|
|
1298
|
+
}
|
|
1299
|
+
async persist() {
|
|
1300
|
+
this.saveChain = this.saveChain.catch(() => void 0).then(() => this.persistence.save(this.entries)).catch((error) => {
|
|
1301
|
+
console.error("[TargetVisibilityHistoryStore] Failed to save history:", error);
|
|
1302
|
+
});
|
|
1303
|
+
await this.saveChain;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
var createChromeStoragePersistence2 = (storageArea = chrome.storage.local, storageKey = DEFAULT_STORAGE_KEY2) => ({
|
|
1307
|
+
load: async () => {
|
|
1308
|
+
const stored = await readStorageValue2(storageArea, storageKey);
|
|
1309
|
+
return sanitizeHiddenTargetEntries(Array.isArray(stored) ? stored : []);
|
|
1310
|
+
},
|
|
1311
|
+
save: async (entries) => {
|
|
1312
|
+
await writeStorageValue2(storageArea, { [storageKey]: entries });
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
function matchesHiddenTarget(hiddenTarget, target) {
|
|
1316
|
+
if (target.type !== "iframe") {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
const candidate = toHiddenTarget(target);
|
|
1320
|
+
return candidate ? isSameHiddenTarget(hiddenTarget, candidate) : false;
|
|
1321
|
+
}
|
|
1322
|
+
function toHiddenTarget(target) {
|
|
1323
|
+
if (target.type !== "iframe") {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
const url = normalizeOptionalText2(target.url);
|
|
1327
|
+
const title = normalizeOptionalText2(target.title);
|
|
1328
|
+
if (!url && !title) {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
return { type: "iframe", url, title };
|
|
1332
|
+
}
|
|
1333
|
+
function uniqueHiddenTargets(targets) {
|
|
1334
|
+
const unique = [];
|
|
1335
|
+
for (const target of targets) {
|
|
1336
|
+
if (!unique.some((candidate) => isSameHiddenTarget(candidate, target))) {
|
|
1337
|
+
unique.push(target);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return unique;
|
|
1341
|
+
}
|
|
1342
|
+
function isSameHiddenTarget(left, right) {
|
|
1343
|
+
if (left.url || right.url) {
|
|
1344
|
+
return left.url === right.url;
|
|
1345
|
+
}
|
|
1346
|
+
return left.title === right.title;
|
|
1347
|
+
}
|
|
1348
|
+
function sanitizeHiddenTargetEntries(entries) {
|
|
1349
|
+
return entries.map(sanitizeHiddenTargetEntry).filter((entry) => entry !== null).sort(sortByUpdatedAtDesc2);
|
|
1350
|
+
}
|
|
1351
|
+
function sanitizeHiddenTargetEntry(entry) {
|
|
1352
|
+
const candidate = entry;
|
|
1353
|
+
if (typeof candidate?.pageUrl !== "string" || candidate.pageUrl.length === 0 || !Array.isArray(candidate.targets)) {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
const targets = uniqueHiddenTargets(candidate.targets.map(sanitizeHiddenTarget).filter((target) => target !== null));
|
|
1357
|
+
if (targets.length === 0) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
pageKey: typeof candidate.pageKey === "string" && candidate.pageKey.length > 0 ? candidate.pageKey : normalizeSelectionPageKey(candidate.pageUrl),
|
|
1362
|
+
pageUrl: candidate.pageUrl,
|
|
1363
|
+
updatedAt: typeof candidate.updatedAt === "number" ? candidate.updatedAt : 0,
|
|
1364
|
+
targets
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function sanitizeHiddenTarget(target) {
|
|
1368
|
+
const candidate = target;
|
|
1369
|
+
if (candidate?.type !== "iframe") {
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
const url = normalizeOptionalText2(typeof candidate.url === "string" ? candidate.url : null);
|
|
1373
|
+
const title = normalizeOptionalText2(typeof candidate.title === "string" ? candidate.title : null);
|
|
1374
|
+
if (!url && !title) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
return { type: "iframe", url, title };
|
|
1378
|
+
}
|
|
1379
|
+
function normalizeOptionalText2(value) {
|
|
1380
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1381
|
+
}
|
|
1382
|
+
var sortByUpdatedAtDesc2 = (left, right) => right.updatedAt - left.updatedAt;
|
|
1383
|
+
var readStorageValue2 = async (storageArea, key) => {
|
|
1384
|
+
return await new Promise((resolve, reject) => {
|
|
1385
|
+
storageArea.get(key, (items) => {
|
|
1386
|
+
const error = chrome.runtime.lastError;
|
|
1387
|
+
if (error) {
|
|
1388
|
+
reject(new Error(error.message));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
resolve(items[key]);
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1394
|
+
};
|
|
1395
|
+
var writeStorageValue2 = async (storageArea, items) => {
|
|
1396
|
+
await new Promise((resolve, reject) => {
|
|
1397
|
+
storageArea.set(items, () => {
|
|
1398
|
+
const error = chrome.runtime.lastError;
|
|
1399
|
+
if (error) {
|
|
1400
|
+
reject(new Error(error.message));
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
resolve();
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// src/background/action-badge.ts
|
|
1409
|
+
var badgeSyncChain = Promise.resolve();
|
|
1410
|
+
function syncActionBadge(debuggerManager2) {
|
|
1411
|
+
badgeSyncChain = badgeSyncChain.catch(() => void 0).then(async () => {
|
|
1412
|
+
const attachedCount = debuggerManager2.listAttached().length;
|
|
1413
|
+
await applyBadgeState(attachedCount);
|
|
1414
|
+
}).catch((error) => {
|
|
1415
|
+
console.error("[ServiceWorker] Failed to sync action badge:", error);
|
|
1416
|
+
});
|
|
1417
|
+
return badgeSyncChain;
|
|
1418
|
+
}
|
|
1419
|
+
async function applyBadgeState(attachedCount) {
|
|
1420
|
+
const badgeText = attachedCount > 0 ? String(attachedCount) : "";
|
|
1421
|
+
const tabs = await chrome.tabs.query({});
|
|
1422
|
+
await Promise.all(
|
|
1423
|
+
tabs.filter((tab) => tab.id !== void 0).map((tab) => chrome.action.setBadgeText({ tabId: tab.id, text: badgeText }))
|
|
1424
|
+
);
|
|
1425
|
+
await chrome.action.setBadgeText({ text: badgeText });
|
|
1426
|
+
if (attachedCount > 0) {
|
|
1427
|
+
await chrome.action.setBadgeBackgroundColor({ color: "#4CAF50" });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/background/popup-targets.ts
|
|
1432
|
+
function parseWatcherTargetId(targetId) {
|
|
1433
|
+
if (targetId.startsWith("tab:")) {
|
|
1434
|
+
const tabId = Number.parseInt(targetId.slice(4), 10);
|
|
1435
|
+
return Number.isFinite(tabId) ? { tabId, frameId: null } : null;
|
|
1436
|
+
}
|
|
1437
|
+
if (targetId.startsWith("frame:")) {
|
|
1438
|
+
const [, tabIdRaw, ...frameIdParts] = targetId.split(":");
|
|
1439
|
+
const tabId = Number.parseInt(tabIdRaw ?? "", 10);
|
|
1440
|
+
const frameId = frameIdParts.join(":");
|
|
1441
|
+
return Number.isFinite(tabId) && frameId ? { tabId, frameId } : null;
|
|
1442
|
+
}
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
function getPopupTargets(debuggerManager2, tabId) {
|
|
1446
|
+
const frames = debuggerManager2.getFrames(tabId);
|
|
1447
|
+
const topFrameId = debuggerManager2.getTarget(tabId)?.topFrameId ?? null;
|
|
1448
|
+
const topFrame = frames.find((frame) => frame.frameId === topFrameId) ?? frames.find((frame) => frame.parentFrameId == null);
|
|
1449
|
+
if (!topFrame) {
|
|
1450
|
+
return [];
|
|
1451
|
+
}
|
|
1452
|
+
return [
|
|
1453
|
+
{
|
|
1454
|
+
type: "page",
|
|
1455
|
+
frameId: null,
|
|
1456
|
+
parentFrameId: null,
|
|
1457
|
+
title: topFrame.title || "Page",
|
|
1458
|
+
url: topFrame.url
|
|
1459
|
+
},
|
|
1460
|
+
...frames.filter((frame) => frame.frameId !== topFrame.frameId).map((frame) => ({
|
|
1461
|
+
type: "iframe",
|
|
1462
|
+
frameId: frame.frameId,
|
|
1463
|
+
parentFrameId: frame.parentFrameId === topFrame.frameId ? null : frame.parentFrameId,
|
|
1464
|
+
title: frame.title || frame.url || `iframe ${frame.frameId.slice(0, 8)}`,
|
|
1465
|
+
url: frame.url
|
|
1466
|
+
}))
|
|
1467
|
+
];
|
|
1468
|
+
}
|
|
1469
|
+
function getPopupTarget(debuggerManager2, tabId, frameId) {
|
|
1470
|
+
return getPopupTargets(debuggerManager2, tabId).find((target) => (target.frameId ?? null) === frameId) ?? null;
|
|
1471
|
+
}
|
|
1472
|
+
function getRequiredIframeTarget(debuggerManager2, tabId, frameId) {
|
|
1473
|
+
const target = getPopupTarget(debuggerManager2, tabId, frameId);
|
|
1474
|
+
if (!target || target.type !== "iframe" || !target.frameId) {
|
|
1475
|
+
throw new Error(`Unknown iframe target for tab ${tabId}`);
|
|
1476
|
+
}
|
|
1477
|
+
return target;
|
|
1478
|
+
}
|
|
1479
|
+
function getPageUrlForTab(debuggerManager2, tabId) {
|
|
1480
|
+
return getPopupTarget(debuggerManager2, tabId, null)?.url ?? debuggerManager2.getTarget(tabId)?.url ?? null;
|
|
1481
|
+
}
|
|
1482
|
+
function toSelectionTarget(target) {
|
|
1483
|
+
return {
|
|
1484
|
+
type: target.type,
|
|
1485
|
+
frameId: target.frameId,
|
|
1486
|
+
title: target.title,
|
|
1487
|
+
url: target.url
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/background/watcher-status.ts
|
|
1492
|
+
function buildWatcherStatus(debuggerManager2, tabId, session) {
|
|
1493
|
+
const watcherInfo = session.getWatcherInfo();
|
|
1494
|
+
const targetInfo = session.getTargetInfo();
|
|
1495
|
+
const parsedTarget = targetInfo ? parseWatcherTargetId(targetInfo.targetId) : null;
|
|
1496
|
+
if (!watcherInfo && !targetInfo) {
|
|
1497
|
+
return createWatcherStatus(debuggerManager2, tabId, session, null, null);
|
|
1498
|
+
}
|
|
1499
|
+
return createWatcherStatus(
|
|
1500
|
+
debuggerManager2,
|
|
1501
|
+
tabId,
|
|
1502
|
+
session,
|
|
1503
|
+
watcherInfo ?? null,
|
|
1504
|
+
targetInfo && parsedTarget ? {
|
|
1505
|
+
type: parsedTarget.frameId ? "iframe" : "page",
|
|
1506
|
+
title: targetInfo.title,
|
|
1507
|
+
url: targetInfo.url,
|
|
1508
|
+
targetId: targetInfo.targetId,
|
|
1509
|
+
frameId: parsedTarget.frameId,
|
|
1510
|
+
attachedAt: targetInfo.attachedAt,
|
|
1511
|
+
targetReady: targetInfo.targetReady
|
|
1512
|
+
} : null
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
function createWatcherStatus(debuggerManager2, tabId, session, watcherInfo, currentTarget) {
|
|
1516
|
+
const readiness = buildWatcherReadiness(debuggerManager2, tabId, session, watcherInfo, currentTarget);
|
|
1517
|
+
return {
|
|
1518
|
+
tabId,
|
|
1519
|
+
bridgeConnected: readiness.nativeHostConnected,
|
|
1520
|
+
nativeHostConnected: readiness.nativeHostConnected,
|
|
1521
|
+
watcherReady: readiness.watcherReady,
|
|
1522
|
+
targetReady: readiness.targetReady,
|
|
1523
|
+
targetState: readiness.targetState,
|
|
1524
|
+
watcherId: watcherInfo?.watcherId ?? null,
|
|
1525
|
+
watcherHost: watcherInfo?.watcherHost ?? null,
|
|
1526
|
+
watcherPort: watcherInfo?.watcherPort ?? null,
|
|
1527
|
+
nativeHostPid: watcherInfo?.pid ?? null,
|
|
1528
|
+
lastMessageAt: session.getLastMessageAt(),
|
|
1529
|
+
currentTarget
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function buildWatcherReadiness(debuggerManager2, tabId, session, watcherInfo, currentTarget) {
|
|
1533
|
+
const nativeHostConnected = session.isConnected();
|
|
1534
|
+
const watcherReady = nativeHostConnected && Boolean(watcherInfo?.watcherId && watcherInfo.watcherHost && watcherInfo.watcherPort != null);
|
|
1535
|
+
const targetReady = currentTarget?.targetReady ?? getTargetReadiness(debuggerManager2, tabId, currentTarget);
|
|
1536
|
+
return {
|
|
1537
|
+
nativeHostConnected,
|
|
1538
|
+
watcherReady,
|
|
1539
|
+
targetReady,
|
|
1540
|
+
targetState: targetReady == null ? "not-selected" : targetReady ? "ready" : "rebinding"
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function getTargetReadiness(debuggerManager2, tabId, target) {
|
|
1544
|
+
if (!target) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
if (!target.frameId) {
|
|
1548
|
+
return debuggerManager2.isAttached(tabId);
|
|
1549
|
+
}
|
|
1550
|
+
const frame = debuggerManager2.getFrames(tabId).find((candidate) => candidate.frameId === target.frameId);
|
|
1551
|
+
return Boolean(frame?.sessionId);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/background/service-worker.ts
|
|
1555
|
+
var debuggerManager = new DebuggerManager();
|
|
1556
|
+
var controlBridgeSession = new ControlBridgeSession(debuggerManager, {
|
|
1557
|
+
onWatcherInfo: (info) => {
|
|
1558
|
+
recordEvent("info", "bridge", `Control watcher ready: ${info.watcherId} (pid ${info.pid})`);
|
|
1559
|
+
},
|
|
1560
|
+
onAttachTabWatcher: attachTabFromControl,
|
|
1561
|
+
onDetachTabWatcher: detachTabFromControl,
|
|
1562
|
+
getWatcherIdForTab,
|
|
1563
|
+
getDiagnostics: buildControlDiagnostics,
|
|
1564
|
+
onDisconnect: () => {
|
|
1565
|
+
recordEvent("error", "bridge", "Control native host disconnected");
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
var bridgeSessions = /* @__PURE__ */ new Map();
|
|
1569
|
+
var selectedFrameByTabId = /* @__PURE__ */ new Map();
|
|
1570
|
+
var pendingRememberedTargetByTabId = /* @__PURE__ */ new Map();
|
|
1571
|
+
var targetSelectionHistory = new TargetSelectionHistoryStore();
|
|
1572
|
+
var targetVisibilityHistory = new TargetVisibilityHistoryStore();
|
|
1573
|
+
var recentEvents = [];
|
|
1574
|
+
var MAX_RECENT_EVENTS = 8;
|
|
1575
|
+
var REMEMBERED_TARGET_RETRY_EVENTS = /* @__PURE__ */ new Set(["Page.frameAttached", "Page.frameDetached", "Page.frameNavigated"]);
|
|
1576
|
+
function recordEvent(level, source, message) {
|
|
1577
|
+
recentEvents.unshift({
|
|
1578
|
+
ts: Date.now(),
|
|
1579
|
+
level,
|
|
1580
|
+
source,
|
|
1581
|
+
message
|
|
1582
|
+
});
|
|
1583
|
+
if (recentEvents.length > MAX_RECENT_EVENTS) {
|
|
1584
|
+
recentEvents.length = MAX_RECENT_EVENTS;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
debuggerManager.onDetach((tabId, reason) => {
|
|
1588
|
+
clearTabState(tabId);
|
|
1589
|
+
recordEvent("error", "debugger", `Tab ${tabId} detached: ${reason}`);
|
|
1590
|
+
void syncActionBadge(debuggerManager);
|
|
1591
|
+
});
|
|
1592
|
+
debuggerManager.onEvent((tabId, method) => {
|
|
1593
|
+
if (!shouldReplayRememberedTarget(tabId, method)) {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
void replayRememberedTargetSelection(tabId);
|
|
1597
|
+
});
|
|
1598
|
+
chrome.runtime.onMessage.addListener(
|
|
1599
|
+
(message, _sender, sendResponse) => {
|
|
1600
|
+
void handlePopupMessage(message, sendResponse);
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
);
|
|
1604
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1605
|
+
ensureControlBridgeSession();
|
|
1606
|
+
});
|
|
1607
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1608
|
+
ensureControlBridgeSession();
|
|
1609
|
+
});
|
|
1610
|
+
async function handlePopupMessage(message, sendResponse) {
|
|
1611
|
+
ensureControlBridgeSession();
|
|
1612
|
+
pruneStaleBridgeSessions();
|
|
1613
|
+
const response = await buildPopupResponse(message);
|
|
1614
|
+
await syncActionBadge(debuggerManager);
|
|
1615
|
+
sendResponse(response);
|
|
1616
|
+
}
|
|
1617
|
+
function ensureControlBridgeSession() {
|
|
1618
|
+
if (controlBridgeSession.isConnected()) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
controlBridgeSession.connect();
|
|
1622
|
+
}
|
|
1623
|
+
ensureControlBridgeSession();
|
|
1624
|
+
async function attachTabFromControl(tabId, options = {}) {
|
|
1625
|
+
try {
|
|
1626
|
+
const session = await attachBridgeSession(tabId, options);
|
|
1627
|
+
setSelectedFrame(tabId, null);
|
|
1628
|
+
await prepareRememberedTargetSelection(tabId);
|
|
1629
|
+
recordEvent("info", "bridge", `Control attached tab ${tabId}`);
|
|
1630
|
+
void syncActionBadge(debuggerManager);
|
|
1631
|
+
const tab = await getTabInfo(tabId);
|
|
1632
|
+
if (!tab) {
|
|
1633
|
+
return { ok: false, error: `Tab ${tabId} is no longer available` };
|
|
1634
|
+
}
|
|
1635
|
+
return { ok: true, tab, watcherId: session.getWatcherInfo()?.watcherId };
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async function detachTabFromControl(tabId) {
|
|
1641
|
+
try {
|
|
1642
|
+
await detachTab(tabId);
|
|
1643
|
+
recordEvent("info", "bridge", `Control detached tab ${tabId}`);
|
|
1644
|
+
void syncActionBadge(debuggerManager);
|
|
1645
|
+
const tab = await getTabInfo(tabId);
|
|
1646
|
+
if (!tab) {
|
|
1647
|
+
return { ok: false, error: `Tab ${tabId} is no longer available` };
|
|
1648
|
+
}
|
|
1649
|
+
return { ok: true, tab };
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
async function buildPopupResponse(message) {
|
|
1655
|
+
try {
|
|
1656
|
+
switch (message.action) {
|
|
1657
|
+
case "getTargets":
|
|
1658
|
+
return { success: true, tabs: await getTabsWithTargets() };
|
|
1659
|
+
case "attach": {
|
|
1660
|
+
const tabId = requireTabId(message);
|
|
1661
|
+
await attachBridgeSession(tabId);
|
|
1662
|
+
setSelectedFrame(tabId, null);
|
|
1663
|
+
await prepareRememberedTargetSelection(tabId);
|
|
1664
|
+
recordEvent("info", "popup", `Attached tab ${tabId}`);
|
|
1665
|
+
return { success: true };
|
|
1666
|
+
}
|
|
1667
|
+
case "detach": {
|
|
1668
|
+
const tabId = requireTabId(message);
|
|
1669
|
+
await detachTab(tabId);
|
|
1670
|
+
recordEvent("info", "popup", `Detached tab ${tabId}`);
|
|
1671
|
+
return { success: true };
|
|
1672
|
+
}
|
|
1673
|
+
case "focusTab": {
|
|
1674
|
+
const tabId = requireTabId(message);
|
|
1675
|
+
await focusTab(tabId);
|
|
1676
|
+
recordEvent("info", "popup", `Focused tab ${tabId}`);
|
|
1677
|
+
return { success: true };
|
|
1678
|
+
}
|
|
1679
|
+
case "selectTarget": {
|
|
1680
|
+
const tabId = requireTabId(message);
|
|
1681
|
+
const session = bridgeSessions.get(tabId);
|
|
1682
|
+
if (!session) {
|
|
1683
|
+
return { success: false, error: `No watcher bridge for tab ${tabId}` };
|
|
1684
|
+
}
|
|
1685
|
+
const frameId = message.frameId ?? null;
|
|
1686
|
+
const target = getPopupTarget(debuggerManager, tabId, frameId);
|
|
1687
|
+
if (!target) {
|
|
1688
|
+
return { success: false, error: `Unknown target for tab ${tabId}` };
|
|
1689
|
+
}
|
|
1690
|
+
session.selectTarget(frameId);
|
|
1691
|
+
clearPendingRememberedTarget(tabId);
|
|
1692
|
+
setSelectedFrame(tabId, frameId);
|
|
1693
|
+
await rememberTargetSelection(tabId, target);
|
|
1694
|
+
recordEvent("info", "popup", `Selected ${frameId ? `iframe ${frameId}` : "page"} on tab ${tabId}`);
|
|
1695
|
+
return { success: true };
|
|
1696
|
+
}
|
|
1697
|
+
case "hideTarget": {
|
|
1698
|
+
const tabId = requireTabId(message);
|
|
1699
|
+
const target = getRequiredIframeTarget(debuggerManager, tabId, message.frameId ?? null);
|
|
1700
|
+
await hideTarget(tabId, target);
|
|
1701
|
+
recordEvent("info", "popup", `Hid iframe ${target.frameId} on tab ${tabId}`);
|
|
1702
|
+
return { success: true };
|
|
1703
|
+
}
|
|
1704
|
+
case "showTarget": {
|
|
1705
|
+
const tabId = requireTabId(message);
|
|
1706
|
+
const target = getRequiredIframeTarget(debuggerManager, tabId, message.frameId ?? null);
|
|
1707
|
+
await showTarget(tabId, target);
|
|
1708
|
+
recordEvent("info", "popup", `Restored iframe ${target.frameId} on tab ${tabId}`);
|
|
1709
|
+
return { success: true };
|
|
1710
|
+
}
|
|
1711
|
+
case "getStatus":
|
|
1712
|
+
return {
|
|
1713
|
+
success: true,
|
|
1714
|
+
status: buildPopupStatusPayload()
|
|
1715
|
+
};
|
|
1716
|
+
default:
|
|
1717
|
+
return { success: false, error: `Unknown action: ${message.action}` };
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
recordEvent("error", "popup", err instanceof Error ? err.message : "Unknown popup error");
|
|
1721
|
+
return {
|
|
1722
|
+
success: false,
|
|
1723
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
function requireTabId(message) {
|
|
1728
|
+
if (message.tabId !== void 0) {
|
|
1729
|
+
return message.tabId;
|
|
1730
|
+
}
|
|
1731
|
+
throw new Error("No tabId provided");
|
|
1732
|
+
}
|
|
1733
|
+
async function attachBridgeSession(tabId, options = {}) {
|
|
1734
|
+
const existing = bridgeSessions.get(tabId);
|
|
1735
|
+
if (existing) {
|
|
1736
|
+
await connectBridgeSession(tabId, existing);
|
|
1737
|
+
return existing;
|
|
1738
|
+
}
|
|
1739
|
+
const session = new TabBridgeSession(
|
|
1740
|
+
tabId,
|
|
1741
|
+
debuggerManager,
|
|
1742
|
+
{
|
|
1743
|
+
onWatcherInfo: (info) => {
|
|
1744
|
+
recordEvent("info", "bridge", `Watcher ready for tab ${tabId}: ${info.watcherId} (pid ${info.pid})`);
|
|
1745
|
+
},
|
|
1746
|
+
onTargetInfo: (info) => {
|
|
1747
|
+
syncSelectedFrameFromWatcher(info.targetId);
|
|
1748
|
+
},
|
|
1749
|
+
onDisconnect: () => {
|
|
1750
|
+
recordEvent("error", "bridge", `Native host disconnected for tab ${tabId}`);
|
|
1751
|
+
void debuggerManager.detach(tabId).catch(() => {
|
|
1752
|
+
});
|
|
1753
|
+
clearTabState(tabId);
|
|
1754
|
+
void syncActionBadge(debuggerManager);
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
options
|
|
1758
|
+
);
|
|
1759
|
+
bridgeSessions.set(tabId, session);
|
|
1760
|
+
await connectBridgeSession(tabId, session);
|
|
1761
|
+
return session;
|
|
1762
|
+
}
|
|
1763
|
+
async function detachTab(tabId) {
|
|
1764
|
+
const session = bridgeSessions.get(tabId);
|
|
1765
|
+
if (session) {
|
|
1766
|
+
await session.detach();
|
|
1767
|
+
} else {
|
|
1768
|
+
await debuggerManager.detach(tabId);
|
|
1769
|
+
}
|
|
1770
|
+
clearTabState(tabId);
|
|
1771
|
+
}
|
|
1772
|
+
async function focusTab(tabId) {
|
|
1773
|
+
const tab = await chrome.tabs.get(tabId);
|
|
1774
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
1775
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
1776
|
+
}
|
|
1777
|
+
async function connectBridgeSession(tabId, session) {
|
|
1778
|
+
try {
|
|
1779
|
+
await session.connectAndAttach();
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
destroyBridgeSession(tabId);
|
|
1782
|
+
throw error;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function clearTabState(tabId) {
|
|
1786
|
+
selectedFrameByTabId.delete(tabId);
|
|
1787
|
+
clearPendingRememberedTarget(tabId);
|
|
1788
|
+
destroyBridgeSession(tabId);
|
|
1789
|
+
}
|
|
1790
|
+
function pruneStaleBridgeSessions() {
|
|
1791
|
+
for (const [tabId, session] of bridgeSessions.entries()) {
|
|
1792
|
+
if (debuggerManager.isAttached(tabId)) {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
if (!session.getTargetInfo()) {
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
clearTabState(tabId);
|
|
1799
|
+
recordEvent("error", "bridge", `Pruned stale watcher state for tab ${tabId}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function destroyBridgeSession(tabId) {
|
|
1803
|
+
const session = bridgeSessions.get(tabId);
|
|
1804
|
+
if (!session) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
bridgeSessions.delete(tabId);
|
|
1808
|
+
session.dispose();
|
|
1809
|
+
}
|
|
1810
|
+
function buildPopupStatusPayload() {
|
|
1811
|
+
return {
|
|
1812
|
+
bridgeConnected: [...bridgeSessions.values()].some((session) => session.isConnected()),
|
|
1813
|
+
attachedTabs: debuggerManager.listAttached().map((target) => ({
|
|
1814
|
+
tabId: target.tabId,
|
|
1815
|
+
url: target.url,
|
|
1816
|
+
title: target.title
|
|
1817
|
+
})),
|
|
1818
|
+
watchers: getWatcherStatuses(),
|
|
1819
|
+
recentEvents
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
function buildControlDiagnostics() {
|
|
1823
|
+
const controlInfo = controlBridgeSession.getWatcherInfo();
|
|
1824
|
+
return {
|
|
1825
|
+
extensionId: chrome.runtime.id ?? null,
|
|
1826
|
+
extensionVersion: chrome.runtime.getManifest().version ?? null,
|
|
1827
|
+
control: {
|
|
1828
|
+
connected: controlBridgeSession.isConnected(),
|
|
1829
|
+
watcherId: controlInfo?.watcherId ?? null,
|
|
1830
|
+
watcherHost: controlInfo?.watcherHost ?? null,
|
|
1831
|
+
watcherPort: controlInfo?.watcherPort ?? null,
|
|
1832
|
+
pid: controlInfo?.pid ?? null,
|
|
1833
|
+
lastMessageAt: controlBridgeSession.getLastMessageAt()
|
|
1834
|
+
},
|
|
1835
|
+
tabWatchers: [...bridgeSessions.entries()].map(([tabId, session]) => buildTabBridgeStatus(tabId, session)),
|
|
1836
|
+
recentEvents
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
function buildTabBridgeStatus(tabId, session) {
|
|
1840
|
+
const watcher = session.getWatcherInfo();
|
|
1841
|
+
const target = session.getTargetInfo();
|
|
1842
|
+
return {
|
|
1843
|
+
tabId,
|
|
1844
|
+
connected: session.isConnected(),
|
|
1845
|
+
watcherId: watcher?.watcherId ?? null,
|
|
1846
|
+
watcherHost: watcher?.watcherHost ?? null,
|
|
1847
|
+
watcherPort: watcher?.watcherPort ?? null,
|
|
1848
|
+
pid: watcher?.pid ?? null,
|
|
1849
|
+
targetId: target?.targetId ?? null,
|
|
1850
|
+
targetTitle: target?.title ?? null,
|
|
1851
|
+
targetUrl: target?.url ?? null,
|
|
1852
|
+
targetReady: target?.targetReady ?? null,
|
|
1853
|
+
lastMessageAt: session.getLastMessageAt()
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
async function getTabInfo(tabId) {
|
|
1857
|
+
const tabs = await listBrowserTabs(debuggerManager, void 0, {
|
|
1858
|
+
getWatcherIdForTab
|
|
1859
|
+
});
|
|
1860
|
+
return tabs.find((tab) => tab.tabId === tabId) ?? null;
|
|
1861
|
+
}
|
|
1862
|
+
function getWatcherIdForTab(tabId) {
|
|
1863
|
+
return bridgeSessions.get(tabId)?.getWatcherInfo()?.watcherId ?? null;
|
|
1864
|
+
}
|
|
1865
|
+
function getWatcherStatuses() {
|
|
1866
|
+
return [...bridgeSessions.entries()].map(([tabId, session]) => buildWatcherStatus(debuggerManager, tabId, session)).filter((status) => status !== null);
|
|
1867
|
+
}
|
|
1868
|
+
async function getTabsWithTargets() {
|
|
1869
|
+
const tabs = await listBrowserTabs(debuggerManager);
|
|
1870
|
+
return await Promise.all(
|
|
1871
|
+
tabs.map(async (tab) => {
|
|
1872
|
+
const session = bridgeSessions.get(tab.tabId);
|
|
1873
|
+
const { visibleTargets, hiddenTargets } = await getPopupTargetVisibility(tab.tabId, tab.attached);
|
|
1874
|
+
return {
|
|
1875
|
+
...tab,
|
|
1876
|
+
targets: visibleTargets,
|
|
1877
|
+
hiddenTargets,
|
|
1878
|
+
selectedFrameId: getSelectedFrameId(tab.tabId),
|
|
1879
|
+
watcher: session ? buildWatcherStatus(debuggerManager, tab.tabId, session) : null
|
|
1880
|
+
};
|
|
1881
|
+
})
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
async function getPopupTargetVisibility(tabId, attached) {
|
|
1885
|
+
const targets = attached ? getPopupTargets(debuggerManager, tabId) : [];
|
|
1886
|
+
const pageUrl = getPageUrlForTab(debuggerManager, tabId);
|
|
1887
|
+
if (!pageUrl) {
|
|
1888
|
+
return { visibleTargets: targets, hiddenTargets: [] };
|
|
1889
|
+
}
|
|
1890
|
+
const hiddenTargets = await filterHiddenTargets(pageUrl, targets);
|
|
1891
|
+
if (hiddenTargets.length === 0) {
|
|
1892
|
+
return { visibleTargets: targets, hiddenTargets };
|
|
1893
|
+
}
|
|
1894
|
+
return {
|
|
1895
|
+
visibleTargets: targets.filter((target) => !hiddenTargets.includes(target)),
|
|
1896
|
+
hiddenTargets
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
function syncSelectedFrameFromWatcher(targetId) {
|
|
1900
|
+
const target = parseWatcherTargetId(targetId);
|
|
1901
|
+
if (!target) {
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
setSelectedFrame(target.tabId, target.frameId);
|
|
1905
|
+
}
|
|
1906
|
+
function getSelectedFrameId(tabId) {
|
|
1907
|
+
if (!selectedFrameByTabId.has(tabId)) {
|
|
1908
|
+
return void 0;
|
|
1909
|
+
}
|
|
1910
|
+
return selectedFrameByTabId.get(tabId) ?? null;
|
|
1911
|
+
}
|
|
1912
|
+
async function rememberTargetSelection(tabId, target) {
|
|
1913
|
+
const pageUrl = getPageUrlForTab(debuggerManager, tabId);
|
|
1914
|
+
if (!pageUrl) {
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
await targetSelectionHistory.remember(pageUrl, toSelectionTarget(target));
|
|
1918
|
+
}
|
|
1919
|
+
async function hideTarget(tabId, target) {
|
|
1920
|
+
const pageUrl = getPageUrlForTab(debuggerManager, tabId);
|
|
1921
|
+
if (!pageUrl) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
await targetVisibilityHistory.hide(pageUrl, toSelectionTarget(target));
|
|
1925
|
+
if (getSelectedFrameId(tabId) !== target.frameId) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
const session = bridgeSessions.get(tabId);
|
|
1929
|
+
session?.selectTarget(null);
|
|
1930
|
+
setSelectedFrame(tabId, null);
|
|
1931
|
+
const pageTarget = getPopupTarget(debuggerManager, tabId, null);
|
|
1932
|
+
if (pageTarget) {
|
|
1933
|
+
await rememberTargetSelection(tabId, pageTarget);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
async function showTarget(tabId, target) {
|
|
1937
|
+
const pageUrl = getPageUrlForTab(debuggerManager, tabId);
|
|
1938
|
+
if (!pageUrl) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
await targetVisibilityHistory.show(pageUrl, toSelectionTarget(target));
|
|
1942
|
+
}
|
|
1943
|
+
async function filterHiddenTargets(pageUrl, targets) {
|
|
1944
|
+
const hiddenTargets = await targetVisibilityHistory.getHiddenTargets(pageUrl);
|
|
1945
|
+
if (hiddenTargets.length === 0) {
|
|
1946
|
+
return [];
|
|
1947
|
+
}
|
|
1948
|
+
return targets.filter((target) => hiddenTargets.some((hiddenTarget) => matchesHiddenTarget(hiddenTarget, toSelectionTarget(target))));
|
|
1949
|
+
}
|
|
1950
|
+
async function prepareRememberedTargetSelection(tabId) {
|
|
1951
|
+
const pageUrl = getPageUrlForTab(debuggerManager, tabId);
|
|
1952
|
+
if (!pageUrl) {
|
|
1953
|
+
clearPendingRememberedTarget(tabId);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const remembered = await targetSelectionHistory.getByPageUrl(pageUrl);
|
|
1957
|
+
if (!remembered || remembered.target.type === "page") {
|
|
1958
|
+
clearPendingRememberedTarget(tabId);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
setPendingRememberedTarget(tabId, remembered);
|
|
1962
|
+
await replayRememberedTargetSelection(tabId);
|
|
1963
|
+
}
|
|
1964
|
+
async function replayRememberedTargetSelection(tabId) {
|
|
1965
|
+
const remembered = pendingRememberedTargetByTabId.get(tabId);
|
|
1966
|
+
const session = bridgeSessions.get(tabId);
|
|
1967
|
+
if (!remembered || !session) {
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const target = matchRememberedIframeTarget(
|
|
1971
|
+
remembered,
|
|
1972
|
+
getPopupTargets(debuggerManager, tabId).map((candidate) => toSelectionTarget(candidate))
|
|
1973
|
+
);
|
|
1974
|
+
if (!target?.frameId) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
session.selectTarget(target.frameId);
|
|
1979
|
+
setSelectedFrame(tabId, target.frameId);
|
|
1980
|
+
clearPendingRememberedTarget(tabId);
|
|
1981
|
+
recordEvent("info", "bridge", `Restored iframe ${target.frameId} on tab ${tabId}`);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
console.warn(`[ServiceWorker] Failed to restore remembered iframe for tab ${tabId}:`, error);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function shouldReplayRememberedTarget(tabId, method) {
|
|
1987
|
+
return pendingRememberedTargetByTabId.has(tabId) && REMEMBERED_TARGET_RETRY_EVENTS.has(method);
|
|
1988
|
+
}
|
|
1989
|
+
function setSelectedFrame(tabId, frameId) {
|
|
1990
|
+
selectedFrameByTabId.set(tabId, frameId);
|
|
1991
|
+
}
|
|
1992
|
+
function setPendingRememberedTarget(tabId, remembered) {
|
|
1993
|
+
pendingRememberedTargetByTabId.set(tabId, remembered);
|
|
1994
|
+
}
|
|
1995
|
+
function clearPendingRememberedTarget(tabId) {
|
|
1996
|
+
pendingRememberedTargetByTabId.delete(tabId);
|
|
1997
|
+
}
|
|
1998
|
+
console.log("[ServiceWorker] Argus CDP Bridge extension loaded");
|
|
1999
|
+
void syncActionBadge(debuggerManager);
|
|
2000
|
+
export {
|
|
2001
|
+
bridgeSessions,
|
|
2002
|
+
debuggerManager
|
|
2003
|
+
};
|
|
2004
|
+
//# sourceMappingURL=service-worker.js.map
|