@sovovs/bycli 2.1.59 → 2.1.61

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.
@@ -0,0 +1,3380 @@
1
+ const DAEMON_PORT = 19825;
2
+ const DAEMON_HOST = "127.0.0.1";
3
+ const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
4
+ const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
5
+ const WS_RECONNECT_BASE_DELAY = 2e3;
6
+ const WS_RECONNECT_MAX_DELAY = 5e3;
7
+
8
+ const AUTH_PARAM_SEGMENTS = /* @__PURE__ */ new Set([
9
+ "token",
10
+ "tokens",
11
+ "jwt",
12
+ "secret",
13
+ "fingerprint",
14
+ "signature",
15
+ "sig",
16
+ "sign",
17
+ "csrf",
18
+ "xsrf",
19
+ "session",
20
+ "sessionid",
21
+ "sid",
22
+ "password",
23
+ "passwd",
24
+ "pwd",
25
+ "auth",
26
+ "authorization",
27
+ "accesstoken",
28
+ "refreshtoken",
29
+ "idtoken",
30
+ "apikey",
31
+ "appkey",
32
+ "bearer",
33
+ "credential",
34
+ "credentials"
35
+ ]);
36
+ const JWT_VALUE_RE = /^ey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]+$/;
37
+ const MASKED = "***";
38
+ function paramNameLooksAuth(name) {
39
+ return name.toLowerCase().split(/[-_.]/).some((seg) => AUTH_PARAM_SEGMENTS.has(seg));
40
+ }
41
+ function maskUrlAuthTokens(url) {
42
+ if (!url) return url ?? "";
43
+ try {
44
+ const u = new URL(url);
45
+ let changed = false;
46
+ for (const [k, v] of [...u.searchParams.entries()]) {
47
+ if (paramNameLooksAuth(k) || JWT_VALUE_RE.test(v)) {
48
+ u.searchParams.set(k, MASKED);
49
+ changed = true;
50
+ }
51
+ }
52
+ return changed ? u.toString() : url;
53
+ } catch {
54
+ return url;
55
+ }
56
+ }
57
+
58
+ const UI_BINDING_NAME = "__bycli_ui";
59
+ const MAX_UI_EVENTS = 2e3;
60
+ const ALLOWED_TYPES = ["click", "input", "submit", "keydown", "navigate"];
61
+ const clampStr = (v, max) => typeof v === "string" && v.length ? v.slice(0, max) : void 0;
62
+ function parseUiEvent(payload) {
63
+ let raw;
64
+ try {
65
+ raw = JSON.parse(payload);
66
+ } catch {
67
+ return null;
68
+ }
69
+ if (!raw || typeof raw !== "object") return null;
70
+ const type = raw.type;
71
+ if (!ALLOWED_TYPES.includes(type)) return null;
72
+ const ts = typeof raw.ts === "number" && Number.isFinite(raw.ts) ? raw.ts : Date.now();
73
+ if (type === "navigate") {
74
+ const rawUrl = clampStr(raw.url, 2048);
75
+ if (!rawUrl) return null;
76
+ return { type, ts, selector: "", tag: "document", url: maskUrlAuthTokens(rawUrl).slice(0, 2048) };
77
+ }
78
+ const selector = clampStr(raw.selector, 300);
79
+ if (!selector) return null;
80
+ const ev = { type, ts, selector, tag: clampStr(raw.tag, 24) ?? "unknown" };
81
+ const role = clampStr(raw.role, 40);
82
+ if (role) ev.role = role;
83
+ const text = clampStr(raw.text, 80);
84
+ if (text) ev.text = text;
85
+ const key = clampStr(raw.key, 24);
86
+ if (key) ev.key = key;
87
+ const vs = raw.valueShape;
88
+ if (vs && typeof vs.len === "number" && Number.isFinite(vs.len)) {
89
+ const kind = vs.kind;
90
+ const k = kind === "email" || kind === "url" || kind === "number" ? kind : "text";
91
+ ev.valueShape = { len: Math.max(0, Math.min(1e5, Math.floor(vs.len))), kind: k };
92
+ }
93
+ return ev;
94
+ }
95
+ const UI_LISTENER_SOURCE = `(function(){
96
+ if (window.__bycli_ui_installed) return; window.__bycli_ui_installed = true;
97
+ var B = ${JSON.stringify(UI_BINDING_NAME)};
98
+ function esc(s){ try { return (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g,'\\\\$&'); } catch(e){ return String(s); } }
99
+ function sel(el){
100
+ if(!el||el.nodeType!==1) return '';
101
+ if(el.id) return '#'+esc(el.id);
102
+ var dt = el.getAttribute && el.getAttribute('data-testid'); if(dt) return '[data-testid="'+dt+'"]';
103
+ var parts=[], n=el, depth=0;
104
+ while(n && n.nodeType===1 && depth<5){
105
+ if(n.id){ parts.unshift('#'+esc(n.id)); break; }
106
+ var part=n.tagName.toLowerCase(), p=n.parentElement;
107
+ if(p){ var same=[]; for(var i=0;i<p.children.length;i++){ if(p.children[i].tagName===n.tagName) same.push(p.children[i]); }
108
+ if(same.length>1) part+=':nth-of-type('+(same.indexOf(n)+1)+')'; }
109
+ parts.unshift(part); n=n.parentElement; depth++;
110
+ }
111
+ return parts.join(' > ');
112
+ }
113
+ function tag(el){ return el && el.tagName ? el.tagName.toLowerCase() : 'unknown'; }
114
+ function txt(el){ try { var t=(el.innerText||el.textContent||'').trim(); return t ? t.slice(0,80) : undefined; } catch(e){ return undefined; } }
115
+ function shape(v){ v=String(v==null?'':v); var kind='text';
116
+ if(/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(v)) kind='email';
117
+ else if(/^https?:\\/\\//.test(v)) kind='url';
118
+ else if(v!=='' && !isNaN(Number(v))) kind='number';
119
+ return { len: v.length, kind: kind }; }
120
+ function emit(o){ try { o.ts=Date.now(); if(typeof window[B]==='function') window[B](JSON.stringify(o)); } catch(e){} }
121
+ document.addEventListener('click', function(e){ var t=e.target; emit({ type:'click', selector:sel(t), tag:tag(t), role:(t.getAttribute&&t.getAttribute('role'))||undefined, text:txt(t) }); }, { capture:true, passive:true });
122
+ document.addEventListener('change', function(e){ var t=e.target; if(!t||!('value' in t)) return;
123
+ if(t.type==='password'){ emit({ type:'input', selector:sel(t), tag:tag(t) }); return; }
124
+ emit({ type:'input', selector:sel(t), tag:tag(t), valueShape:shape(t.value) }); }, { capture:true, passive:true });
125
+ document.addEventListener('submit', function(e){ emit({ type:'submit', selector:sel(e.target), tag:'form' }); }, { capture:true, passive:true });
126
+ document.addEventListener('keydown', function(e){ if(e.key==='Enter'||e.key==='Escape'){ emit({ type:'keydown', selector:sel(e.target), tag:tag(e.target), key:e.key }); } }, { capture:true, passive:true });
127
+ // 导航录制:整页导航(脚本随新文档重注入,init 即发当前 URL)+ SPA 路由(history/popstate/hashchange)。
128
+ // dedupe 连续相同 URL,避免 replaceState 刷参噪音;脱敏在扩展 parse 侧再做一道,这里只发原始 location.href。
129
+ var lastUrl='';
130
+ function navEmit(){ try { var u=location.href; if(u&&u!==lastUrl){ lastUrl=u; emit({ type:'navigate', url:u }); } } catch(e){} }
131
+ navEmit();
132
+ try { var _ps=history.pushState; history.pushState=function(){ var r=_ps.apply(this,arguments); navEmit(); return r; }; } catch(e){}
133
+ try { var _rs=history.replaceState; history.replaceState=function(){ var r=_rs.apply(this,arguments); navEmit(); return r; }; } catch(e){}
134
+ window.addEventListener('popstate', navEmit, { passive:true });
135
+ window.addEventListener('hashchange', navEmit, { passive:true });
136
+ })();`;
137
+
138
+ const READER_ORIGIN = "https://ima.qq.com";
139
+ const READER_PREFIX = "/cgi-bin/knowledge_tab_reader/";
140
+ const READER_PATHS = /* @__PURE__ */ new Set([
141
+ "/get_knowledge_base_list",
142
+ "/get_knowledge_list"
143
+ ]);
144
+ const MEDIA_PATH = "/cgi-bin/file_manager/get_media";
145
+ const AUTH_SOURCE_PATHS = /* @__PURE__ */ new Set([
146
+ "/cgi-bin/activity_tab/get_available_activities"
147
+ ]);
148
+ const AUTH_TTL_MS = 3e4;
149
+ function normalizedHeaders(headers) {
150
+ const allowed = /* @__PURE__ */ new Set([
151
+ "x-ima-cookie",
152
+ "x-ima-bkn",
153
+ "extension_version",
154
+ "from_browser_ima"
155
+ ]);
156
+ return Object.fromEntries(Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), String(value)]).filter(([name]) => allowed.has(name)));
157
+ }
158
+ function isImaReaderRequest(url) {
159
+ try {
160
+ const parsed = new URL(url ?? "");
161
+ return parsed.origin === READER_ORIGIN && (parsed.pathname.startsWith(READER_PREFIX) || AUTH_SOURCE_PATHS.has(parsed.pathname));
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+ function isImaMediaRequest(url) {
167
+ try {
168
+ const parsed = new URL(url ?? "");
169
+ return parsed.origin === READER_ORIGIN && parsed.pathname === MEDIA_PATH;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+ function imaBknFromCookie(cookie) {
175
+ const token = cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith("IMA-TOKEN="))?.slice("IMA-TOKEN=".length);
176
+ if (!token) return null;
177
+ let hash = 5381;
178
+ for (const character of token) hash += (hash << 5) + character.charCodeAt(0);
179
+ return String(hash & 2147483647);
180
+ }
181
+ class ImaReaderAuthStore {
182
+ sessions = /* @__PURE__ */ new Map();
183
+ capture(tabId, request) {
184
+ if (!isImaReaderRequest(request.url) && !isImaMediaRequest(request.url)) return false;
185
+ const headers = normalizedHeaders(request.headers);
186
+ if (!headers["x-ima-cookie"]) return false;
187
+ headers["x-ima-bkn"] ??= imaBknFromCookie(headers["x-ima-cookie"]) ?? "";
188
+ if (!headers["x-ima-bkn"]) return false;
189
+ const existing = this.sessions.get(tabId);
190
+ this.sessions.set(tabId, {
191
+ authId: existing && existing.expiresAt > Date.now() ? existing.authId : crypto.randomUUID(),
192
+ expiresAt: Date.now() + AUTH_TTL_MS,
193
+ headers
194
+ });
195
+ return true;
196
+ }
197
+ read(tabId) {
198
+ const session = this.sessions.get(tabId);
199
+ if (!session || session.expiresAt <= Date.now()) {
200
+ this.sessions.delete(tabId);
201
+ return null;
202
+ }
203
+ return { authId: session.authId };
204
+ }
205
+ async request(tabId, authId, path, body, perform) {
206
+ if (!READER_PATHS.has(path)) throw new Error(`ima reader path is not allowed: ${path}`);
207
+ const session = this.sessions.get(tabId);
208
+ if (session?.authId === authId) {
209
+ if (session.expiresAt <= Date.now()) this.sessions.delete(tabId);
210
+ else return perform(session.headers, body);
211
+ }
212
+ throw new Error("ima reader authentication is missing or expired");
213
+ }
214
+ async mediaRequest(tabId, authId, body, perform) {
215
+ const session = this.sessions.get(tabId);
216
+ if (!session || session.authId !== authId || session.expiresAt <= Date.now()) throw new Error("ima reader authentication is missing or expired");
217
+ return perform(session.headers, body);
218
+ }
219
+ release(authId) {
220
+ for (const [tabId, session] of this.sessions) {
221
+ if (session.authId === authId) this.sessions.delete(tabId);
222
+ }
223
+ }
224
+ }
225
+
226
+ const attached = /* @__PURE__ */ new Set();
227
+ const tabFrameContexts = /* @__PURE__ */ new Map();
228
+ const frameTargets = /* @__PURE__ */ new Map();
229
+ const frameTargetKeys = /* @__PURE__ */ new Map();
230
+ let frameTargetCleanupRegistered = false;
231
+ const sessionToTab = /* @__PURE__ */ new Map();
232
+ const sessionToFrameUrl = /* @__PURE__ */ new Map();
233
+ const sessionToParent = /* @__PURE__ */ new Map();
234
+ const armedChildSessions = /* @__PURE__ */ new Map();
235
+ const MAX_CHILD_SESSIONS_PER_TAB = 50;
236
+ const tabCaptureGeneration = /* @__PURE__ */ new Map();
237
+ function childStateMap(tabId) {
238
+ let m = armedChildSessions.get(tabId);
239
+ if (!m) {
240
+ m = /* @__PURE__ */ new Map();
241
+ armedChildSessions.set(tabId, m);
242
+ }
243
+ return m;
244
+ }
245
+ function childDebuggee(tabId, sessionId) {
246
+ return { tabId, sessionId };
247
+ }
248
+ function sessionIdOf(source) {
249
+ return source.sessionId;
250
+ }
251
+ async function sendSafe(target, method, params = {}) {
252
+ try {
253
+ await chrome.debugger.sendCommand(target, method, params);
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ function reqKey(sessionId, requestId) {
260
+ return `${sessionId ?? "top"}:${requestId}`;
261
+ }
262
+ const AUTO_ATTACH_IFRAME_PARAMS = {
263
+ autoAttach: true,
264
+ waitForDebuggerOnStart: true,
265
+ flatten: true,
266
+ filter: [{ type: "iframe", exclude: false }]
267
+ };
268
+ const CDP_RESPONSE_BODY_CAPTURE_LIMIT = 8 * 1024 * 1024;
269
+ const CDP_REQUEST_BODY_CAPTURE_LIMIT = 1 * 1024 * 1024;
270
+ const CDP_WS_FRAME_CAPTURE_LIMIT = 256 * 1024;
271
+ const MAX_WS_FRAMES_PER_CONN = 500;
272
+ const IMA_READER_AUTH_BINDING = "__bycli_ima_reader_auth";
273
+ const IMA_READER_AUTH_CAPTURE_SOURCE = String.raw`
274
+ (() => {
275
+ if (window.__bycliImaReaderAuthCaptureInstalled) return;
276
+ window.__bycliImaReaderAuthCaptureInstalled = true;
277
+ const report = (url, headers) => {
278
+ try {
279
+ const normalized = Object.fromEntries(Object.entries(headers || {}).map(([key, value]) => [String(key).toLowerCase(), String(value)]));
280
+ if (normalized['x-ima-cookie'] && typeof window.${IMA_READER_AUTH_BINDING} === 'function') {
281
+ window.${IMA_READER_AUTH_BINDING}(JSON.stringify({ url: String(url), headers: normalized }));
282
+ }
283
+ } catch {}
284
+ };
285
+ const originalFetch = window.fetch;
286
+ window.fetch = function(input, init) {
287
+ try {
288
+ const request = new Request(input, init);
289
+ report(request.url, Object.fromEntries(request.headers.entries()));
290
+ } catch {}
291
+ return originalFetch.apply(this, arguments);
292
+ };
293
+ const originalOpen = XMLHttpRequest.prototype.open;
294
+ const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
295
+ XMLHttpRequest.prototype.open = function(method, url) {
296
+ this.__bycliImaReaderUrl = String(url);
297
+ this.__bycliImaReaderHeaders = {};
298
+ return originalOpen.apply(this, arguments);
299
+ };
300
+ XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
301
+ try {
302
+ this.__bycliImaReaderHeaders[String(name).toLowerCase()] = String(value);
303
+ report(this.__bycliImaReaderUrl, this.__bycliImaReaderHeaders);
304
+ } catch {}
305
+ return originalSetRequestHeader.apply(this, arguments);
306
+ };
307
+ })();`;
308
+ const networkCaptures = /* @__PURE__ */ new Map();
309
+ const imaReaderAuthStore = new ImaReaderAuthStore();
310
+ const imaReaderCaptureTabs = /* @__PURE__ */ new Set();
311
+ const imaReaderRequestUrls = /* @__PURE__ */ new Map();
312
+ let imaReaderFetchListenerRegistered = false;
313
+ function ensureImaReaderFetchListener() {
314
+ if (imaReaderFetchListenerRegistered) return;
315
+ imaReaderFetchListenerRegistered = true;
316
+ chrome.debugger.onEvent.addListener((source, method, params) => {
317
+ if (method !== "Fetch.requestPaused" || !source.tabId || !imaReaderCaptureTabs.has(source.tabId)) return;
318
+ const request = params;
319
+ if (!request?.requestId || !isImaReaderRequest(request.request?.url)) return;
320
+ imaReaderAuthStore.capture(source.tabId, request.request ?? {});
321
+ chrome.debugger.sendCommand({ tabId: source.tabId }, "Fetch.continueRequest", {
322
+ requestId: request.requestId
323
+ }).catch(() => {
324
+ });
325
+ });
326
+ }
327
+ const CAPTURE_RESOURCE_TYPES = /* @__PURE__ */ new Set(["XHR", "Fetch", "Document"]);
328
+ function isApiResourceType(type) {
329
+ return typeof type === "string" && CAPTURE_RESOURCE_TYPES.has(type);
330
+ }
331
+ function isStaticAssetContentType(ct) {
332
+ if (!ct) return false;
333
+ const t = ct.toLowerCase().split(";")[0].trim();
334
+ if (t.startsWith("image/") || t.startsWith("font/") || t.startsWith("audio/") || t.startsWith("video/")) return true;
335
+ return t === "text/css" || t === "text/javascript" || t === "application/javascript" || t === "application/x-javascript" || t === "application/ecmascript" || t === "application/font-woff" || t === "application/font-woff2" || t === "application/x-font-ttf" || t === "application/vnd.ms-fontobject";
336
+ }
337
+ const uiCaptures = /* @__PURE__ */ new Map();
338
+ function isDebuggableUrl$1(url) {
339
+ if (!url) return true;
340
+ return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:");
341
+ }
342
+ async function ensureAttached(tabId, aggressiveRetry = false) {
343
+ try {
344
+ const tab = await chrome.tabs.get(tabId);
345
+ if (!isDebuggableUrl$1(tab.url)) {
346
+ attached.delete(tabId);
347
+ throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`);
348
+ }
349
+ } catch (e) {
350
+ if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e;
351
+ attached.delete(tabId);
352
+ throw new Error(`Tab ${tabId} no longer exists`);
353
+ }
354
+ if (attached.has(tabId)) {
355
+ try {
356
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
357
+ expression: "1",
358
+ returnByValue: true
359
+ });
360
+ return;
361
+ } catch {
362
+ attached.delete(tabId);
363
+ }
364
+ }
365
+ const MAX_ATTACH_RETRIES = aggressiveRetry ? 5 : 2;
366
+ const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500;
367
+ let lastError = "";
368
+ for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) {
369
+ try {
370
+ try {
371
+ await chrome.debugger.detach({ tabId });
372
+ } catch {
373
+ }
374
+ await chrome.debugger.attach({ tabId }, "1.3");
375
+ lastError = "";
376
+ break;
377
+ } catch (e) {
378
+ lastError = e instanceof Error ? e.message : String(e);
379
+ if (attempt < MAX_ATTACH_RETRIES) {
380
+ console.warn(`[bycli] attach attempt ${attempt}/${MAX_ATTACH_RETRIES} failed: ${lastError}, retrying in ${RETRY_DELAY_MS}ms...`);
381
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
382
+ try {
383
+ const tab = await chrome.tabs.get(tabId);
384
+ if (!isDebuggableUrl$1(tab.url)) {
385
+ lastError = `Tab URL changed to ${tab.url} during retry`;
386
+ break;
387
+ }
388
+ } catch {
389
+ lastError = `Tab ${tabId} no longer exists`;
390
+ }
391
+ }
392
+ }
393
+ }
394
+ if (lastError) {
395
+ let finalUrl = "unknown";
396
+ let finalWindowId = "unknown";
397
+ try {
398
+ const tab = await chrome.tabs.get(tabId);
399
+ finalUrl = tab.url ?? "undefined";
400
+ finalWindowId = String(tab.windowId);
401
+ } catch {
402
+ }
403
+ console.warn(`[bycli] attach failed for tab ${tabId}: url=${finalUrl}, windowId=${finalWindowId}, error=${lastError}`);
404
+ const hint = lastError.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
405
+ throw new Error(`attach failed: ${lastError}${hint}`);
406
+ }
407
+ attached.add(tabId);
408
+ try {
409
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
410
+ } catch {
411
+ }
412
+ }
413
+ async function evaluate(tabId, expression, aggressiveRetry = false) {
414
+ const MAX_EVAL_RETRIES = aggressiveRetry ? 3 : 2;
415
+ for (let attempt = 1; attempt <= MAX_EVAL_RETRIES; attempt++) {
416
+ try {
417
+ await ensureAttached(tabId, aggressiveRetry);
418
+ const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
419
+ expression,
420
+ returnByValue: true,
421
+ awaitPromise: true
422
+ });
423
+ if (result.exceptionDetails) {
424
+ const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
425
+ throw new Error(errMsg);
426
+ }
427
+ return result.result?.value;
428
+ } catch (e) {
429
+ const msg = e instanceof Error ? e.message : String(e);
430
+ const isNavigateError = msg.includes("Inspected target navigated") || msg.includes("Target closed");
431
+ const isAttachError = isNavigateError || msg.includes("attach failed") || msg.includes("Debugger is not attached") || msg.includes("chrome-extension://");
432
+ if (isAttachError && attempt < MAX_EVAL_RETRIES) {
433
+ attached.delete(tabId);
434
+ const retryMs = isNavigateError ? 200 : 500;
435
+ await new Promise((resolve) => setTimeout(resolve, retryMs));
436
+ continue;
437
+ }
438
+ throw e;
439
+ }
440
+ }
441
+ throw new Error("evaluate: max retries exhausted");
442
+ }
443
+ const evaluateAsync = evaluate;
444
+ async function screenshot(tabId, options = {}) {
445
+ await ensureAttached(tabId);
446
+ const format = options.format ?? "png";
447
+ const fullPage = options.fullPage === true;
448
+ const overrideWidth = options.width && options.width > 0 ? Math.ceil(options.width) : void 0;
449
+ const overrideHeight = !fullPage && options.height && options.height > 0 ? Math.ceil(options.height) : void 0;
450
+ const needsOverride = fullPage || overrideWidth !== void 0 || overrideHeight !== void 0;
451
+ if (needsOverride) {
452
+ if (overrideWidth !== void 0 && fullPage) {
453
+ await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
454
+ mobile: false,
455
+ width: overrideWidth,
456
+ height: 0,
457
+ deviceScaleFactor: 1
458
+ });
459
+ }
460
+ let finalWidth = overrideWidth ?? 0;
461
+ let finalHeight = overrideHeight ?? 0;
462
+ if (fullPage) {
463
+ const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
464
+ const size = metrics.cssContentSize || metrics.contentSize;
465
+ if (size) {
466
+ if (finalWidth === 0) finalWidth = Math.ceil(size.width);
467
+ finalHeight = Math.ceil(size.height);
468
+ }
469
+ }
470
+ await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
471
+ mobile: false,
472
+ width: finalWidth,
473
+ height: finalHeight,
474
+ deviceScaleFactor: 1
475
+ });
476
+ }
477
+ try {
478
+ const params = { format };
479
+ if (format === "jpeg" && options.quality !== void 0) {
480
+ params.quality = Math.max(0, Math.min(100, options.quality));
481
+ }
482
+ const result = await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params);
483
+ return result.data;
484
+ } finally {
485
+ if (needsOverride) {
486
+ await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {
487
+ });
488
+ }
489
+ }
490
+ }
491
+ async function setFileInputFiles(tabId, files, selector) {
492
+ await ensureAttached(tabId);
493
+ await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
494
+ const doc = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument");
495
+ const query = selector || 'input[type="file"]';
496
+ const result = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
497
+ nodeId: doc.root.nodeId,
498
+ selector: query
499
+ });
500
+ if (!result.nodeId) {
501
+ throw new Error(`No element found matching selector: ${query}`);
502
+ }
503
+ await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
504
+ files,
505
+ nodeId: result.nodeId
506
+ });
507
+ }
508
+ function matchesDownloadPattern(item, pattern) {
509
+ if (!pattern) return true;
510
+ const haystack = [
511
+ item.filename,
512
+ item.url,
513
+ item.finalUrl,
514
+ item.mime
515
+ ].filter(Boolean).join("\n").toLowerCase();
516
+ return haystack.includes(pattern.toLowerCase());
517
+ }
518
+ function downloadResult(item, startedAt) {
519
+ return {
520
+ downloaded: item.state === "complete",
521
+ id: item.id,
522
+ filename: item.filename,
523
+ url: item.url,
524
+ finalUrl: item.finalUrl,
525
+ mime: item.mime,
526
+ totalBytes: item.totalBytes,
527
+ state: item.state,
528
+ danger: item.danger,
529
+ error: item.error,
530
+ elapsedMs: Date.now() - startedAt
531
+ };
532
+ }
533
+ async function waitForDownload(pattern = "", timeoutMs = 3e4, options = {}) {
534
+ const startedAt = Date.now();
535
+ const timeout = Math.max(1, timeoutMs);
536
+ const includeRecent = options.includeRecent ?? true;
537
+ const explicitStartedAfter = typeof options.startedAfterMs === "number" && Number.isFinite(options.startedAfterMs) && options.startedAfterMs > 0 ? options.startedAfterMs : void 0;
538
+ const recentStartedAfter = explicitStartedAfter ?? startedAt - Math.max(timeout, 1e3);
539
+ const searchStartedAfter = explicitStartedAfter === void 0 ? recentStartedAfter : Math.max(0, recentStartedAfter - 1);
540
+ return await new Promise((resolve) => {
541
+ let done = false;
542
+ const inProgressIds = /* @__PURE__ */ new Set();
543
+ const finish = (result) => {
544
+ if (done) return;
545
+ done = true;
546
+ clearTimeout(timer);
547
+ chrome.downloads.onCreated.removeListener(onCreated);
548
+ chrome.downloads.onChanged.removeListener(onChanged);
549
+ resolve(result);
550
+ };
551
+ const inspectById = async (id) => {
552
+ const items = await chrome.downloads.search({ id });
553
+ const item = items[0];
554
+ if (!item || !matchesDownloadPattern(item, pattern)) return;
555
+ inProgressIds.add(id);
556
+ if (item.state === "complete" || item.state === "interrupted") finish(downloadResult(item, startedAt));
557
+ };
558
+ const onCreated = (item) => {
559
+ if (!matchesDownloadPattern(item, pattern)) return;
560
+ inProgressIds.add(item.id);
561
+ if (item.state === "complete" || item.state === "interrupted") finish(downloadResult(item, startedAt));
562
+ };
563
+ const onChanged = (delta) => {
564
+ if (!delta.id) return;
565
+ if (!inProgressIds.has(delta.id)) return;
566
+ if (delta.filename?.current || delta.url?.current) {
567
+ void inspectById(delta.id);
568
+ return;
569
+ }
570
+ if (delta.state?.current === "complete" || delta.state?.current === "interrupted") {
571
+ void inspectById(delta.id);
572
+ }
573
+ };
574
+ const timer = setTimeout(() => {
575
+ finish({
576
+ downloaded: false,
577
+ state: "interrupted",
578
+ error: `No download matched "${pattern || "*"}" within ${timeout}ms`,
579
+ elapsedMs: Date.now() - startedAt
580
+ });
581
+ }, timeout);
582
+ chrome.downloads.onCreated.addListener(onCreated);
583
+ chrome.downloads.onChanged.addListener(onChanged);
584
+ if (!includeRecent) return;
585
+ void chrome.downloads.search({
586
+ limit: 50,
587
+ orderBy: ["-startTime"],
588
+ startedAfter: new Date(searchStartedAfter).toISOString()
589
+ }).then((recent) => {
590
+ if (done) return;
591
+ const eligible = recent.filter((item) => {
592
+ if (explicitStartedAfter === void 0) return true;
593
+ const itemStartedAt = typeof item.startTime === "string" ? Date.parse(item.startTime) : Number.NaN;
594
+ return Number.isFinite(itemStartedAt) && itemStartedAt >= explicitStartedAfter;
595
+ });
596
+ const completed = eligible.find((item) => item.state === "complete" && matchesDownloadPattern(item, pattern));
597
+ if (completed) {
598
+ finish(downloadResult(completed, startedAt));
599
+ return;
600
+ }
601
+ for (const item of eligible) {
602
+ if (item.state === "in_progress" && matchesDownloadPattern(item, pattern)) {
603
+ inProgressIds.add(item.id);
604
+ void inspectById(item.id);
605
+ }
606
+ }
607
+ }).catch((err) => {
608
+ finish({
609
+ downloaded: false,
610
+ state: "interrupted",
611
+ error: err instanceof Error ? err.message : String(err),
612
+ elapsedMs: Date.now() - startedAt
613
+ });
614
+ });
615
+ });
616
+ }
617
+ function frameTargetKey(tabId, frameId) {
618
+ return `${tabId}:${frameId}`;
619
+ }
620
+ function registerFrameTargetCleanup() {
621
+ if (frameTargetCleanupRegistered) return;
622
+ frameTargetCleanupRegistered = true;
623
+ chrome.debugger.onEvent.addListener((_source, method, params) => {
624
+ if (method === "Target.detachedFromTarget") {
625
+ const targetId = String(params?.targetId || "");
626
+ clearFrameTarget(targetId);
627
+ }
628
+ });
629
+ }
630
+ function clearFrameTarget(targetId) {
631
+ if (!targetId) return;
632
+ const key = frameTargetKeys.get(targetId);
633
+ if (key) frameTargets.delete(key);
634
+ frameTargetKeys.delete(targetId);
635
+ }
636
+ async function ensureFrameTarget(tabId, frameId, aggressiveRetry = false, targetUrl) {
637
+ registerFrameTargetCleanup();
638
+ await ensureAttached(tabId, aggressiveRetry);
639
+ const key = frameTargetKey(tabId, frameId);
640
+ const existing = frameTargets.get(key);
641
+ if (existing) return existing;
642
+ await chrome.debugger.sendCommand({ tabId }, "Target.setDiscoverTargets", { discover: true }).catch(() => {
643
+ });
644
+ const captureActive = networkCaptures.has(tabId) || uiCaptures.has(tabId);
645
+ await chrome.debugger.sendCommand({ tabId }, "Target.setAutoAttach", {
646
+ autoAttach: true,
647
+ waitForDebuggerOnStart: captureActive,
648
+ flatten: true,
649
+ filter: [{ type: "iframe", exclude: false }]
650
+ }).catch(() => {
651
+ });
652
+ const targetId = await resolveFrameTargetId(tabId, frameId, targetUrl);
653
+ try {
654
+ await chrome.debugger.attach({ targetId }, "1.3");
655
+ } catch (err) {
656
+ const message = err instanceof Error ? err.message : String(err);
657
+ if (!message.includes("Another debugger is already attached")) throw err;
658
+ }
659
+ frameTargets.set(key, targetId);
660
+ frameTargetKeys.set(targetId, key);
661
+ return targetId;
662
+ }
663
+ async function resolveFrameTargetId(tabId, frameId, targetUrl) {
664
+ const result = await chrome.debugger.sendCommand({ tabId }, "Target.getTargets").catch(() => null);
665
+ const targets = result?.targetInfos ?? [];
666
+ const frameTarget = targets.find((candidate) => {
667
+ const candidateId = candidate.targetId || candidate.id;
668
+ return candidate.type === "iframe" && (candidateId === frameId || !!targetUrl && candidate.url === targetUrl);
669
+ });
670
+ const targetId = frameTarget?.targetId || frameTarget?.id;
671
+ if (targetId) return targetId;
672
+ const candidates = targets.filter((target) => target.type === "iframe").map((target) => `${target.targetId || target.id || "?"} ${target.url || ""}`).join("; ");
673
+ throw new Error(`No iframe target found for frame ${frameId}${targetUrl ? ` (${targetUrl})` : ""}. Candidates: ${candidates || "none"}`);
674
+ }
675
+ async function sendCommandInFrameTarget(tabId, frameId, method, params = {}, aggressiveRetry = false, _timeoutMs = 3e4, targetUrl) {
676
+ const targetId = await ensureFrameTarget(tabId, frameId, aggressiveRetry, targetUrl);
677
+ const target = { targetId };
678
+ return chrome.debugger.sendCommand(target, method, params);
679
+ }
680
+ async function insertText(tabId, text) {
681
+ await ensureAttached(tabId);
682
+ await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text });
683
+ }
684
+ function registerFrameTracking() {
685
+ registerFrameTargetCleanup();
686
+ chrome.debugger.onEvent.addListener((source, method, params) => {
687
+ const tabId = source.tabId;
688
+ if (!tabId) return;
689
+ if (sessionIdOf(source)) return;
690
+ if (method === "Runtime.executionContextCreated") {
691
+ const context = params.context;
692
+ if (!context?.auxData?.frameId || context.auxData.isDefault !== true) return;
693
+ const frameId = context.auxData.frameId;
694
+ if (!tabFrameContexts.has(tabId)) {
695
+ tabFrameContexts.set(tabId, /* @__PURE__ */ new Map());
696
+ }
697
+ tabFrameContexts.get(tabId).set(frameId, context.id);
698
+ }
699
+ if (method === "Runtime.executionContextDestroyed") {
700
+ const ctxId = params.executionContextId;
701
+ const contexts = tabFrameContexts.get(tabId);
702
+ if (contexts) {
703
+ for (const [fid, cid] of contexts) {
704
+ if (cid === ctxId) {
705
+ contexts.delete(fid);
706
+ break;
707
+ }
708
+ }
709
+ }
710
+ }
711
+ if (method === "Runtime.executionContextsCleared") {
712
+ tabFrameContexts.delete(tabId);
713
+ }
714
+ });
715
+ chrome.tabs.onRemoved.addListener((tabId) => {
716
+ tabFrameContexts.delete(tabId);
717
+ });
718
+ }
719
+ async function getFrameTree(tabId) {
720
+ await ensureAttached(tabId);
721
+ return chrome.debugger.sendCommand({ tabId }, "Page.getFrameTree");
722
+ }
723
+ async function evaluateInFrame(tabId, expression, frameId, aggressiveRetry = false) {
724
+ await ensureAttached(tabId, aggressiveRetry);
725
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable").catch(() => {
726
+ });
727
+ const contexts = tabFrameContexts.get(tabId);
728
+ const contextId = contexts?.get(frameId);
729
+ if (contextId === void 0) {
730
+ await sendCommandInFrameTarget(tabId, frameId, "Runtime.enable", {}, aggressiveRetry).catch(() => void 0);
731
+ const result2 = await sendCommandInFrameTarget(tabId, frameId, "Runtime.evaluate", {
732
+ expression,
733
+ returnByValue: true,
734
+ awaitPromise: true
735
+ }, aggressiveRetry);
736
+ if (result2.exceptionDetails) {
737
+ const errMsg = result2.exceptionDetails.exception?.description || result2.exceptionDetails.text || "Eval error";
738
+ throw new Error(errMsg);
739
+ }
740
+ return result2.result?.value;
741
+ }
742
+ const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
743
+ expression,
744
+ contextId,
745
+ returnByValue: true,
746
+ awaitPromise: true
747
+ });
748
+ if (result.exceptionDetails) {
749
+ const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
750
+ throw new Error(errMsg);
751
+ }
752
+ return result.result?.value;
753
+ }
754
+ function normalizeCapturePatterns(pattern) {
755
+ return String(pattern || "").split("|").map((part) => part.trim()).filter(Boolean);
756
+ }
757
+ function shouldCaptureUrl(url, patterns) {
758
+ if (!url) return false;
759
+ if (!patterns.length) return true;
760
+ return patterns.some((pattern) => url.includes(pattern));
761
+ }
762
+ function normalizeHeaders(headers) {
763
+ if (!headers || typeof headers !== "object") return {};
764
+ const out = {};
765
+ for (const [key, value] of Object.entries(headers)) {
766
+ out[String(key)] = String(value);
767
+ }
768
+ return out;
769
+ }
770
+ function getOrCreateNetworkCaptureEntry(tabId, key, fallback, kind = "cdp") {
771
+ const state = networkCaptures.get(tabId);
772
+ if (!state) return null;
773
+ const existingIndex = state.requestToIndex.get(key);
774
+ if (existingIndex !== void 0) {
775
+ return state.entries[existingIndex] || null;
776
+ }
777
+ const url = fallback?.url || "";
778
+ if (!shouldCaptureUrl(url, state.patterns)) return null;
779
+ const entry = {
780
+ kind,
781
+ url,
782
+ method: fallback?.method || "GET",
783
+ requestHeaders: fallback?.requestHeaders || {},
784
+ timestamp: Date.now()
785
+ };
786
+ state.entries.push(entry);
787
+ state.requestToIndex.set(key, state.entries.length - 1);
788
+ return entry;
789
+ }
790
+ async function armChildSession(tabId, sessionId) {
791
+ const states = childStateMap(tabId);
792
+ let st = states.get(sessionId);
793
+ const isNew = st === void 0;
794
+ if (isNew && states.size >= MAX_CHILD_SESSIONS_PER_TAB) {
795
+ states.set(sessionId, { autoAttach: false, network: false, ui: false, overCap: true });
796
+ sessionToTab.set(sessionId, tabId);
797
+ await sendSafe(childDebuggee(tabId, sessionId), "Runtime.runIfWaitingForDebugger", {});
798
+ return;
799
+ }
800
+ if (!st) {
801
+ st = { autoAttach: false, network: false, ui: false, overCap: false };
802
+ states.set(sessionId, st);
803
+ }
804
+ if (st.overCap) return;
805
+ sessionToTab.set(sessionId, tabId);
806
+ const child = childDebuggee(tabId, sessionId);
807
+ try {
808
+ if (!st.autoAttach) {
809
+ if (await sendSafe(child, "Target.setAutoAttach", { ...AUTO_ATTACH_IFRAME_PARAMS })) st.autoAttach = true;
810
+ }
811
+ if (networkCaptures.has(tabId) && !st.network) {
812
+ if (await sendSafe(child, "Network.enable", {})) st.network = true;
813
+ }
814
+ if (uiCaptures.has(tabId) && !st.ui) {
815
+ const ok = await sendSafe(child, "Runtime.enable", {}) && await sendSafe(child, "Page.enable", {}) && await sendSafe(child, "Runtime.addBinding", { name: UI_BINDING_NAME }) && await sendSafe(child, "Page.addScriptToEvaluateOnNewDocument", { source: UI_LISTENER_SOURCE });
816
+ await sendSafe(child, "Runtime.evaluate", { expression: UI_LISTENER_SOURCE });
817
+ if (ok) st.ui = true;
818
+ }
819
+ } finally {
820
+ if (isNew) await sendSafe(child, "Runtime.runIfWaitingForDebugger", {});
821
+ }
822
+ }
823
+ async function rearmChildSessions(tabId) {
824
+ const states = armedChildSessions.get(tabId);
825
+ if (!states) return;
826
+ const gen = tabCaptureGeneration.get(tabId) ?? 0;
827
+ for (const sessionId of [...states.keys()]) {
828
+ if ((tabCaptureGeneration.get(tabId) ?? 0) !== gen) return;
829
+ if (armedChildSessions.get(tabId) !== states) return;
830
+ if (!states.has(sessionId)) continue;
831
+ await armChildSession(tabId, sessionId);
832
+ }
833
+ }
834
+ async function enableIframeAutoAttach(tabId) {
835
+ await sendSafe({ tabId }, "Target.setAutoAttach", { ...AUTO_ATTACH_IFRAME_PARAMS });
836
+ }
837
+ async function startNetworkCapture(tabId, pattern) {
838
+ await ensureAttached(tabId);
839
+ await chrome.debugger.sendCommand({ tabId }, "Network.enable");
840
+ networkCaptures.set(tabId, {
841
+ patterns: normalizeCapturePatterns(pattern),
842
+ entries: [],
843
+ requestToIndex: /* @__PURE__ */ new Map()
844
+ });
845
+ await enableIframeAutoAttach(tabId);
846
+ await rearmChildSessions(tabId);
847
+ }
848
+ async function readNetworkCapture(tabId, filter) {
849
+ const state = networkCaptures.get(tabId);
850
+ if (!state) return [];
851
+ let entries = state.entries.filter(
852
+ (e) => e.kind === "cdp-websocket" || !isStaticAssetContentType(e.responseContentType)
853
+ );
854
+ if (filter) {
855
+ const r = applyFrameFilter(tabId, entries, filter);
856
+ if (r.resolve.kind === "ambiguous") throw new AmbiguousIframeTargetError(r.resolve.candidates.length);
857
+ entries = r.items;
858
+ }
859
+ state.entries = [];
860
+ state.requestToIndex.clear();
861
+ return entries;
862
+ }
863
+ async function startImaReaderAuthCapture(tabId) {
864
+ await ensureAttached(tabId);
865
+ await chrome.debugger.sendCommand({ tabId }, "Network.enable");
866
+ ensureImaReaderFetchListener();
867
+ await chrome.debugger.sendCommand({ tabId }, "Fetch.enable", {
868
+ patterns: [
869
+ { urlPattern: "https://ima.qq.com/cgi-bin/knowledge_tab_reader/*", requestStage: "Request" },
870
+ { urlPattern: "https://ima.qq.com/cgi-bin/file_manager/get_media", requestStage: "Request" },
871
+ { urlPattern: "https://ima.qq.com/cgi-bin/activity_tab/get_available_activities", requestStage: "Request" }
872
+ ]
873
+ });
874
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.addBinding", { name: IMA_READER_AUTH_BINDING });
875
+ await chrome.debugger.sendCommand({ tabId }, "Page.addScriptToEvaluateOnNewDocument", {
876
+ source: IMA_READER_AUTH_CAPTURE_SOURCE
877
+ });
878
+ await evaluate(tabId, IMA_READER_AUTH_CAPTURE_SOURCE, true);
879
+ imaReaderCaptureTabs.add(tabId);
880
+ }
881
+ function readImaReaderAuth(tabId) {
882
+ return imaReaderAuthStore.read(tabId);
883
+ }
884
+ function readerRequestExpression(headers, path, body, base = "https://ima.qq.com/cgi-bin/knowledge_tab_reader") {
885
+ const url = `${base}${path}`;
886
+ const requestHeaders = { "content-type": "application/json", ...headers };
887
+ return `(async () => {
888
+ const response = await fetch(${JSON.stringify(url)}, {
889
+ method: 'POST', credentials: 'include', redirect: 'error',
890
+ headers: ${JSON.stringify(requestHeaders)}, body: ${JSON.stringify(JSON.stringify(body))},
891
+ signal: AbortSignal.timeout(30000),
892
+ });
893
+ if (!response.ok) throw new Error('ima reader API returned HTTP ' + response.status);
894
+ return response.json();
895
+ })()`;
896
+ }
897
+ async function requestImaReader(tabId, authId, path, body) {
898
+ return imaReaderAuthStore.request(tabId, authId, path, body, async (headers, requestBody) => evaluate(tabId, readerRequestExpression(headers, path, requestBody), true));
899
+ }
900
+ async function requestImaMedia(tabId, authId, body) {
901
+ return imaReaderAuthStore.mediaRequest(tabId, authId, body, async (headers, requestBody) => evaluate(tabId, readerRequestExpression(headers, "", requestBody, "https://ima.qq.com/cgi-bin/file_manager/get_media"), true));
902
+ }
903
+ function releaseImaReaderAuth(authId) {
904
+ imaReaderAuthStore.release(authId);
905
+ }
906
+ class AmbiguousIframeTargetError extends Error {
907
+ code = "ambiguous_iframe_target";
908
+ constructor(count) {
909
+ super(`ambiguous iframe target: ${count} candidate frames matched`);
910
+ }
911
+ }
912
+ function normalizeUrlForMatch(raw) {
913
+ try {
914
+ const u = new URL(raw);
915
+ const pathname = u.pathname.replace(/\/+$/, "") || "/";
916
+ return { href: `${u.origin}${pathname}${u.search}`, origin: u.origin, pathname };
917
+ } catch {
918
+ return null;
919
+ }
920
+ }
921
+ function resolveTargetFrameSessions(tabId, targetFrameUrl) {
922
+ const target = normalizeUrlForMatch(maskUrlAuthTokens(targetFrameUrl));
923
+ if (!target) return { kind: "none" };
924
+ const states = armedChildSessions.get(tabId);
925
+ if (!states || states.size === 0) return { kind: "none" };
926
+ const candidates = [];
927
+ for (const sid of states.keys()) {
928
+ const frameUrl = sessionToFrameUrl.get(sid);
929
+ if (!frameUrl) continue;
930
+ candidates.push({ sid, norm: normalizeUrlForMatch(frameUrl) });
931
+ }
932
+ const exact = candidates.filter((c) => c.norm && c.norm.href === target.href);
933
+ if (exact.length === 1) return { kind: "ok", sessionIds: collectDescendants(tabId, exact[0].sid) };
934
+ if (exact.length > 1) return { kind: "ambiguous", candidates: exact.map((c) => c.sid) };
935
+ const loose = candidates.filter((c) => c.norm && c.norm.origin === target.origin && c.norm.pathname === target.pathname);
936
+ if (loose.length === 1) return { kind: "ok", sessionIds: collectDescendants(tabId, loose[0].sid) };
937
+ if (loose.length > 1) return { kind: "ambiguous", candidates: loose.map((c) => c.sid) };
938
+ const sameOrigin = candidates.filter((c) => c.norm && c.norm.origin === target.origin);
939
+ if (sameOrigin.length === 1) return { kind: "ok", sessionIds: collectDescendants(tabId, sameOrigin[0].sid) };
940
+ if (sameOrigin.length > 1) return { kind: "ambiguous", candidates: sameOrigin.map((c) => c.sid) };
941
+ const all = /* @__PURE__ */ new Set();
942
+ for (const sid of states.keys()) for (const d of collectDescendants(tabId, sid)) all.add(d);
943
+ if (all.size > 0) return { kind: "ok", sessionIds: all };
944
+ return { kind: "none" };
945
+ }
946
+ function collectDescendants(tabId, rootSid) {
947
+ const out = /* @__PURE__ */ new Set([rootSid]);
948
+ const states = armedChildSessions.get(tabId);
949
+ if (!states) return out;
950
+ let grew = true;
951
+ while (grew) {
952
+ grew = false;
953
+ for (const sid of states.keys()) {
954
+ if (out.has(sid)) continue;
955
+ const parent = sessionToParent.get(sid);
956
+ if (parent !== void 0 && out.has(parent)) {
957
+ out.add(sid);
958
+ grew = true;
959
+ }
960
+ }
961
+ }
962
+ return out;
963
+ }
964
+ function applyFrameFilter(tabId, items, filter) {
965
+ const resolve = resolveTargetFrameSessions(tabId, filter.targetFrameUrl);
966
+ if (resolve.kind !== "ok") return { items: [], resolve };
967
+ const allow = resolve.sessionIds;
968
+ return { items: items.filter((it) => it.frameSessionId !== void 0 && allow.has(it.frameSessionId)), resolve };
969
+ }
970
+ function hasActiveNetworkCapture(tabId) {
971
+ return networkCaptures.has(tabId) || imaReaderCaptureTabs.has(tabId);
972
+ }
973
+ async function startUiCapture(tabId) {
974
+ await ensureAttached(tabId);
975
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
976
+ await chrome.debugger.sendCommand({ tabId }, "Page.enable");
977
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.addBinding", { name: UI_BINDING_NAME });
978
+ await chrome.debugger.sendCommand({ tabId }, "Page.addScriptToEvaluateOnNewDocument", { source: UI_LISTENER_SOURCE });
979
+ try {
980
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { expression: UI_LISTENER_SOURCE });
981
+ } catch {
982
+ }
983
+ uiCaptures.set(tabId, { events: [], dropped: 0 });
984
+ await enableIframeAutoAttach(tabId);
985
+ await rearmChildSessions(tabId);
986
+ }
987
+ async function readUiCapture(tabId, filter) {
988
+ const state = uiCaptures.get(tabId);
989
+ if (!state) return { events: [], dropped: 0 };
990
+ let events = state.events.slice();
991
+ if (filter) {
992
+ const r = applyFrameFilter(tabId, events, filter);
993
+ if (r.resolve.kind === "ambiguous") throw new AmbiguousIframeTargetError(r.resolve.candidates.length);
994
+ events = r.items;
995
+ }
996
+ const out = { events, dropped: state.dropped };
997
+ state.events = [];
998
+ state.dropped = 0;
999
+ return out;
1000
+ }
1001
+ const fetchGuards = /* @__PURE__ */ new Map();
1002
+ let fetchListenerRegistered = false;
1003
+ function ensureFetchListener() {
1004
+ if (fetchListenerRegistered) return;
1005
+ fetchListenerRegistered = true;
1006
+ chrome.debugger.onEvent.addListener((source, method, params) => {
1007
+ const tabId = source.tabId;
1008
+ if (!tabId) return;
1009
+ const guard = fetchGuards.get(tabId);
1010
+ if (!guard) return;
1011
+ const p = params;
1012
+ if (method === "Fetch.requestPaused") {
1013
+ const requestId = p?.requestId;
1014
+ const url = p?.request?.url ?? "";
1015
+ const resourceType = p?.resourceType;
1016
+ if (imaReaderCaptureTabs.has(tabId) && isImaReaderRequest(url)) return;
1017
+ if (resourceType !== "Document") {
1018
+ chrome.debugger.sendCommand({ tabId }, "Fetch.continueRequest", { requestId }).catch(() => {
1019
+ });
1020
+ return;
1021
+ }
1022
+ if (guard.allow(url)) {
1023
+ chrome.debugger.sendCommand({ tabId }, "Fetch.continueRequest", { requestId }).catch(() => {
1024
+ });
1025
+ } else {
1026
+ guard.blocked.push(url);
1027
+ chrome.debugger.sendCommand({ tabId }, "Fetch.failRequest", { requestId, errorReason: "BlockedByClient" }).catch(() => {
1028
+ });
1029
+ }
1030
+ return;
1031
+ }
1032
+ if (method === "Network.responseReceived") {
1033
+ const ip = p?.response?.remoteIPAddress;
1034
+ if (ip) guard.observedIps.push(ip);
1035
+ }
1036
+ });
1037
+ }
1038
+ async function armFetchGuard(tabId, allow, aggressiveRetry = false) {
1039
+ await ensureAttached(tabId, aggressiveRetry);
1040
+ ensureFetchListener();
1041
+ const state = { allow, blocked: [], observedIps: [] };
1042
+ fetchGuards.set(tabId, state);
1043
+ try {
1044
+ await chrome.debugger.sendCommand({ tabId }, "Network.enable").catch(() => {
1045
+ });
1046
+ await chrome.debugger.sendCommand({ tabId }, "Fetch.enable", {
1047
+ patterns: [
1048
+ { urlPattern: "*", resourceType: "Document", requestStage: "Request" },
1049
+ ...imaReaderCaptureTabs.has(tabId) ? [{
1050
+ urlPattern: "https://ima.qq.com/cgi-bin/knowledge_tab_reader/*",
1051
+ requestStage: "Request"
1052
+ }, {
1053
+ urlPattern: "https://ima.qq.com/cgi-bin/activity_tab/get_available_activities",
1054
+ requestStage: "Request"
1055
+ }] : []
1056
+ ]
1057
+ });
1058
+ } catch (e) {
1059
+ fetchGuards.delete(tabId);
1060
+ throw e;
1061
+ }
1062
+ return state;
1063
+ }
1064
+ function clearFrameTargetsForTab(tabId) {
1065
+ for (const [key, targetId] of [...frameTargets.entries()]) {
1066
+ if (!key.startsWith(`${tabId}:`)) continue;
1067
+ frameTargets.delete(key);
1068
+ frameTargetKeys.delete(targetId);
1069
+ chrome.debugger.detach({ targetId }).catch(() => {
1070
+ });
1071
+ }
1072
+ }
1073
+ function clearChildSessionsForTab(tabId) {
1074
+ tabCaptureGeneration.set(tabId, (tabCaptureGeneration.get(tabId) ?? 0) + 1);
1075
+ const states = armedChildSessions.get(tabId);
1076
+ if (states) {
1077
+ for (const sid of states.keys()) {
1078
+ sessionToTab.delete(sid);
1079
+ sessionToFrameUrl.delete(sid);
1080
+ sessionToParent.delete(sid);
1081
+ }
1082
+ armedChildSessions.delete(tabId);
1083
+ }
1084
+ }
1085
+ async function detach(tabId) {
1086
+ clearFrameTargetsForTab(tabId);
1087
+ clearChildSessionsForTab(tabId);
1088
+ networkCaptures.delete(tabId);
1089
+ imaReaderCaptureTabs.delete(tabId);
1090
+ imaReaderRequestUrls.delete(tabId);
1091
+ uiCaptures.delete(tabId);
1092
+ fetchGuards.delete(tabId);
1093
+ tabFrameContexts.delete(tabId);
1094
+ if (!attached.has(tabId)) return;
1095
+ attached.delete(tabId);
1096
+ try {
1097
+ await chrome.debugger.detach({ tabId });
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ function registerListeners() {
1102
+ chrome.tabs.onRemoved.addListener((tabId) => {
1103
+ attached.delete(tabId);
1104
+ networkCaptures.delete(tabId);
1105
+ imaReaderCaptureTabs.delete(tabId);
1106
+ imaReaderRequestUrls.delete(tabId);
1107
+ uiCaptures.delete(tabId);
1108
+ fetchGuards.delete(tabId);
1109
+ tabFrameContexts.delete(tabId);
1110
+ clearFrameTargetsForTab(tabId);
1111
+ clearChildSessionsForTab(tabId);
1112
+ });
1113
+ chrome.debugger.onDetach.addListener((source) => {
1114
+ if (source.tabId) {
1115
+ attached.delete(source.tabId);
1116
+ networkCaptures.delete(source.tabId);
1117
+ imaReaderCaptureTabs.delete(source.tabId);
1118
+ imaReaderRequestUrls.delete(source.tabId);
1119
+ uiCaptures.delete(source.tabId);
1120
+ fetchGuards.delete(source.tabId);
1121
+ tabFrameContexts.delete(source.tabId);
1122
+ clearFrameTargetsForTab(source.tabId);
1123
+ clearChildSessionsForTab(source.tabId);
1124
+ return;
1125
+ }
1126
+ if (source.targetId) clearFrameTarget(source.targetId);
1127
+ });
1128
+ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
1129
+ if (info.url && !isDebuggableUrl$1(info.url)) {
1130
+ await detach(tabId);
1131
+ }
1132
+ });
1133
+ chrome.debugger.onEvent.addListener(async (source, method, params) => {
1134
+ if (method === "Target.attachedToTarget") {
1135
+ const ap = params;
1136
+ const childSessionId = ap?.sessionId;
1137
+ const parentTabId = source.tabId;
1138
+ if (!childSessionId || !parentTabId) return;
1139
+ if (ap?.targetInfo?.type !== "iframe") {
1140
+ await sendSafe(childDebuggee(parentTabId, childSessionId), "Runtime.runIfWaitingForDebugger", {});
1141
+ return;
1142
+ }
1143
+ if (typeof ap.targetInfo.url === "string") {
1144
+ sessionToFrameUrl.set(childSessionId, maskUrlAuthTokens(ap.targetInfo.url));
1145
+ }
1146
+ sessionToParent.set(childSessionId, sessionIdOf(source) ?? "");
1147
+ await armChildSession(parentTabId, childSessionId);
1148
+ return;
1149
+ }
1150
+ if (method === "Target.detachedFromTarget") {
1151
+ const dp = params;
1152
+ const sid = dp?.sessionId;
1153
+ if (sid) {
1154
+ const owner = sessionToTab.get(sid);
1155
+ sessionToTab.delete(sid);
1156
+ sessionToFrameUrl.delete(sid);
1157
+ sessionToParent.delete(sid);
1158
+ if (owner !== void 0) armedChildSessions.get(owner)?.delete(sid);
1159
+ }
1160
+ return;
1161
+ }
1162
+ if (method === "Target.targetInfoChanged") {
1163
+ const cp = params;
1164
+ const ti = cp?.targetInfo;
1165
+ const sid = sessionIdOf(source);
1166
+ if (sid && ti?.type === "iframe" && typeof ti.url === "string" && ti.url) {
1167
+ sessionToFrameUrl.set(sid, maskUrlAuthTokens(ti.url));
1168
+ }
1169
+ return;
1170
+ }
1171
+ const eventSessionId = sessionIdOf(source);
1172
+ const tabId = source.tabId ?? (eventSessionId ? sessionToTab.get(eventSessionId) : void 0);
1173
+ if (!tabId) return;
1174
+ const sessionId = eventSessionId;
1175
+ if (method === "Runtime.bindingCalled") {
1176
+ const bp = params;
1177
+ if (bp?.name === IMA_READER_AUTH_BINDING) {
1178
+ if (imaReaderCaptureTabs.has(tabId)) {
1179
+ try {
1180
+ const request = JSON.parse(String(bp.payload ?? ""));
1181
+ imaReaderAuthStore.capture(tabId, request);
1182
+ } catch {
1183
+ }
1184
+ }
1185
+ return;
1186
+ }
1187
+ if (bp?.name === UI_BINDING_NAME) {
1188
+ const ui = uiCaptures.get(tabId);
1189
+ if (ui) {
1190
+ const ev = parseUiEvent(String(bp.payload ?? ""));
1191
+ if (ev) {
1192
+ if (sessionId) {
1193
+ ev.frameSessionId = sessionId;
1194
+ const fu = sessionToFrameUrl.get(sessionId);
1195
+ if (fu) ev.frameUrl = fu;
1196
+ }
1197
+ if (ui.events.length < MAX_UI_EVENTS) ui.events.push(ev);
1198
+ else ui.dropped++;
1199
+ }
1200
+ }
1201
+ }
1202
+ return;
1203
+ }
1204
+ const eventParams = params;
1205
+ const state = networkCaptures.get(tabId);
1206
+ if (method === "Network.requestWillBeSent") {
1207
+ const request = eventParams?.request;
1208
+ if (isImaReaderRequest(request?.url)) {
1209
+ const requestKey = reqKey(sessionId, String(eventParams?.requestId || ""));
1210
+ let requests = imaReaderRequestUrls.get(tabId);
1211
+ if (!requests) {
1212
+ requests = /* @__PURE__ */ new Map();
1213
+ imaReaderRequestUrls.set(tabId, requests);
1214
+ }
1215
+ requests.set(requestKey, request?.url ?? "");
1216
+ if (imaReaderCaptureTabs.has(tabId) && imaReaderAuthStore.capture(tabId, request ?? {})) {
1217
+ requests.delete(requestKey);
1218
+ }
1219
+ return;
1220
+ }
1221
+ if (!state) return;
1222
+ if (!isApiResourceType(eventParams?.type)) return;
1223
+ const requestId = String(eventParams?.requestId || "");
1224
+ const entry = getOrCreateNetworkCaptureEntry(tabId, reqKey(sessionId, requestId), {
1225
+ url: maskUrlAuthTokens(request?.url),
1226
+ method: request?.method,
1227
+ requestHeaders: normalizeHeaders(request?.headers)
1228
+ });
1229
+ if (!entry) return;
1230
+ if (typeof eventParams?.type === "string") entry.resourceType = eventParams.type;
1231
+ const initiator = eventParams?.initiator;
1232
+ if (initiator?.type) entry.initiatorType = initiator.type;
1233
+ const frameId = eventParams?.frameId;
1234
+ if (typeof frameId === "string") entry.frameId = frameId;
1235
+ if (sessionId) entry.frameSessionId = sessionId;
1236
+ if (sessionId) {
1237
+ const fu = sessionToFrameUrl.get(sessionId);
1238
+ if (fu) entry.frameUrl = fu;
1239
+ }
1240
+ entry.requestBodyKind = request?.hasPostData ? "string" : "empty";
1241
+ {
1242
+ const raw = String(request?.postData || "");
1243
+ const fullSize = raw.length;
1244
+ const truncated = fullSize > CDP_REQUEST_BODY_CAPTURE_LIMIT;
1245
+ entry.requestBodyPreview = truncated ? raw.slice(0, CDP_REQUEST_BODY_CAPTURE_LIMIT) : raw;
1246
+ entry.requestBodyFullSize = fullSize;
1247
+ entry.requestBodyTruncated = truncated;
1248
+ }
1249
+ try {
1250
+ const postData = await chrome.debugger.sendCommand(source, "Network.getRequestPostData", { requestId });
1251
+ if (postData?.postData) {
1252
+ const raw = postData.postData;
1253
+ const fullSize = raw.length;
1254
+ const truncated = fullSize > CDP_REQUEST_BODY_CAPTURE_LIMIT;
1255
+ entry.requestBodyKind = "string";
1256
+ entry.requestBodyPreview = truncated ? raw.slice(0, CDP_REQUEST_BODY_CAPTURE_LIMIT) : raw;
1257
+ entry.requestBodyFullSize = fullSize;
1258
+ entry.requestBodyTruncated = truncated;
1259
+ }
1260
+ } catch {
1261
+ }
1262
+ return;
1263
+ }
1264
+ if (method === "Network.requestWillBeSentExtraInfo") {
1265
+ const requestKey = reqKey(sessionId, String(eventParams?.requestId || ""));
1266
+ const requests = imaReaderRequestUrls.get(tabId);
1267
+ const url = requests?.get(requestKey);
1268
+ if (url && imaReaderCaptureTabs.has(tabId) && imaReaderAuthStore.capture(tabId, { url, headers: eventParams?.headers })) {
1269
+ requests?.delete(requestKey);
1270
+ }
1271
+ return;
1272
+ }
1273
+ if (!state) return;
1274
+ if (method === "Network.responseReceived") {
1275
+ const requestId = String(eventParams?.requestId || "");
1276
+ const response = eventParams?.response;
1277
+ const stateEntryIndex = state.requestToIndex.get(reqKey(sessionId, requestId));
1278
+ if (stateEntryIndex === void 0) return;
1279
+ const entry = state.entries[stateEntryIndex];
1280
+ if (!entry) return;
1281
+ entry.responseStatus = response?.status;
1282
+ entry.responseContentType = response?.mimeType || "";
1283
+ entry.responseHeaders = normalizeHeaders(response?.headers);
1284
+ return;
1285
+ }
1286
+ if (method === "Network.loadingFinished") {
1287
+ const requestId = String(eventParams?.requestId || "");
1288
+ const stateEntryIndex = state.requestToIndex.get(reqKey(sessionId, requestId));
1289
+ if (stateEntryIndex === void 0) return;
1290
+ const entry = state.entries[stateEntryIndex];
1291
+ if (!entry) return;
1292
+ try {
1293
+ const body = await chrome.debugger.sendCommand(source, "Network.getResponseBody", { requestId });
1294
+ if (typeof body?.body === "string") {
1295
+ const fullSize = body.body.length;
1296
+ const truncated = fullSize > CDP_RESPONSE_BODY_CAPTURE_LIMIT;
1297
+ const stored = truncated ? body.body.slice(0, CDP_RESPONSE_BODY_CAPTURE_LIMIT) : body.body;
1298
+ entry.responsePreview = body.base64Encoded ? `base64:${stored}` : stored;
1299
+ entry.responseBodyFullSize = fullSize;
1300
+ entry.responseBodyTruncated = truncated;
1301
+ }
1302
+ } catch {
1303
+ }
1304
+ return;
1305
+ }
1306
+ if (method === "Network.webSocketCreated") {
1307
+ const requestId = String(eventParams?.requestId || "");
1308
+ const wsEntry = getOrCreateNetworkCaptureEntry(
1309
+ tabId,
1310
+ reqKey(sessionId, requestId),
1311
+ { url: maskUrlAuthTokens(eventParams?.url), method: "GET" },
1312
+ "cdp-websocket"
1313
+ );
1314
+ if (wsEntry) wsEntry.resourceType = "WebSocket";
1315
+ if (wsEntry && sessionId) {
1316
+ wsEntry.frameSessionId = sessionId;
1317
+ const fu = sessionToFrameUrl.get(sessionId);
1318
+ if (fu) wsEntry.frameUrl = fu;
1319
+ }
1320
+ return;
1321
+ }
1322
+ if (method === "Network.webSocketHandshakeResponseReceived") {
1323
+ const requestId = String(eventParams?.requestId || "");
1324
+ const idx = state.requestToIndex.get(reqKey(sessionId, requestId));
1325
+ if (idx === void 0) return;
1326
+ const entry = state.entries[idx];
1327
+ if (!entry) return;
1328
+ const response = eventParams?.response;
1329
+ entry.responseStatus = response?.status ?? 101;
1330
+ entry.responseHeaders = normalizeHeaders(response?.headers);
1331
+ return;
1332
+ }
1333
+ if (method === "Network.webSocketFrameSent" || method === "Network.webSocketFrameReceived") {
1334
+ const requestId = String(eventParams?.requestId || "");
1335
+ const idx = state.requestToIndex.get(reqKey(sessionId, requestId));
1336
+ if (idx === void 0) return;
1337
+ const entry = state.entries[idx];
1338
+ if (!entry) return;
1339
+ const frame = eventParams?.response;
1340
+ const opcode = typeof frame?.opcode === "number" ? frame.opcode : -1;
1341
+ if (opcode !== 1 && opcode !== 2) return;
1342
+ if (!entry.webSocketFrames) entry.webSocketFrames = [];
1343
+ if (entry.webSocketFrames.length >= MAX_WS_FRAMES_PER_CONN) {
1344
+ entry.webSocketFramesDropped = (entry.webSocketFramesDropped ?? 0) + 1;
1345
+ return;
1346
+ }
1347
+ const raw = String(frame?.payloadData ?? "");
1348
+ const fullSize = raw.length;
1349
+ const truncated = fullSize > CDP_WS_FRAME_CAPTURE_LIMIT;
1350
+ const stored = truncated ? raw.slice(0, CDP_WS_FRAME_CAPTURE_LIMIT) : raw;
1351
+ entry.webSocketFrames.push({
1352
+ direction: method === "Network.webSocketFrameSent" ? "sent" : "received",
1353
+ opcode,
1354
+ payloadPreview: opcode === 2 ? `base64:${stored}` : stored,
1355
+ payloadFullSize: fullSize,
1356
+ payloadTruncated: truncated,
1357
+ timestamp: Date.now()
1358
+ });
1359
+ return;
1360
+ }
1361
+ });
1362
+ }
1363
+
1364
+ const targetToTab = /* @__PURE__ */ new Map();
1365
+ const tabToTarget = /* @__PURE__ */ new Map();
1366
+ async function resolveTargetId(tabId) {
1367
+ const cached = tabToTarget.get(tabId);
1368
+ if (cached) return cached;
1369
+ await refreshMappings();
1370
+ const result = tabToTarget.get(tabId);
1371
+ if (!result) throw new Error(`No targetId for tab ${tabId} — page may have been closed`);
1372
+ return result;
1373
+ }
1374
+ async function resolveTabId$1(targetId) {
1375
+ const cached = targetToTab.get(targetId);
1376
+ if (cached !== void 0) return cached;
1377
+ await refreshMappings();
1378
+ const result = targetToTab.get(targetId);
1379
+ if (result === void 0) throw new Error(`Page not found: ${targetId} — stale page identity`);
1380
+ return result;
1381
+ }
1382
+ function evictTab(tabId) {
1383
+ const targetId = tabToTarget.get(tabId);
1384
+ if (targetId) targetToTab.delete(targetId);
1385
+ tabToTarget.delete(tabId);
1386
+ }
1387
+ async function refreshMappings() {
1388
+ const targets = await chrome.debugger.getTargets();
1389
+ targetToTab.clear();
1390
+ tabToTarget.clear();
1391
+ for (const t of targets) {
1392
+ if (t.type === "page" && t.tabId !== void 0) {
1393
+ targetToTab.set(t.id, t.tabId);
1394
+ tabToTarget.set(t.tabId, t.id);
1395
+ }
1396
+ }
1397
+ }
1398
+
1399
+ const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
1400
+ function classifyIp(s) {
1401
+ if (/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(s)) {
1402
+ if (s.split(".").every((o) => Number(o) <= 255)) return 4;
1403
+ }
1404
+ if (s.includes(":") && /^[0-9a-fA-F:.]+$/.test(s) && ipv6Groups(s) !== null) return 6;
1405
+ return 0;
1406
+ }
1407
+ function parseIpv4Loose(host) {
1408
+ if (host.length === 0) return null;
1409
+ const parts = host.split(".");
1410
+ if (parts.length > 4) return null;
1411
+ if (parts.some((p) => p.length === 0)) return null;
1412
+ const numbers = [];
1413
+ for (const part of parts) {
1414
+ const n = parseRadixPart(part);
1415
+ if (n === null) return null;
1416
+ numbers.push(n);
1417
+ }
1418
+ for (let i = 0; i < numbers.length - 1; i++) {
1419
+ if (numbers[i] > 255) return null;
1420
+ }
1421
+ const last = numbers[numbers.length - 1];
1422
+ const maxLast = 256 ** (5 - numbers.length);
1423
+ if (last >= maxLast) return null;
1424
+ let ipv4 = last;
1425
+ for (let i = 0; i < numbers.length - 1; i++) ipv4 += numbers[i] * 256 ** (3 - i);
1426
+ if (ipv4 > 4294967295) return null;
1427
+ return [ipv4 >>> 24 & 255, ipv4 >>> 16 & 255, ipv4 >>> 8 & 255, ipv4 & 255].join(".");
1428
+ }
1429
+ function parseRadixPart(part) {
1430
+ let radix = 10;
1431
+ let digits = part;
1432
+ if (/^0[xX]/.test(part)) {
1433
+ radix = 16;
1434
+ digits = part.slice(2);
1435
+ if (digits.length === 0 || !/^[0-9a-fA-F]+$/.test(digits)) return null;
1436
+ } else if (part.length > 1 && part[0] === "0") {
1437
+ radix = 8;
1438
+ digits = part.slice(1);
1439
+ if (!/^[0-7]+$/.test(digits)) return null;
1440
+ } else if (!/^[0-9]+$/.test(part)) {
1441
+ return null;
1442
+ }
1443
+ const n = parseInt(digits, radix);
1444
+ return Number.isNaN(n) ? null : n;
1445
+ }
1446
+ function asIpLiteral(host) {
1447
+ const unbracketed = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
1448
+ const v = classifyIp(unbracketed);
1449
+ if (v === 4 || v === 6) return unbracketed;
1450
+ return parseIpv4Loose(host);
1451
+ }
1452
+ const FORBIDDEN_IPV4 = [
1453
+ ["0.0.0.0", 8],
1454
+ ["10.0.0.0", 8],
1455
+ ["100.64.0.0", 10],
1456
+ ["127.0.0.0", 8],
1457
+ ["169.254.0.0", 16],
1458
+ ["172.16.0.0", 12],
1459
+ ["192.0.0.0", 24],
1460
+ ["192.0.2.0", 24],
1461
+ ["192.168.0.0", 16],
1462
+ ["198.18.0.0", 15],
1463
+ ["198.51.100.0", 24],
1464
+ ["203.0.113.0", 24],
1465
+ ["224.0.0.0", 4],
1466
+ ["240.0.0.0", 4]
1467
+ ];
1468
+ function ipv4ToInt(ip) {
1469
+ const p = ip.split(".").map((x) => parseInt(x, 10));
1470
+ return (p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]) >>> 0;
1471
+ }
1472
+ function ipv4InForbiddenRange(ip) {
1473
+ const addr = ipv4ToInt(ip);
1474
+ for (const [net, bits] of FORBIDDEN_IPV4) {
1475
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
1476
+ if ((addr & mask) === (ipv4ToInt(net) & mask)) return true;
1477
+ }
1478
+ return false;
1479
+ }
1480
+ function ipv6Groups(ip) {
1481
+ let addr = ip;
1482
+ const lastColon = addr.lastIndexOf(":");
1483
+ const tail = addr.slice(lastColon + 1);
1484
+ if (tail.includes(".")) {
1485
+ if (classifyIp(tail) !== 4) return null;
1486
+ const v4 = ipv4ToInt(tail);
1487
+ addr = addr.slice(0, lastColon + 1) + (v4 >>> 16 & 65535).toString(16) + ":" + (v4 & 65535).toString(16);
1488
+ }
1489
+ const halves = addr.split("::");
1490
+ if (halves.length > 2) return null;
1491
+ const head = halves[0].length ? halves[0].split(":") : [];
1492
+ const back = halves.length === 2 ? halves[1].length ? halves[1].split(":") : [] : null;
1493
+ const parse = (groups) => {
1494
+ const out = [];
1495
+ for (const g of groups) {
1496
+ if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
1497
+ out.push(parseInt(g, 16));
1498
+ }
1499
+ return out;
1500
+ };
1501
+ const headNums = parse(head);
1502
+ if (headNums === null) return null;
1503
+ if (back === null) return headNums.length === 8 ? headNums : null;
1504
+ const backNums = parse(back);
1505
+ if (backNums === null) return null;
1506
+ const fill = 8 - headNums.length - backNums.length;
1507
+ if (fill < 0) return null;
1508
+ return [...headNums, ...new Array(fill).fill(0), ...backNums];
1509
+ }
1510
+ function ipv6InForbiddenRange(ip) {
1511
+ const g = ipv6Groups(ip);
1512
+ if (!g) return false;
1513
+ if (g.every((x) => x === 0)) return true;
1514
+ if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return true;
1515
+ if (g.slice(0, 5).every((x) => x === 0) && g[5] === 65535) {
1516
+ const v4 = `${g[6] >> 8 & 255}.${g[6] & 255}.${g[7] >> 8 & 255}.${g[7] & 255}`;
1517
+ return ipv4InForbiddenRange(v4);
1518
+ }
1519
+ const first = g[0];
1520
+ if ((first & 65024) === 64512) return true;
1521
+ if ((first & 65472) === 65152) return true;
1522
+ if ((first & 65280) === 65280) return true;
1523
+ return false;
1524
+ }
1525
+ function isForbiddenIp(ip) {
1526
+ const v = classifyIp(ip);
1527
+ if (v === 4) return ipv4InForbiddenRange(ip);
1528
+ if (v === 6) return ipv6InForbiddenRange(ip);
1529
+ const canon = parseIpv4Loose(ip);
1530
+ return canon ? ipv4InForbiddenRange(canon) : false;
1531
+ }
1532
+ function checkUrlSyntax(input, opts = {}) {
1533
+ let parsed;
1534
+ try {
1535
+ parsed = new URL(input);
1536
+ } catch {
1537
+ return { ok: false, reason: "empty_host", detail: "unparseable URL" };
1538
+ }
1539
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
1540
+ return { ok: false, reason: "forbidden_protocol", detail: `protocol ${parsed.protocol} not allowed` };
1541
+ }
1542
+ if (parsed.username !== "" || parsed.password !== "") {
1543
+ return { ok: false, reason: "userinfo_present", detail: "userinfo (user:pass@) not allowed" };
1544
+ }
1545
+ let hostname = parsed.hostname;
1546
+ if (hostname.endsWith(".")) hostname = hostname.slice(0, -1);
1547
+ if (hostname === "") return { ok: false, reason: "empty_host", detail: "empty host" };
1548
+ const ipLiteral = asIpLiteral(hostname);
1549
+ if (ipLiteral !== null) {
1550
+ if (isForbiddenIp(ipLiteral)) {
1551
+ return { ok: false, reason: "forbidden_ip", detail: `literal IP ${ipLiteral} is forbidden` };
1552
+ }
1553
+ if (!opts.allowLiteralIp) {
1554
+ return { ok: false, reason: "literal_ip_host", detail: `literal IP host ${ipLiteral} not allowed for navigation` };
1555
+ }
1556
+ return { ok: true, url: parsed.toString(), hostname: ipLiteral, isIpLiteral: true };
1557
+ }
1558
+ return { ok: true, url: parsed.toString(), hostname, isIpLiteral: false };
1559
+ }
1560
+
1561
+ const EXTENSION_CAPABILITIES = ["focus-window-v1", "ima-reader-v1"];
1562
+ let ws = null;
1563
+ let reconnectTimer = null;
1564
+ let reconnectAttempts = 0;
1565
+ const CONTEXT_ID_KEY = "bycli_context_id_v1";
1566
+ let currentContextId = "default";
1567
+ let contextIdPromise = null;
1568
+ let connectInFlight = null;
1569
+ async function getCurrentContextId() {
1570
+ if (contextIdPromise) return contextIdPromise;
1571
+ contextIdPromise = (async () => {
1572
+ try {
1573
+ const local = chrome.storage?.local;
1574
+ if (!local) return currentContextId;
1575
+ const raw = await local.get(CONTEXT_ID_KEY);
1576
+ const existing = raw[CONTEXT_ID_KEY];
1577
+ if (typeof existing === "string" && existing.trim()) {
1578
+ currentContextId = existing.trim();
1579
+ return currentContextId;
1580
+ }
1581
+ const generated = generateContextId();
1582
+ await local.set({ [CONTEXT_ID_KEY]: generated });
1583
+ currentContextId = generated;
1584
+ return currentContextId;
1585
+ } catch {
1586
+ return currentContextId;
1587
+ }
1588
+ })();
1589
+ return contextIdPromise;
1590
+ }
1591
+ function generateContextId() {
1592
+ const alphabet = "23456789abcdefghjkmnpqrstuvwxyz";
1593
+ const maxUnbiasedByte = Math.floor(256 / alphabet.length) * alphabet.length;
1594
+ let id = "";
1595
+ while (id.length < 8) {
1596
+ const bytes = new Uint8Array(8);
1597
+ try {
1598
+ crypto.getRandomValues(bytes);
1599
+ } catch {
1600
+ for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1601
+ }
1602
+ for (const byte of bytes) {
1603
+ if (byte >= maxUnbiasedByte) continue;
1604
+ id += alphabet[byte % alphabet.length];
1605
+ if (id.length === 8) break;
1606
+ }
1607
+ }
1608
+ return id;
1609
+ }
1610
+ const _origLog = console.log.bind(console);
1611
+ const _origWarn = console.warn.bind(console);
1612
+ const _origError = console.error.bind(console);
1613
+ function forwardLog(level, args) {
1614
+ try {
1615
+ const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
1616
+ safeSend(ws, { type: "log", level, msg, ts: Date.now() });
1617
+ } catch {
1618
+ }
1619
+ }
1620
+ function safeSend(socket, payload) {
1621
+ if (!socket || socket.readyState !== WebSocket.OPEN) return false;
1622
+ try {
1623
+ socket.send(JSON.stringify(payload));
1624
+ return true;
1625
+ } catch {
1626
+ return false;
1627
+ }
1628
+ }
1629
+ console.log = (...args) => {
1630
+ _origLog(...args);
1631
+ forwardLog("info", args);
1632
+ };
1633
+ console.warn = (...args) => {
1634
+ _origWarn(...args);
1635
+ forwardLog("warn", args);
1636
+ };
1637
+ console.error = (...args) => {
1638
+ _origError(...args);
1639
+ forwardLog("error", args);
1640
+ };
1641
+ function isDaemonSocketActive(socket = ws) {
1642
+ return socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING;
1643
+ }
1644
+ function connect() {
1645
+ if (isDaemonSocketActive()) return Promise.resolve();
1646
+ if (connectInFlight) return connectInFlight;
1647
+ connectInFlight = connectAttempt().finally(() => {
1648
+ connectInFlight = null;
1649
+ });
1650
+ return connectInFlight;
1651
+ }
1652
+ async function connectAttempt() {
1653
+ if (isDaemonSocketActive()) return;
1654
+ try {
1655
+ const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) });
1656
+ if (!res.ok) return;
1657
+ } catch {
1658
+ return;
1659
+ }
1660
+ if (isDaemonSocketActive()) return;
1661
+ let thisWs;
1662
+ try {
1663
+ const contextId = await getCurrentContextId();
1664
+ if (isDaemonSocketActive()) return;
1665
+ thisWs = new WebSocket(DAEMON_WS_URL);
1666
+ ws = thisWs;
1667
+ currentContextId = contextId;
1668
+ } catch {
1669
+ scheduleReconnect();
1670
+ return;
1671
+ }
1672
+ thisWs.onopen = () => {
1673
+ if (ws !== thisWs) return;
1674
+ console.log("[bycli] Connected to daemon");
1675
+ reconnectAttempts = 0;
1676
+ if (reconnectTimer) {
1677
+ clearTimeout(reconnectTimer);
1678
+ reconnectTimer = null;
1679
+ }
1680
+ safeSend(thisWs, {
1681
+ type: "hello",
1682
+ contextId: currentContextId,
1683
+ version: chrome.runtime.getManifest().version,
1684
+ compatRange: ">=1.7.0",
1685
+ capabilities: EXTENSION_CAPABILITIES
1686
+ });
1687
+ };
1688
+ thisWs.onmessage = async (event) => {
1689
+ if (ws !== thisWs) return;
1690
+ let commandId;
1691
+ try {
1692
+ const parsed = JSON.parse(event.data);
1693
+ commandId = typeof parsed.id === "string" ? parsed.id : void 0;
1694
+ const result = await handleCommand(parsed);
1695
+ if (ws !== thisWs) return;
1696
+ safeSend(thisWs, result);
1697
+ } catch (err) {
1698
+ console.error("[bycli] Message handling error:", err);
1699
+ if (ws === thisWs && commandId) {
1700
+ safeSend(thisWs, {
1701
+ id: commandId,
1702
+ ok: false,
1703
+ error: err instanceof Error ? err.message : String(err)
1704
+ });
1705
+ }
1706
+ }
1707
+ };
1708
+ thisWs.onclose = () => {
1709
+ if (ws !== thisWs) return;
1710
+ console.log("[bycli] Disconnected from daemon");
1711
+ ws = null;
1712
+ scheduleReconnect();
1713
+ };
1714
+ thisWs.onerror = () => {
1715
+ thisWs.close();
1716
+ };
1717
+ }
1718
+ const MAX_EAGER_ATTEMPTS = 6;
1719
+ function scheduleReconnect() {
1720
+ if (reconnectTimer) return;
1721
+ reconnectAttempts++;
1722
+ if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return;
1723
+ const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
1724
+ reconnectTimer = setTimeout(() => {
1725
+ reconnectTimer = null;
1726
+ void connect();
1727
+ }, delay);
1728
+ }
1729
+ const automationSessions = /* @__PURE__ */ new Map();
1730
+ const leaseRevisions = /* @__PURE__ */ new Map();
1731
+ const leaseIdleGenerations = /* @__PURE__ */ new Map();
1732
+ const leaseOperationTails = /* @__PURE__ */ new Map();
1733
+ const IDLE_TIMEOUT_DEFAULT = 3e4;
1734
+ const IDLE_TIMEOUT_INTERACTIVE = 6e5;
1735
+ const IDLE_TIMEOUT_NONE = -1;
1736
+ const REGISTRY_KEY = "bycli_target_lease_registry_v2";
1737
+ const LEASE_IDLE_ALARM_PREFIX = "bycli:lease-idle:";
1738
+ const CONTAINER_TAB_GROUP_TITLE = {
1739
+ interactive: "byCLI Browser",
1740
+ automation: "byCLI Adapter"
1741
+ };
1742
+ const LEGACY_AUTOMATION_TAB_GROUP_TITLE = "byCLI";
1743
+ const AUTOMATION_TAB_GROUP_COLOR = "orange";
1744
+ let leaseMutationQueue = Promise.resolve();
1745
+ const ownedContainers = {
1746
+ interactive: { windowId: null, groupId: null, promise: null, groupPromise: null },
1747
+ automation: { windowId: null, groupId: null, promise: null, groupPromise: null }
1748
+ };
1749
+ class CommandFailure extends Error {
1750
+ constructor(code, message, hint) {
1751
+ super(message);
1752
+ this.code = code;
1753
+ this.hint = hint;
1754
+ this.name = "CommandFailure";
1755
+ }
1756
+ }
1757
+ const sessionTimeoutOverrides = /* @__PURE__ */ new Map();
1758
+ const sessionWindowModeOverrides = /* @__PURE__ */ new Map();
1759
+ const sessionLifecycleOverrides = /* @__PURE__ */ new Map();
1760
+ const LEASE_KEY_SEPARATOR = "\0";
1761
+ function getLeaseKey(session, surface) {
1762
+ return `${surface}${LEASE_KEY_SEPARATOR}${encodeURIComponent(session)}`;
1763
+ }
1764
+ function getSessionName(session) {
1765
+ const raw = session?.trim();
1766
+ if (!raw) throw new CommandFailure(
1767
+ "session_required",
1768
+ "Browser session is required.",
1769
+ "Pass a browser session name, e.g. bycli browser <session> <command>."
1770
+ );
1771
+ return raw;
1772
+ }
1773
+ function getCommandSurface(cmd) {
1774
+ return cmd.surface === "adapter" ? "adapter" : "browser";
1775
+ }
1776
+ function getSurfaceFromKey(key) {
1777
+ return key.split(LEASE_KEY_SEPARATOR, 1)[0] === "adapter" ? "adapter" : "browser";
1778
+ }
1779
+ function getSessionFromKey(key) {
1780
+ const idx = key.indexOf(LEASE_KEY_SEPARATOR);
1781
+ if (idx === -1) return key;
1782
+ try {
1783
+ return decodeURIComponent(key.slice(idx + 1));
1784
+ } catch {
1785
+ return key.slice(idx + 1);
1786
+ }
1787
+ }
1788
+ function getIdleTimeout(key) {
1789
+ const session = automationSessions.get(key);
1790
+ if (session?.kind === "bound") return IDLE_TIMEOUT_NONE;
1791
+ const adapterPersistent = getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent");
1792
+ if (adapterPersistent) return IDLE_TIMEOUT_NONE;
1793
+ const override = sessionTimeoutOverrides.get(key);
1794
+ if (override !== void 0) return override;
1795
+ return getSurfaceFromKey(key) === "browser" ? IDLE_TIMEOUT_INTERACTIVE : IDLE_TIMEOUT_DEFAULT;
1796
+ }
1797
+ function getLeaseLifecycle(key, kind) {
1798
+ if (kind === "bound") return "pinned";
1799
+ const override = sessionLifecycleOverrides.get(key);
1800
+ if (override) return override;
1801
+ return getSurfaceFromKey(key) === "browser" ? "persistent" : "ephemeral";
1802
+ }
1803
+ function getOwnedWindowRole(key) {
1804
+ return getSurfaceFromKey(key) === "browser" ? "interactive" : "automation";
1805
+ }
1806
+ function getWindowRole(key, ownership) {
1807
+ return ownership === "borrowed" ? "borrowed-user" : getOwnedWindowRole(key);
1808
+ }
1809
+ function getWindowMode(key) {
1810
+ return sessionWindowModeOverrides.get(key) ?? "foreground";
1811
+ }
1812
+ function getWindowModeForOwnedPage(key, url) {
1813
+ if (getSurfaceFromKey(key) === "adapter") return "foreground";
1814
+ if (getSurfaceFromKey(key) === "browser" && url === BLANK_PAGE) return "background";
1815
+ return getWindowMode(key);
1816
+ }
1817
+ function makeAlarmName(leaseKey) {
1818
+ return `${LEASE_IDLE_ALARM_PREFIX}${encodeURIComponent(leaseKey)}`;
1819
+ }
1820
+ function leaseKeyFromAlarmName(name) {
1821
+ if (!name.startsWith(LEASE_IDLE_ALARM_PREFIX)) return null;
1822
+ try {
1823
+ return decodeURIComponent(name.slice(LEASE_IDLE_ALARM_PREFIX.length));
1824
+ } catch {
1825
+ return null;
1826
+ }
1827
+ }
1828
+ function withLeaseMutation(fn) {
1829
+ const run = leaseMutationQueue.then(fn, fn);
1830
+ leaseMutationQueue = run.then(() => void 0, () => void 0);
1831
+ return run;
1832
+ }
1833
+ function invalidateLease(leaseKey) {
1834
+ leaseRevisions.set(leaseKey, (leaseRevisions.get(leaseKey) ?? 0) + 1);
1835
+ }
1836
+ function withLeaseLock(leaseKey, fn) {
1837
+ const previous = leaseOperationTails.get(leaseKey) ?? Promise.resolve();
1838
+ const run = previous.then(fn);
1839
+ const tail = run.then(() => void 0, () => void 0);
1840
+ leaseOperationTails.set(leaseKey, tail);
1841
+ void tail.then(() => {
1842
+ if (leaseOperationTails.get(leaseKey) === tail) leaseOperationTails.delete(leaseKey);
1843
+ });
1844
+ return run;
1845
+ }
1846
+ function deleteAutomationSession(leaseKey) {
1847
+ invalidateLease(leaseKey);
1848
+ automationSessions.delete(leaseKey);
1849
+ }
1850
+ function storeAutomationSession(leaseKey, session) {
1851
+ invalidateLease(leaseKey);
1852
+ automationSessions.set(leaseKey, session);
1853
+ }
1854
+ function clearAutomationSessions() {
1855
+ for (const leaseKey of automationSessions.keys()) invalidateLease(leaseKey);
1856
+ automationSessions.clear();
1857
+ }
1858
+ function makeSession(key, session) {
1859
+ const ownership = session.owned ? "owned" : "borrowed";
1860
+ return {
1861
+ ...session,
1862
+ contextId: currentContextId,
1863
+ ownership,
1864
+ lifecycle: getLeaseLifecycle(key, session.kind),
1865
+ windowRole: getWindowRole(key, ownership)
1866
+ };
1867
+ }
1868
+ function emptyRegistry() {
1869
+ return {
1870
+ version: 2,
1871
+ contextId: currentContextId,
1872
+ ownedContainers: {
1873
+ interactive: {
1874
+ windowId: ownedContainers.interactive.windowId,
1875
+ groupId: ownedContainers.interactive.groupId
1876
+ },
1877
+ automation: {
1878
+ windowId: ownedContainers.automation.windowId,
1879
+ groupId: ownedContainers.automation.groupId
1880
+ }
1881
+ },
1882
+ leases: {}
1883
+ };
1884
+ }
1885
+ async function readRegistry() {
1886
+ try {
1887
+ const local = chrome.storage?.local;
1888
+ if (!local) return emptyRegistry();
1889
+ const raw = await local.get(REGISTRY_KEY);
1890
+ const stored = raw[REGISTRY_KEY];
1891
+ if (!stored || stored.version !== 2 || typeof stored.leases !== "object") return emptyRegistry();
1892
+ const storedContainers = stored.ownedContainers && typeof stored.ownedContainers === "object" ? stored.ownedContainers : emptyRegistry().ownedContainers;
1893
+ return {
1894
+ version: 2,
1895
+ contextId: currentContextId,
1896
+ ownedContainers: {
1897
+ interactive: {
1898
+ windowId: typeof storedContainers.interactive?.windowId === "number" ? storedContainers.interactive.windowId : null,
1899
+ groupId: typeof storedContainers.interactive?.groupId === "number" ? storedContainers.interactive.groupId : null
1900
+ },
1901
+ automation: {
1902
+ windowId: typeof storedContainers.automation?.windowId === "number" ? storedContainers.automation.windowId : null,
1903
+ groupId: typeof storedContainers.automation?.groupId === "number" ? storedContainers.automation.groupId : null
1904
+ }
1905
+ },
1906
+ leases: stored.leases
1907
+ };
1908
+ } catch {
1909
+ return emptyRegistry();
1910
+ }
1911
+ }
1912
+ async function writeRegistry(registry) {
1913
+ try {
1914
+ await chrome.storage?.local?.set({ [REGISTRY_KEY]: registry });
1915
+ } catch {
1916
+ }
1917
+ }
1918
+ async function persistRuntimeState() {
1919
+ const leases = {};
1920
+ for (const [leaseKey, session] of automationSessions.entries()) {
1921
+ leases[leaseKey] = {
1922
+ session: session.session,
1923
+ surface: session.surface,
1924
+ kind: session.kind,
1925
+ windowId: session.windowId,
1926
+ owned: session.owned,
1927
+ preferredTabId: session.preferredTabId,
1928
+ contextId: session.contextId,
1929
+ ownership: session.ownership,
1930
+ lifecycle: session.lifecycle,
1931
+ windowRole: session.windowRole,
1932
+ idleDeadlineAt: session.idleDeadlineAt,
1933
+ updatedAt: Date.now()
1934
+ };
1935
+ }
1936
+ await writeRegistry({
1937
+ version: 2,
1938
+ contextId: currentContextId,
1939
+ ownedContainers: {
1940
+ interactive: {
1941
+ windowId: ownedContainers.interactive.windowId,
1942
+ groupId: ownedContainers.interactive.groupId
1943
+ },
1944
+ automation: {
1945
+ windowId: ownedContainers.automation.windowId,
1946
+ groupId: ownedContainers.automation.groupId
1947
+ }
1948
+ },
1949
+ leases
1950
+ });
1951
+ }
1952
+ function nextIdleGeneration(leaseKey) {
1953
+ const generation = (leaseIdleGenerations.get(leaseKey) ?? 0) + 1;
1954
+ leaseIdleGenerations.set(leaseKey, generation);
1955
+ return generation;
1956
+ }
1957
+ function scheduleIdleAlarmAt(leaseKey, deadline) {
1958
+ const alarmName = makeAlarmName(leaseKey);
1959
+ try {
1960
+ if (deadline > 0) {
1961
+ chrome.alarms?.create?.(alarmName, { when: deadline });
1962
+ } else {
1963
+ chrome.alarms?.clear?.(alarmName);
1964
+ }
1965
+ } catch {
1966
+ }
1967
+ }
1968
+ function cancelIdleExpiry(leaseKey) {
1969
+ nextIdleGeneration(leaseKey);
1970
+ scheduleIdleAlarmAt(leaseKey, 0);
1971
+ }
1972
+ async function safeDetach(tabId) {
1973
+ try {
1974
+ const detach$1 = detach;
1975
+ if (typeof detach$1 === "function") await detach$1(tabId);
1976
+ } catch {
1977
+ }
1978
+ }
1979
+ async function removeLeaseSession(leaseKey, expected = automationSessions.get(leaseKey)) {
1980
+ const existing = expected;
1981
+ if (automationSessions.get(leaseKey) !== existing) return;
1982
+ if (existing?.idleTimer) clearTimeout(existing.idleTimer);
1983
+ deleteAutomationSession(leaseKey);
1984
+ sessionTimeoutOverrides.delete(leaseKey);
1985
+ sessionWindowModeOverrides.delete(leaseKey);
1986
+ sessionLifecycleOverrides.delete(leaseKey);
1987
+ cancelIdleExpiry(leaseKey);
1988
+ await persistRuntimeState();
1989
+ }
1990
+ async function releaseLeaseIfIdleExpired(leaseKey, expectedSession, expectedGeneration, expectedDeadline, reason) {
1991
+ await withLeaseLock(leaseKey, async () => {
1992
+ const current = automationSessions.get(leaseKey);
1993
+ if (current !== expectedSession) return;
1994
+ if (leaseIdleGenerations.get(leaseKey) !== expectedGeneration) return;
1995
+ if (current.idleDeadlineAt !== expectedDeadline) return;
1996
+ if (expectedDeadline <= 0 || Date.now() < expectedDeadline) return;
1997
+ await releaseLeaseUnlocked(leaseKey, reason);
1998
+ });
1999
+ }
2000
+ function resetWindowIdleTimer(leaseKey) {
2001
+ const session = automationSessions.get(leaseKey);
2002
+ if (!session) return;
2003
+ if (session.idleTimer) clearTimeout(session.idleTimer);
2004
+ const timeout = getIdleTimeout(leaseKey);
2005
+ const generation = nextIdleGeneration(leaseKey);
2006
+ if (timeout <= 0) {
2007
+ session.idleTimer = null;
2008
+ session.idleDeadlineAt = 0;
2009
+ scheduleIdleAlarmAt(leaseKey, 0);
2010
+ void persistRuntimeState();
2011
+ return;
2012
+ }
2013
+ const deadline = Date.now() + timeout;
2014
+ session.idleDeadlineAt = deadline;
2015
+ scheduleIdleAlarmAt(leaseKey, deadline);
2016
+ void persistRuntimeState();
2017
+ session.idleTimer = setTimeout(async () => {
2018
+ await releaseLeaseIfIdleExpired(leaseKey, session, generation, deadline, "idle timeout");
2019
+ }, timeout);
2020
+ }
2021
+ async function getOwnedContainerGroupId(role, windowId) {
2022
+ const container = ownedContainers[role];
2023
+ if (container.groupId !== null) {
2024
+ try {
2025
+ const group = await chrome.tabGroups.get(container.groupId);
2026
+ if (group.windowId === windowId) return container.groupId;
2027
+ } catch {
2028
+ }
2029
+ container.groupId = null;
2030
+ }
2031
+ for (const title of getOwnedContainerGroupTitles(role)) {
2032
+ const groups = await chrome.tabGroups.query({ windowId, title });
2033
+ const existing = groups[0];
2034
+ if (existing) {
2035
+ container.groupId = existing.id;
2036
+ return existing.id;
2037
+ }
2038
+ }
2039
+ return null;
2040
+ }
2041
+ function getOwnedContainerGroupTitles(role) {
2042
+ return role === "automation" ? [CONTAINER_TAB_GROUP_TITLE.automation, LEGACY_AUTOMATION_TAB_GROUP_TITLE] : [CONTAINER_TAB_GROUP_TITLE.interactive];
2043
+ }
2044
+ async function focusOwnedWindowIfRequested(windowId, mode) {
2045
+ if (mode !== "foreground") return;
2046
+ const updateWindow = chrome.windows.update;
2047
+ if (typeof updateWindow === "function") await updateWindow(windowId, { focused: true }).catch(() => {
2048
+ });
2049
+ }
2050
+ async function toOwnedContainerDiscoveryCandidate(group) {
2051
+ try {
2052
+ const chromeWindow = await chrome.windows.get(group.windowId);
2053
+ const reusableTabId = await findReusableOwnedContainerTab(group.windowId, group.id);
2054
+ return {
2055
+ windowId: group.windowId,
2056
+ groupId: group.id,
2057
+ focused: !!chromeWindow.focused,
2058
+ hasReusableTab: reusableTabId !== void 0
2059
+ };
2060
+ } catch {
2061
+ return null;
2062
+ }
2063
+ }
2064
+ function selectOwnedContainerDiscoveryCandidate(candidates) {
2065
+ if (candidates.length === 0) return null;
2066
+ return [...candidates].sort((a, b) => {
2067
+ if (a.focused !== b.focused) return a.focused ? -1 : 1;
2068
+ if (a.hasReusableTab !== b.hasReusableTab) return a.hasReusableTab ? -1 : 1;
2069
+ return a.groupId - b.groupId;
2070
+ })[0];
2071
+ }
2072
+ async function discoverOwnedContainerFromTabGroup(role) {
2073
+ const container = ownedContainers[role];
2074
+ if (container.groupId !== null) {
2075
+ try {
2076
+ const group = await chrome.tabGroups.get(container.groupId);
2077
+ await chrome.windows.get(group.windowId);
2078
+ container.windowId = group.windowId;
2079
+ return { windowId: group.windowId, groupId: group.id };
2080
+ } catch {
2081
+ container.windowId = null;
2082
+ container.groupId = null;
2083
+ }
2084
+ }
2085
+ for (const title of getOwnedContainerGroupTitles(role)) {
2086
+ const groups = await chrome.tabGroups.query({ title });
2087
+ const candidates = (await Promise.all(groups.map(toOwnedContainerDiscoveryCandidate))).filter((candidate) => candidate !== null);
2088
+ const selected = selectOwnedContainerDiscoveryCandidate(candidates);
2089
+ if (!selected) continue;
2090
+ container.windowId = selected.windowId;
2091
+ container.groupId = selected.groupId;
2092
+ return { windowId: selected.windowId, groupId: selected.groupId };
2093
+ }
2094
+ return null;
2095
+ }
2096
+ async function ensureOwnedContainerTabGroup(role, windowId, tabIds) {
2097
+ const ids = [...new Set(tabIds.filter((id) => id !== void 0))];
2098
+ if (ids.length === 0) return;
2099
+ const container = ownedContainers[role];
2100
+ const previousGroupPromise = container.groupPromise ?? Promise.resolve();
2101
+ const nextGroupPromise = previousGroupPromise.catch(() => void 0).then(() => ensureOwnedContainerTabGroupUnlocked(role, windowId, ids));
2102
+ const trackedGroupPromise = nextGroupPromise.finally(() => {
2103
+ if (container.groupPromise === trackedGroupPromise) container.groupPromise = null;
2104
+ });
2105
+ container.groupPromise = trackedGroupPromise;
2106
+ return trackedGroupPromise;
2107
+ }
2108
+ async function ensureOwnedContainerTabGroupUnlocked(role, windowId, ids) {
2109
+ try {
2110
+ const existingGroupId = await getOwnedContainerGroupId(role, windowId);
2111
+ if (existingGroupId !== null) {
2112
+ const tabs = await chrome.tabs.query({ windowId });
2113
+ const alreadyGrouped = new Set(
2114
+ tabs.filter((tab) => tab.id !== void 0 && ids.includes(tab.id) && tab.groupId === existingGroupId).map((tab) => tab.id)
2115
+ );
2116
+ const missing = ids.filter((id) => !alreadyGrouped.has(id));
2117
+ if (missing.length > 0) await chrome.tabs.group({ groupId: existingGroupId, tabIds: missing });
2118
+ return;
2119
+ }
2120
+ const groupId = await chrome.tabs.group({ tabIds: ids, createProperties: { windowId } });
2121
+ ownedContainers[role].groupId = groupId;
2122
+ await chrome.tabGroups.update(groupId, {
2123
+ color: AUTOMATION_TAB_GROUP_COLOR,
2124
+ title: CONTAINER_TAB_GROUP_TITLE[role],
2125
+ collapsed: false
2126
+ });
2127
+ } catch (err) {
2128
+ console.warn(`[bycli] Failed to mark ${role} tab group: ${err instanceof Error ? err.message : String(err)}`);
2129
+ }
2130
+ }
2131
+ async function ensureOwnedContainerWindow(role, initialUrl, mode = "background") {
2132
+ const container = ownedContainers[role];
2133
+ if (container.promise) return container.promise;
2134
+ container.promise = ensureOwnedContainerWindowUnlocked(role, initialUrl, mode).finally(() => {
2135
+ container.promise = null;
2136
+ });
2137
+ return container.promise;
2138
+ }
2139
+ async function ensureOwnedContainerWindowUnlocked(role, initialUrl, mode = "background") {
2140
+ const container = ownedContainers[role];
2141
+ if (container.windowId !== null) {
2142
+ try {
2143
+ await chrome.windows.get(container.windowId);
2144
+ await focusOwnedWindowIfRequested(container.windowId, mode);
2145
+ const initialTabId2 = await findReusableOwnedContainerTab(container.windowId, await getOwnedContainerGroupId(role, container.windowId));
2146
+ await ensureOwnedContainerTabGroup(role, container.windowId, [initialTabId2]);
2147
+ return {
2148
+ windowId: container.windowId,
2149
+ initialTabId: initialTabId2
2150
+ };
2151
+ } catch {
2152
+ container.windowId = null;
2153
+ container.groupId = null;
2154
+ }
2155
+ }
2156
+ const discovered = await discoverOwnedContainerFromTabGroup(role);
2157
+ if (discovered) {
2158
+ await focusOwnedWindowIfRequested(discovered.windowId, mode);
2159
+ const initialTabId2 = await findReusableOwnedContainerTab(discovered.windowId, discovered.groupId);
2160
+ await ensureOwnedContainerTabGroup(role, discovered.windowId, [initialTabId2]);
2161
+ await persistRuntimeState();
2162
+ return {
2163
+ windowId: discovered.windowId,
2164
+ initialTabId: initialTabId2
2165
+ };
2166
+ }
2167
+ const startUrl = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE;
2168
+ const win = await chrome.windows.create({
2169
+ url: startUrl,
2170
+ focused: mode === "foreground",
2171
+ width: 1280,
2172
+ height: 900,
2173
+ type: "normal"
2174
+ });
2175
+ container.windowId = win.id;
2176
+ console.log(`[bycli] Created owned ${role} window ${container.windowId} (start=${startUrl})`);
2177
+ const tabs = await chrome.tabs.query({ windowId: win.id });
2178
+ const initialTabId = tabs[0]?.id;
2179
+ if (initialTabId) {
2180
+ await new Promise((resolve) => {
2181
+ const timeout = setTimeout(resolve, 500);
2182
+ const listener = (tabId, info) => {
2183
+ if (tabId === initialTabId && info.status === "complete") {
2184
+ chrome.tabs.onUpdated.removeListener(listener);
2185
+ clearTimeout(timeout);
2186
+ resolve();
2187
+ }
2188
+ };
2189
+ if (tabs[0].status === "complete") {
2190
+ clearTimeout(timeout);
2191
+ resolve();
2192
+ } else {
2193
+ chrome.tabs.onUpdated.addListener(listener);
2194
+ }
2195
+ });
2196
+ }
2197
+ await ensureOwnedContainerTabGroup(role, container.windowId, [initialTabId]);
2198
+ await persistRuntimeState();
2199
+ return { windowId: container.windowId, initialTabId };
2200
+ }
2201
+ async function findReusableOwnedContainerTab(windowId, groupId) {
2202
+ if (groupId === null) return void 0;
2203
+ try {
2204
+ const tabs = await chrome.tabs.query({ windowId, groupId });
2205
+ const reusable = tabs.find(
2206
+ (tab) => tab.id !== void 0 && initialTabIsAvailable(tab.id) && isDebuggableUrl(tab.url)
2207
+ );
2208
+ return reusable?.id;
2209
+ } catch {
2210
+ return void 0;
2211
+ }
2212
+ }
2213
+ function initialTabIsAvailable(tabId) {
2214
+ if (tabId === void 0) return false;
2215
+ for (const session of automationSessions.values()) {
2216
+ if (session.owned && session.preferredTabId === tabId) return false;
2217
+ }
2218
+ return true;
2219
+ }
2220
+ async function createOwnedTabLeaseUnlocked(leaseKey, initialUrl, blankFirst = false) {
2221
+ return withLeaseMutation(() => createOwnedTabLeaseMutationUnlocked(leaseKey, initialUrl, blankFirst));
2222
+ }
2223
+ async function createOwnedTabLeaseMutationUnlocked(leaseKey, initialUrl, blankFirst = false) {
2224
+ const createUrl = blankFirst ? BLANK_PAGE : initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE;
2225
+ const role = getOwnedWindowRole(leaseKey);
2226
+ const windowMode = getWindowModeForOwnedPage(leaseKey, createUrl);
2227
+ const { windowId, initialTabId } = await ensureOwnedContainerWindow(role, blankFirst ? void 0 : initialUrl, windowMode);
2228
+ let tab;
2229
+ if (initialTabIsAvailable(initialTabId)) {
2230
+ tab = await chrome.tabs.get(initialTabId);
2231
+ if (!blankFirst && !isTargetUrl(tab.url, createUrl)) {
2232
+ tab = await chrome.tabs.update(initialTabId, { url: createUrl });
2233
+ await new Promise((resolve) => setTimeout(resolve, 300));
2234
+ tab = await chrome.tabs.get(initialTabId);
2235
+ }
2236
+ } else {
2237
+ const activateTab = windowMode === "foreground";
2238
+ tab = await chrome.tabs.create({ windowId, url: createUrl, active: activateTab });
2239
+ }
2240
+ if (!tab.id) throw new Error("Failed to create tab lease in automation container");
2241
+ await ensureOwnedContainerTabGroup(role, windowId, [tab.id]);
2242
+ setLeaseSession(leaseKey, {
2243
+ session: getSessionFromKey(leaseKey),
2244
+ surface: getSurfaceFromKey(leaseKey),
2245
+ kind: "owned",
2246
+ windowId,
2247
+ owned: true,
2248
+ preferredTabId: tab.id
2249
+ });
2250
+ resetWindowIdleTimer(leaseKey);
2251
+ return { tabId: tab.id, tab };
2252
+ }
2253
+ async function getAutomationWindow(leaseKey, initialUrl) {
2254
+ const existing = automationSessions.get(leaseKey);
2255
+ if (existing) {
2256
+ if (!existing.owned) {
2257
+ throw new CommandFailure(
2258
+ "bound_window_operation_blocked",
2259
+ `Session "${existing.session}" is bound to a user tab and does not own an byCLI tab lease.`,
2260
+ "Use page commands on the bound tab, or unbind the session first."
2261
+ );
2262
+ }
2263
+ try {
2264
+ const tabId = existing.preferredTabId;
2265
+ if (tabId !== null) {
2266
+ const tab = await chrome.tabs.get(tabId);
2267
+ if (isDebuggableUrl(tab.url)) return tab.windowId;
2268
+ }
2269
+ await chrome.windows.get(existing.windowId);
2270
+ return existing.windowId;
2271
+ } catch {
2272
+ await removeLeaseSession(leaseKey);
2273
+ }
2274
+ }
2275
+ const role = getOwnedWindowRole(leaseKey);
2276
+ const url = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE;
2277
+ return (await ensureOwnedContainerWindow(role, initialUrl, getWindowModeForOwnedPage(leaseKey, url))).windowId;
2278
+ }
2279
+ chrome.windows.onRemoved.addListener(async (windowId) => {
2280
+ for (const container of Object.values(ownedContainers)) {
2281
+ if (container.windowId === windowId) {
2282
+ container.windowId = null;
2283
+ container.groupId = null;
2284
+ }
2285
+ }
2286
+ for (const [leaseKey, expected] of [...automationSessions.entries()]) {
2287
+ if (expected.windowId === windowId) await withLeaseLock(leaseKey, async () => {
2288
+ const session = automationSessions.get(leaseKey);
2289
+ if (!session || session !== expected || session.windowId !== windowId) return;
2290
+ console.log(`[bycli] ${session.surface} container closed (session=${session.session})`);
2291
+ if (session.idleTimer) clearTimeout(session.idleTimer);
2292
+ deleteAutomationSession(leaseKey);
2293
+ sessionTimeoutOverrides.delete(leaseKey);
2294
+ sessionWindowModeOverrides.delete(leaseKey);
2295
+ sessionLifecycleOverrides.delete(leaseKey);
2296
+ cancelIdleExpiry(leaseKey);
2297
+ });
2298
+ }
2299
+ await persistRuntimeState();
2300
+ });
2301
+ chrome.tabs.onRemoved.addListener(async (tabId) => {
2302
+ evictTab(tabId);
2303
+ for (const [leaseKey, expected] of [...automationSessions.entries()]) {
2304
+ if (expected.preferredTabId === tabId) await withLeaseLock(leaseKey, async () => {
2305
+ const session = automationSessions.get(leaseKey);
2306
+ if (!session || session !== expected || session.preferredTabId !== tabId) return;
2307
+ if (session.idleTimer) clearTimeout(session.idleTimer);
2308
+ deleteAutomationSession(leaseKey);
2309
+ sessionTimeoutOverrides.delete(leaseKey);
2310
+ sessionWindowModeOverrides.delete(leaseKey);
2311
+ sessionLifecycleOverrides.delete(leaseKey);
2312
+ cancelIdleExpiry(leaseKey);
2313
+ console.log(`[bycli] Session ${session.session} detached from tab ${tabId} (tab closed)`);
2314
+ });
2315
+ }
2316
+ await persistRuntimeState();
2317
+ });
2318
+ let initialized = false;
2319
+ function initialize() {
2320
+ if (initialized) return;
2321
+ initialized = true;
2322
+ chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
2323
+ registerListeners();
2324
+ try {
2325
+ const registerFrameTracking$1 = registerFrameTracking;
2326
+ registerFrameTracking$1?.();
2327
+ } catch {
2328
+ }
2329
+ void (async () => {
2330
+ await getCurrentContextId();
2331
+ await reconcileTargetLeaseRegistry();
2332
+ await connect();
2333
+ })();
2334
+ console.log("[bycli] byCLI extension initialized");
2335
+ }
2336
+ chrome.runtime.onInstalled.addListener(() => {
2337
+ initialize();
2338
+ });
2339
+ chrome.runtime.onStartup.addListener(() => {
2340
+ initialize();
2341
+ });
2342
+ initialize();
2343
+ chrome.alarms.onAlarm.addListener(async (alarm) => {
2344
+ if (alarm.name === "keepalive") void connect();
2345
+ const leaseKey = leaseKeyFromAlarmName(alarm.name);
2346
+ if (leaseKey) {
2347
+ const session = automationSessions.get(leaseKey);
2348
+ if (!session) return;
2349
+ const generation = leaseIdleGenerations.get(leaseKey) ?? 0;
2350
+ const deadline = Number.isFinite(alarm.scheduledTime) && alarm.scheduledTime > 0 ? alarm.scheduledTime : session.idleDeadlineAt;
2351
+ await releaseLeaseIfIdleExpired(leaseKey, session, generation, deadline, "idle alarm");
2352
+ }
2353
+ });
2354
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
2355
+ if (msg?.type === "getStatus") {
2356
+ void (async () => {
2357
+ const contextId = await getCurrentContextId();
2358
+ const connected = ws?.readyState === WebSocket.OPEN;
2359
+ const extensionVersion = chrome.runtime.getManifest().version;
2360
+ const daemonVersion = connected ? await fetchDaemonVersion() : null;
2361
+ sendResponse({
2362
+ connected,
2363
+ reconnecting: reconnectTimer !== null,
2364
+ contextId,
2365
+ extensionVersion,
2366
+ daemonVersion
2367
+ });
2368
+ })();
2369
+ return true;
2370
+ }
2371
+ return false;
2372
+ });
2373
+ async function fetchDaemonVersion() {
2374
+ try {
2375
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/status`, {
2376
+ method: "GET",
2377
+ headers: { "X-byCLI": "1" },
2378
+ signal: AbortSignal.timeout(1500)
2379
+ });
2380
+ if (!res.ok) return null;
2381
+ const body = await res.json();
2382
+ return typeof body.daemonVersion === "string" ? body.daemonVersion : null;
2383
+ } catch {
2384
+ return null;
2385
+ }
2386
+ }
2387
+ async function handleCommand(cmd) {
2388
+ const session = getSessionName(cmd.session);
2389
+ const surface = getCommandSurface(cmd);
2390
+ const leaseKey = getLeaseKey(session, surface);
2391
+ if (cmd.windowMode === "foreground" || cmd.windowMode === "background") {
2392
+ sessionWindowModeOverrides.set(leaseKey, cmd.windowMode);
2393
+ }
2394
+ if (surface === "adapter" && (cmd.siteSession === "persistent" || cmd.siteSession === "ephemeral")) {
2395
+ sessionLifecycleOverrides.set(leaseKey, cmd.siteSession);
2396
+ }
2397
+ if (cmd.idleTimeout != null && cmd.idleTimeout > 0) {
2398
+ sessionTimeoutOverrides.set(leaseKey, cmd.idleTimeout * 1e3);
2399
+ }
2400
+ resetWindowIdleTimer(leaseKey);
2401
+ try {
2402
+ switch (cmd.action) {
2403
+ case "exec":
2404
+ return await handleExec(cmd, leaseKey);
2405
+ case "navigate":
2406
+ return await handleNavigate(cmd, leaseKey);
2407
+ case "tabs":
2408
+ return await handleTabs(cmd, leaseKey);
2409
+ case "cookies":
2410
+ return await handleCookies(cmd);
2411
+ case "screenshot":
2412
+ return await handleScreenshot(cmd, leaseKey);
2413
+ case "close-window":
2414
+ return await handleCloseWindow(cmd, leaseKey);
2415
+ case "cdp":
2416
+ return await handleCdp(cmd, leaseKey);
2417
+ case "set-file-input":
2418
+ return await handleSetFileInput(cmd, leaseKey);
2419
+ case "insert-text":
2420
+ return await handleInsertText(cmd, leaseKey);
2421
+ case "bind":
2422
+ return await handleBind(cmd, leaseKey);
2423
+ case "network-capture-start":
2424
+ return await handleNetworkCaptureStart(cmd, leaseKey);
2425
+ case "network-capture-read":
2426
+ return await handleNetworkCaptureRead(cmd, leaseKey);
2427
+ case "ima-auth-start":
2428
+ return await handleImaAuthStart(cmd, leaseKey);
2429
+ case "ima-auth-read":
2430
+ return await handleImaAuthRead(cmd, leaseKey);
2431
+ case "ima-reader-request":
2432
+ return await handleImaReaderRequest(cmd, leaseKey);
2433
+ case "ima-media-request":
2434
+ return await handleImaMediaRequest(cmd, leaseKey);
2435
+ case "ima-auth-release":
2436
+ return await handleImaAuthRelease(cmd, leaseKey);
2437
+ case "ui-capture-start":
2438
+ return await handleUiCaptureStart(cmd, leaseKey);
2439
+ case "ui-capture-read":
2440
+ return await handleUiCaptureRead(cmd, leaseKey);
2441
+ case "wait-download":
2442
+ return await handleWaitDownload(cmd);
2443
+ case "frames":
2444
+ return await handleFrames(cmd, leaseKey);
2445
+ default:
2446
+ return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
2447
+ }
2448
+ } catch (err) {
2449
+ return {
2450
+ id: cmd.id,
2451
+ ok: false,
2452
+ error: err instanceof Error ? err.message : String(err),
2453
+ ...err instanceof CommandFailure ? { errorCode: err.code } : {},
2454
+ ...err instanceof CommandFailure && err.hint ? { errorHint: err.hint } : {}
2455
+ };
2456
+ }
2457
+ }
2458
+ const BLANK_PAGE = "about:blank";
2459
+ function isDebuggableUrl(url) {
2460
+ if (!url) return true;
2461
+ return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:");
2462
+ }
2463
+ function isSafeNavigationUrl(url) {
2464
+ return url.startsWith("http://") || url.startsWith("https://");
2465
+ }
2466
+ function normalizeUrlForComparison(url) {
2467
+ if (!url) return "";
2468
+ try {
2469
+ const parsed = new URL(url);
2470
+ if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") {
2471
+ parsed.port = "";
2472
+ }
2473
+ const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
2474
+ return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
2475
+ } catch {
2476
+ return url;
2477
+ }
2478
+ }
2479
+ function isTargetUrl(currentUrl, targetUrl) {
2480
+ return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
2481
+ }
2482
+ function getUrlOrigin(url) {
2483
+ if (!url) return null;
2484
+ try {
2485
+ return new URL(url).origin;
2486
+ } catch {
2487
+ return null;
2488
+ }
2489
+ }
2490
+ function enumerateCrossOriginFrames(tree) {
2491
+ const frames = [];
2492
+ function collect(node, accessibleOrigin) {
2493
+ for (const child of node.childFrames || []) {
2494
+ const frame = child.frame;
2495
+ const frameUrl = frame.url || frame.unreachableUrl || "";
2496
+ const frameOrigin = getUrlOrigin(frameUrl);
2497
+ if (accessibleOrigin && frameOrigin && frameOrigin === accessibleOrigin) {
2498
+ collect(child, frameOrigin);
2499
+ continue;
2500
+ }
2501
+ frames.push({
2502
+ index: frames.length,
2503
+ frameId: frame.id,
2504
+ url: frameUrl,
2505
+ name: frame.name || ""
2506
+ });
2507
+ }
2508
+ }
2509
+ const rootFrame = tree?.frameTree?.frame;
2510
+ const rootUrl = rootFrame?.url || rootFrame?.unreachableUrl || "";
2511
+ collect(tree.frameTree, getUrlOrigin(rootUrl));
2512
+ return frames;
2513
+ }
2514
+ function setLeaseSession(leaseKey, session) {
2515
+ const existing = automationSessions.get(leaseKey);
2516
+ if (existing?.idleTimer) clearTimeout(existing.idleTimer);
2517
+ const timeout = getIdleTimeout(leaseKey);
2518
+ storeAutomationSession(leaseKey, {
2519
+ ...makeSession(leaseKey, session),
2520
+ idleTimer: null,
2521
+ idleDeadlineAt: timeout <= 0 ? 0 : Date.now() + timeout
2522
+ });
2523
+ void persistRuntimeState();
2524
+ }
2525
+ async function resolveCommandTabId(cmd) {
2526
+ if (cmd.page) return resolveTabId$1(cmd.page);
2527
+ return void 0;
2528
+ }
2529
+ async function resolveTab(tabId, leaseKey, initialUrl, blankFirst = false) {
2530
+ return withLeaseLock(leaseKey, () => resolveTabUnlocked(tabId, leaseKey, initialUrl, blankFirst));
2531
+ }
2532
+ async function resolveTabUnlocked(tabId, leaseKey, initialUrl, blankFirst = false) {
2533
+ const existingSession = automationSessions.get(leaseKey);
2534
+ if (tabId !== void 0) {
2535
+ try {
2536
+ const tab = await chrome.tabs.get(tabId);
2537
+ const session = existingSession;
2538
+ const matchesSession = session ? session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId : false;
2539
+ if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab };
2540
+ if (session && !session.owned) {
2541
+ throw new CommandFailure(
2542
+ matchesSession ? "bound_tab_not_debuggable" : "bound_tab_mismatch",
2543
+ matchesSession ? `Bound tab for session "${session.session}" is not debuggable (${tab.url ?? "unknown URL"}).` : `Target tab is not the tab bound to session "${session.session}".`,
2544
+ 'Run "bycli browser bind" again on a debuggable http(s) tab.'
2545
+ );
2546
+ }
2547
+ if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) {
2548
+ console.warn(`[bycli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);
2549
+ try {
2550
+ await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
2551
+ const moved = await chrome.tabs.get(tabId);
2552
+ if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) {
2553
+ return { tabId, tab: moved };
2554
+ }
2555
+ } catch (moveErr) {
2556
+ console.warn(`[bycli] Failed to move tab back: ${moveErr}`);
2557
+ }
2558
+ } else if (!isDebuggableUrl(tab.url)) {
2559
+ console.warn(`[bycli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
2560
+ }
2561
+ } catch (err) {
2562
+ if (err instanceof CommandFailure) throw err;
2563
+ if (existingSession && !existingSession.owned) {
2564
+ await removeLeaseSession(leaseKey, existingSession);
2565
+ throw new CommandFailure(
2566
+ "bound_tab_gone",
2567
+ `Bound tab for session "${existingSession.session}" no longer exists.`,
2568
+ 'Run "bycli browser bind" again, then retry the command.'
2569
+ );
2570
+ }
2571
+ console.warn(`[bycli] Tab ${tabId} no longer exists, re-resolving`);
2572
+ }
2573
+ }
2574
+ const existingPreferredTabId = existingSession?.preferredTabId ?? null;
2575
+ if (existingSession && existingPreferredTabId !== null) {
2576
+ const session = existingSession;
2577
+ try {
2578
+ const preferredTab = await chrome.tabs.get(existingPreferredTabId);
2579
+ if (isDebuggableUrl(preferredTab.url)) return { tabId: preferredTab.id, tab: preferredTab };
2580
+ if (!session.owned) {
2581
+ throw new CommandFailure(
2582
+ "bound_tab_not_debuggable",
2583
+ `Bound tab for session "${session.session}" is not debuggable (${preferredTab.url ?? "unknown URL"}).`,
2584
+ 'Switch the tab to an http(s) page or run "bycli browser bind" on another tab.'
2585
+ );
2586
+ }
2587
+ } catch (err) {
2588
+ if (err instanceof CommandFailure) throw err;
2589
+ await removeLeaseSession(leaseKey, session);
2590
+ if (!session.owned) {
2591
+ throw new CommandFailure(
2592
+ "bound_tab_gone",
2593
+ `Bound tab for session "${session.session}" no longer exists.`,
2594
+ 'Run "bycli browser bind" again, then retry the command.'
2595
+ );
2596
+ }
2597
+ return createOwnedTabLeaseUnlocked(leaseKey, initialUrl, blankFirst);
2598
+ }
2599
+ }
2600
+ if (!existingSession || existingSession.owned && existingSession.preferredTabId === null) {
2601
+ return createOwnedTabLeaseUnlocked(leaseKey, initialUrl, blankFirst);
2602
+ }
2603
+ const windowId = await getAutomationWindow(leaseKey, initialUrl);
2604
+ const tabs = await chrome.tabs.query({ windowId });
2605
+ const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
2606
+ if (debuggableTab?.id) return { tabId: debuggableTab.id, tab: debuggableTab };
2607
+ const reuseTab = tabs.find((t) => t.id);
2608
+ if (reuseTab?.id) {
2609
+ await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
2610
+ await new Promise((resolve) => setTimeout(resolve, 300));
2611
+ try {
2612
+ const updated = await chrome.tabs.get(reuseTab.id);
2613
+ if (isDebuggableUrl(updated.url)) return { tabId: reuseTab.id, tab: updated };
2614
+ console.warn(`[bycli] data: URI was intercepted (${updated.url}), creating fresh tab`);
2615
+ } catch {
2616
+ }
2617
+ }
2618
+ const newTab = await chrome.tabs.create({
2619
+ windowId,
2620
+ url: BLANK_PAGE,
2621
+ active: getWindowModeForOwnedPage(leaseKey, BLANK_PAGE) === "foreground"
2622
+ });
2623
+ if (!newTab.id) throw new Error("Failed to create tab in automation container");
2624
+ return { tabId: newTab.id, tab: newTab };
2625
+ }
2626
+ async function pageScopedResult(id, tabId, data) {
2627
+ const page = await resolveTargetId(tabId);
2628
+ return { id, ok: true, data, page };
2629
+ }
2630
+ async function resolveTabId(tabId, leaseKey, initialUrl) {
2631
+ const resolved = await resolveTab(tabId, leaseKey, initialUrl);
2632
+ return resolved.tabId;
2633
+ }
2634
+ async function resolveTabIdUnlocked(tabId, leaseKey, initialUrl) {
2635
+ const resolved = await resolveTabUnlocked(tabId, leaseKey, initialUrl);
2636
+ return resolved.tabId;
2637
+ }
2638
+ async function listAutomationTabsUnlocked(leaseKey) {
2639
+ const session = automationSessions.get(leaseKey);
2640
+ if (!session) return [];
2641
+ if (session.preferredTabId !== null) {
2642
+ try {
2643
+ return [await chrome.tabs.get(session.preferredTabId)];
2644
+ } catch {
2645
+ await removeLeaseSession(leaseKey, session);
2646
+ return [];
2647
+ }
2648
+ }
2649
+ try {
2650
+ return await chrome.tabs.query({ windowId: session.windowId });
2651
+ } catch {
2652
+ await removeLeaseSession(leaseKey, session);
2653
+ return [];
2654
+ }
2655
+ }
2656
+ async function listAutomationWebTabs(leaseKey) {
2657
+ return withLeaseLock(leaseKey, () => listAutomationWebTabsUnlocked(leaseKey));
2658
+ }
2659
+ async function listAutomationWebTabsUnlocked(leaseKey) {
2660
+ const tabs = await listAutomationTabsUnlocked(leaseKey);
2661
+ return tabs.filter((tab) => isDebuggableUrl(tab.url));
2662
+ }
2663
+ async function handleExec(cmd, leaseKey) {
2664
+ if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" };
2665
+ const cmdTabId = await resolveCommandTabId(cmd);
2666
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
2667
+ try {
2668
+ const aggressive = getSurfaceFromKey(leaseKey) === "browser";
2669
+ if (cmd.frameIndex != null) {
2670
+ const tree = await getFrameTree(tabId);
2671
+ const frames = enumerateCrossOriginFrames(tree);
2672
+ if (cmd.frameIndex < 0 || cmd.frameIndex >= frames.length) {
2673
+ return { id: cmd.id, ok: false, error: `Frame index ${cmd.frameIndex} out of range (${frames.length} cross-origin frames available)` };
2674
+ }
2675
+ const data2 = await evaluateInFrame(tabId, cmd.code, frames[cmd.frameIndex].frameId, aggressive);
2676
+ return pageScopedResult(cmd.id, tabId, data2);
2677
+ }
2678
+ const data = await evaluateAsync(tabId, cmd.code, aggressive);
2679
+ return pageScopedResult(cmd.id, tabId, data);
2680
+ } catch (err) {
2681
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
2682
+ }
2683
+ }
2684
+ async function handleFrames(cmd, leaseKey) {
2685
+ const cmdTabId = await resolveCommandTabId(cmd);
2686
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
2687
+ try {
2688
+ const tree = await getFrameTree(tabId);
2689
+ return { id: cmd.id, ok: true, data: enumerateCrossOriginFrames(tree) };
2690
+ } catch (err) {
2691
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
2692
+ }
2693
+ }
2694
+ async function handleNavigate(cmd, leaseKey) {
2695
+ return withLeaseLock(leaseKey, () => handleNavigateUnlocked(cmd, leaseKey));
2696
+ }
2697
+ async function handleNavigateUnlocked(cmd, leaseKey) {
2698
+ if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" };
2699
+ if (!isSafeNavigationUrl(cmd.url)) {
2700
+ return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
2701
+ }
2702
+ const pre = checkUrlSyntax(cmd.url);
2703
+ if (!pre.ok) {
2704
+ return {
2705
+ id: cmd.id,
2706
+ ok: false,
2707
+ errorCode: "navigation_blocked_by_policy",
2708
+ error: `Navigation blocked by URL policy: ${pre.detail}`
2709
+ };
2710
+ }
2711
+ const cmdTabId = await resolveCommandTabId(cmd);
2712
+ const resolved = await resolveTabUnlocked(cmdTabId, leaseKey, cmd.url, true);
2713
+ const tabId = resolved.tabId;
2714
+ const beforeTab = resolved.tab ?? await chrome.tabs.get(tabId);
2715
+ const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
2716
+ const targetUrl = cmd.url;
2717
+ if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) {
2718
+ return pageScopedResult(cmd.id, tabId, { title: beforeTab.title, url: beforeTab.url, timedOut: false });
2719
+ }
2720
+ if (!hasActiveNetworkCapture(tabId)) {
2721
+ await detach(tabId);
2722
+ }
2723
+ const aggressive = getSurfaceFromKey(leaseKey) === "browser";
2724
+ let guard;
2725
+ try {
2726
+ guard = await armFetchGuard(tabId, (url) => checkUrlSyntax(url).ok, aggressive);
2727
+ } catch (err) {
2728
+ return {
2729
+ id: cmd.id,
2730
+ ok: false,
2731
+ errorCode: "navigation_redirect_requires_interception",
2732
+ error: `Cannot arm navigation request interception: ${err instanceof Error ? err.message : String(err)}`,
2733
+ errorHint: "Navigation is refused because redirects cannot be checked before send."
2734
+ };
2735
+ }
2736
+ await chrome.tabs.update(tabId, { url: targetUrl });
2737
+ let timedOut = false;
2738
+ await new Promise((resolve) => {
2739
+ let settled = false;
2740
+ let checkTimer = null;
2741
+ let blockTimer = null;
2742
+ let timeoutTimer = null;
2743
+ const finish = () => {
2744
+ if (settled) return;
2745
+ settled = true;
2746
+ chrome.tabs.onUpdated.removeListener(listener);
2747
+ if (checkTimer) clearTimeout(checkTimer);
2748
+ if (blockTimer) clearInterval(blockTimer);
2749
+ if (timeoutTimer) clearTimeout(timeoutTimer);
2750
+ resolve();
2751
+ };
2752
+ const isNavigationDone = (url) => {
2753
+ return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
2754
+ };
2755
+ const listener = (id, info, tab2) => {
2756
+ if (id !== tabId) return;
2757
+ if (info.status === "complete" && isNavigationDone(tab2.url ?? info.url)) {
2758
+ finish();
2759
+ }
2760
+ };
2761
+ chrome.tabs.onUpdated.addListener(listener);
2762
+ checkTimer = setTimeout(async () => {
2763
+ try {
2764
+ const currentTab = await chrome.tabs.get(tabId);
2765
+ if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) {
2766
+ finish();
2767
+ }
2768
+ } catch {
2769
+ }
2770
+ }, 100);
2771
+ blockTimer = setInterval(() => {
2772
+ if ((guard?.blocked.length ?? 0) > 0) {
2773
+ finish();
2774
+ }
2775
+ }, 100);
2776
+ timeoutTimer = setTimeout(() => {
2777
+ timedOut = true;
2778
+ console.warn(`[bycli] Navigate to ${targetUrl} timed out after 15s`);
2779
+ finish();
2780
+ }, 15e3);
2781
+ });
2782
+ if ((guard?.blocked.length ?? 0) > 0) {
2783
+ return {
2784
+ id: cmd.id,
2785
+ ok: false,
2786
+ errorCode: "navigation_blocked_by_policy",
2787
+ error: `Navigation blocked by URL policy on redirect: ${guard?.blocked.join(", ")}`,
2788
+ errorHint: "A redirected main-frame request resolved to a forbidden target and was blocked before send."
2789
+ };
2790
+ }
2791
+ let tab = await chrome.tabs.get(tabId);
2792
+ const postNavigationSession = automationSessions.get(leaseKey);
2793
+ if (postNavigationSession && tab.windowId !== postNavigationSession.windowId) {
2794
+ console.warn(`[bycli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${postNavigationSession.windowId}`);
2795
+ try {
2796
+ await chrome.tabs.move(tabId, { windowId: postNavigationSession.windowId, index: -1 });
2797
+ tab = await chrome.tabs.get(tabId);
2798
+ } catch (moveErr) {
2799
+ console.warn(`[bycli] Failed to recover drifted tab: ${moveErr}`);
2800
+ }
2801
+ }
2802
+ const navWindowMode = getWindowModeForOwnedPage(leaseKey, targetUrl);
2803
+ await focusOwnedWindowIfRequested(tab.windowId, navWindowMode);
2804
+ if (navWindowMode === "foreground") {
2805
+ await chrome.tabs.update(tabId, { active: true }).catch(() => {
2806
+ });
2807
+ }
2808
+ return pageScopedResult(cmd.id, tabId, {
2809
+ title: tab.title,
2810
+ url: tab.url,
2811
+ timedOut,
2812
+ // M1: interception evidence for acceptance/observability. Extension capture form
2813
+ // is ip-observed-only per ADR-0006 (no DNS in SW; rebinding not closed here).
2814
+ interception: {
2815
+ armed: true,
2816
+ tier: "ip-observed-only",
2817
+ blocked: guard?.blocked ?? [],
2818
+ observedIps: guard?.observedIps ?? []
2819
+ }
2820
+ });
2821
+ }
2822
+ function captureFocusLease(leaseKey, lease) {
2823
+ if (!lease.owned || lease.preferredTabId === null) return null;
2824
+ return {
2825
+ revision: leaseRevisions.get(leaseKey) ?? 0,
2826
+ session: lease.session,
2827
+ surface: lease.surface,
2828
+ kind: lease.kind,
2829
+ windowId: lease.windowId,
2830
+ preferredTabId: lease.preferredTabId,
2831
+ contextId: lease.contextId,
2832
+ ownership: lease.ownership,
2833
+ lifecycle: lease.lifecycle,
2834
+ windowRole: lease.windowRole,
2835
+ windowMode: getWindowMode(leaseKey)
2836
+ };
2837
+ }
2838
+ function isFocusLeaseCurrent(leaseKey, expected) {
2839
+ const current = automationSessions.get(leaseKey);
2840
+ return (leaseRevisions.get(leaseKey) ?? 0) === expected.revision && current?.owned === true && current.session === expected.session && current.surface === expected.surface && current.kind === expected.kind && current.windowId === expected.windowId && current.preferredTabId === expected.preferredTabId && current.contextId === expected.contextId && current.ownership === expected.ownership && current.lifecycle === expected.lifecycle && current.windowRole === expected.windowRole && getWindowMode(leaseKey) === expected.windowMode;
2841
+ }
2842
+ function focusLeaseChangedResult(id) {
2843
+ return { id, ok: false, error: "Current automation lease changed during focus; no further focus action was performed" };
2844
+ }
2845
+ async function handleFocusUnlocked(cmd, leaseKey) {
2846
+ const currentSession = automationSessions.get(leaseKey);
2847
+ const focusLease = currentSession ? captureFocusLease(leaseKey, currentSession) : null;
2848
+ if (!focusLease) {
2849
+ return { id: cmd.id, ok: false, error: "No owned tab is leased to the current automation session" };
2850
+ }
2851
+ const cmdTabId = await resolveCommandTabId(cmd);
2852
+ if (!isFocusLeaseCurrent(leaseKey, focusLease)) return focusLeaseChangedResult(cmd.id);
2853
+ const tabId = cmdTabId ?? focusLease.preferredTabId;
2854
+ if (tabId !== focusLease.preferredTabId) {
2855
+ return { id: cmd.id, ok: false, error: "Page is not leased to the current automation session" };
2856
+ }
2857
+ let tab;
2858
+ try {
2859
+ tab = await chrome.tabs.get(tabId);
2860
+ } catch {
2861
+ return { id: cmd.id, ok: false, error: "Current automation session tab no longer exists" };
2862
+ }
2863
+ if (!isFocusLeaseCurrent(leaseKey, focusLease)) return focusLeaseChangedResult(cmd.id);
2864
+ if (!Number.isInteger(tab.windowId) || tab.windowId < 0 || tab.windowId !== focusLease.windowId) {
2865
+ return { id: cmd.id, ok: false, error: "Current automation session tab is outside its owned window" };
2866
+ }
2867
+ const updateWindow = chrome.windows.update;
2868
+ if (typeof updateWindow !== "function") throw new Error("chrome.windows.update is unavailable");
2869
+ const activatedTab = await chrome.tabs.update(tabId, { active: true });
2870
+ if (!isFocusLeaseCurrent(leaseKey, focusLease)) return focusLeaseChangedResult(cmd.id);
2871
+ if (activatedTab.id !== tabId || activatedTab.windowId !== focusLease.windowId) {
2872
+ return { id: cmd.id, ok: false, error: "Current automation session tab changed window during focus" };
2873
+ }
2874
+ await updateWindow(focusLease.windowId, { focused: true });
2875
+ if (!isFocusLeaseCurrent(leaseKey, focusLease)) return focusLeaseChangedResult(cmd.id);
2876
+ return { id: cmd.id, ok: true, data: { focused: true } };
2877
+ }
2878
+ async function handleTabNewUnlocked(cmd, leaseKey) {
2879
+ const currentSession = automationSessions.get(leaseKey);
2880
+ if (currentSession && !currentSession.owned) {
2881
+ return boundTabMutationResult(cmd, currentSession);
2882
+ }
2883
+ if (cmd.url && !isSafeNavigationUrl(cmd.url)) {
2884
+ return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
2885
+ }
2886
+ if (!automationSessions.has(leaseKey)) {
2887
+ const created = await createOwnedTabLeaseUnlocked(leaseKey, cmd.url);
2888
+ return pageScopedResult(cmd.id, created.tabId, { url: created.tab?.url });
2889
+ }
2890
+ const newTabUrl = cmd.url ?? BLANK_PAGE;
2891
+ const windowId = await getAutomationWindow(leaseKey, cmd.url);
2892
+ const activateNewTab = getWindowModeForOwnedPage(leaseKey, newTabUrl) === "foreground";
2893
+ const tab = await chrome.tabs.create({ windowId, url: newTabUrl, active: activateNewTab });
2894
+ if (!tab.id) return { id: cmd.id, ok: false, error: "Failed to create tab" };
2895
+ await ensureOwnedContainerTabGroup(getOwnedWindowRole(leaseKey), windowId, [tab.id]);
2896
+ setLeaseSession(leaseKey, {
2897
+ session: getSessionFromKey(leaseKey),
2898
+ surface: getSurfaceFromKey(leaseKey),
2899
+ kind: "owned",
2900
+ windowId: tab.windowId,
2901
+ owned: true,
2902
+ preferredTabId: tab.id
2903
+ });
2904
+ resetWindowIdleTimer(leaseKey);
2905
+ return pageScopedResult(cmd.id, tab.id, { url: tab.url });
2906
+ }
2907
+ async function handleTabCloseUnlocked(cmd, leaseKey) {
2908
+ const lockedSession = automationSessions.get(leaseKey);
2909
+ if (lockedSession && !lockedSession.owned) {
2910
+ return boundTabMutationResult(cmd, lockedSession);
2911
+ }
2912
+ if (cmd.index !== void 0) {
2913
+ const tabs = await listAutomationWebTabsUnlocked(leaseKey);
2914
+ const target = tabs[cmd.index];
2915
+ if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
2916
+ const closedPage2 = await resolveTargetId(target.id).catch(() => void 0);
2917
+ const currentSession2 = automationSessions.get(leaseKey);
2918
+ if (currentSession2?.preferredTabId === target.id) {
2919
+ await releaseLeaseUnlocked(leaseKey, "tab close");
2920
+ } else {
2921
+ await safeDetach(target.id);
2922
+ await chrome.tabs.remove(target.id);
2923
+ }
2924
+ return { id: cmd.id, ok: true, data: { closed: closedPage2 } };
2925
+ }
2926
+ const cmdTabId = await resolveCommandTabId(cmd);
2927
+ const tabId = await resolveTabIdUnlocked(cmdTabId, leaseKey);
2928
+ const closedPage = await resolveTargetId(tabId).catch(() => void 0);
2929
+ const currentSession = automationSessions.get(leaseKey);
2930
+ if (currentSession?.preferredTabId === tabId) {
2931
+ await releaseLeaseUnlocked(leaseKey, "tab close");
2932
+ } else {
2933
+ await safeDetach(tabId);
2934
+ await chrome.tabs.remove(tabId);
2935
+ }
2936
+ return { id: cmd.id, ok: true, data: { closed: closedPage } };
2937
+ }
2938
+ function boundTabMutationResult(cmd, session) {
2939
+ return {
2940
+ id: cmd.id,
2941
+ ok: false,
2942
+ errorCode: "bound_tab_mutation_blocked",
2943
+ error: `Session "${session.session}" is bound to a user tab; tab new/select/close/focus requires an owned byCLI session.`,
2944
+ errorHint: "Unbind the session first, or use a different session for owned byCLI tabs."
2945
+ };
2946
+ }
2947
+ async function handleTabSelectUnlocked(cmd, leaseKey) {
2948
+ const lockedSession = automationSessions.get(leaseKey);
2949
+ if (lockedSession && !lockedSession.owned) {
2950
+ return boundTabMutationResult(cmd, lockedSession);
2951
+ }
2952
+ if (cmd.index === void 0 && cmd.page === void 0) {
2953
+ return { id: cmd.id, ok: false, error: "Missing index or page" };
2954
+ }
2955
+ const cmdTabId = await resolveCommandTabId(cmd);
2956
+ if (cmdTabId !== void 0) {
2957
+ const session = automationSessions.get(leaseKey);
2958
+ let tab;
2959
+ try {
2960
+ tab = await chrome.tabs.get(cmdTabId);
2961
+ } catch {
2962
+ return { id: cmd.id, ok: false, error: "Page no longer exists" };
2963
+ }
2964
+ if (!session || tab.windowId !== session.windowId) {
2965
+ return { id: cmd.id, ok: false, error: "Page is not in the automation container" };
2966
+ }
2967
+ await chrome.tabs.update(cmdTabId, { active: true });
2968
+ return pageScopedResult(cmd.id, cmdTabId, { selected: true });
2969
+ }
2970
+ const tabs = await listAutomationWebTabsUnlocked(leaseKey);
2971
+ const target = tabs[cmd.index];
2972
+ if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` };
2973
+ await chrome.tabs.update(target.id, { active: true });
2974
+ return pageScopedResult(cmd.id, target.id, { selected: true });
2975
+ }
2976
+ async function handleTabs(cmd, leaseKey) {
2977
+ const session = automationSessions.get(leaseKey);
2978
+ if (session && !session.owned && cmd.op !== "list") {
2979
+ return boundTabMutationResult(cmd, session);
2980
+ }
2981
+ switch (cmd.op) {
2982
+ case "list": {
2983
+ const tabs = await listAutomationWebTabs(leaseKey);
2984
+ const data = await Promise.all(tabs.map(async (t, i) => {
2985
+ let page;
2986
+ try {
2987
+ page = t.id ? await resolveTargetId(t.id) : void 0;
2988
+ } catch {
2989
+ }
2990
+ return { index: i, page, url: t.url, title: t.title, active: t.active };
2991
+ }));
2992
+ return { id: cmd.id, ok: true, data };
2993
+ }
2994
+ case "new":
2995
+ return withLeaseLock(leaseKey, () => handleTabNewUnlocked(cmd, leaseKey));
2996
+ case "close":
2997
+ return withLeaseLock(leaseKey, () => handleTabCloseUnlocked(cmd, leaseKey));
2998
+ case "select":
2999
+ return withLeaseLock(leaseKey, () => handleTabSelectUnlocked(cmd, leaseKey));
3000
+ case "focus":
3001
+ return withLeaseLock(leaseKey, () => handleFocusUnlocked(cmd, leaseKey));
3002
+ default:
3003
+ return { id: cmd.id, ok: false, error: `Unknown tabs op: ${cmd.op}` };
3004
+ }
3005
+ }
3006
+ async function handleCookies(cmd) {
3007
+ if (!cmd.domain && !cmd.url) {
3008
+ return { id: cmd.id, ok: false, error: "Cookie scope required: provide domain or url to avoid dumping all cookies" };
3009
+ }
3010
+ const details = {};
3011
+ if (cmd.domain) details.domain = cmd.domain;
3012
+ if (cmd.url) details.url = cmd.url;
3013
+ const cookies = await chrome.cookies.getAll(details);
3014
+ const data = cookies.map((c) => ({
3015
+ name: c.name,
3016
+ value: c.value,
3017
+ domain: c.domain,
3018
+ path: c.path,
3019
+ secure: c.secure,
3020
+ httpOnly: c.httpOnly,
3021
+ expirationDate: c.expirationDate
3022
+ }));
3023
+ return { id: cmd.id, ok: true, data };
3024
+ }
3025
+ async function handleScreenshot(cmd, leaseKey) {
3026
+ const cmdTabId = await resolveCommandTabId(cmd);
3027
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3028
+ try {
3029
+ const data = await screenshot(tabId, {
3030
+ format: cmd.format,
3031
+ quality: cmd.quality,
3032
+ fullPage: cmd.fullPage,
3033
+ width: cmd.width,
3034
+ height: cmd.height
3035
+ });
3036
+ return pageScopedResult(cmd.id, tabId, data);
3037
+ } catch (err) {
3038
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3039
+ }
3040
+ }
3041
+ const CDP_ALLOWLIST = /* @__PURE__ */ new Set([
3042
+ // Agent DOM context
3043
+ "Accessibility.enable",
3044
+ "Accessibility.getFullAXTree",
3045
+ "DOM.enable",
3046
+ "DOM.getDocument",
3047
+ "DOM.getBoxModel",
3048
+ "DOM.getContentQuads",
3049
+ "DOM.focus",
3050
+ "DOM.querySelector",
3051
+ "DOM.querySelectorAll",
3052
+ "DOM.scrollIntoViewIfNeeded",
3053
+ "DOMSnapshot.captureSnapshot",
3054
+ // Native input events
3055
+ "Input.dispatchMouseEvent",
3056
+ "Input.dispatchKeyEvent",
3057
+ "Input.insertText",
3058
+ // 投屏滚动:synthesizeScrollGesture 真正驱动合成器滚动(dispatchMouseEvent mouseWheel 只发事件、很多页面不滚)。
3059
+ "Input.synthesizeScrollGesture",
3060
+ // Page metrics & screenshots
3061
+ "Page.getLayoutMetrics",
3062
+ "Page.captureScreenshot",
3063
+ "Page.getFrameTree",
3064
+ "Page.handleJavaScriptDialog",
3065
+ // Runtime.enable needed for CDP attach setup (Runtime.evaluate goes through 'exec' action)
3066
+ "Runtime.enable",
3067
+ // Emulation (used by screenshot full-page)
3068
+ "Emulation.setDeviceMetricsOverride",
3069
+ "Emulation.clearDeviceMetricsOverride"
3070
+ ]);
3071
+ async function handleCdp(cmd, leaseKey) {
3072
+ if (!cmd.cdpMethod) return { id: cmd.id, ok: false, error: "Missing cdpMethod" };
3073
+ if (!CDP_ALLOWLIST.has(cmd.cdpMethod)) {
3074
+ return { id: cmd.id, ok: false, error: `CDP method not permitted: ${cmd.cdpMethod}` };
3075
+ }
3076
+ const cmdTabId = await resolveCommandTabId(cmd);
3077
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3078
+ try {
3079
+ const aggressive = getSurfaceFromKey(leaseKey) === "browser";
3080
+ await ensureAttached(tabId, aggressive);
3081
+ const params = cmd.cdpParams ?? {};
3082
+ const routeFrameId = typeof params.frameId === "string" && params.sessionId === "target" ? params.frameId : void 0;
3083
+ const routeTargetUrl = typeof params.targetUrl === "string" ? params.targetUrl : void 0;
3084
+ const data = routeFrameId ? await sendCommandInFrameTarget(tabId, routeFrameId, cmd.cdpMethod, stripbyCliFrameRoutingParams(params, true), aggressive, 3e4, routeTargetUrl) : await chrome.debugger.sendCommand(
3085
+ { tabId },
3086
+ cmd.cdpMethod,
3087
+ stripbyCliFrameRoutingParams(params, false)
3088
+ );
3089
+ return pageScopedResult(cmd.id, tabId, data);
3090
+ } catch (err) {
3091
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3092
+ }
3093
+ }
3094
+ function stripbyCliFrameRoutingParams(params, stripFrameId) {
3095
+ const { sessionId, frameId, targetUrl, ...rest } = params;
3096
+ if (!stripFrameId && frameId !== void 0) return { ...rest, frameId };
3097
+ return rest;
3098
+ }
3099
+ async function handleCloseWindow(cmd, leaseKey) {
3100
+ const sessionName = automationSessions.get(leaseKey)?.session ?? getSessionFromKey(leaseKey);
3101
+ await releaseLease(leaseKey, "explicit close");
3102
+ return { id: cmd.id, ok: true, data: { closed: true, session: sessionName } };
3103
+ }
3104
+ async function handleSetFileInput(cmd, leaseKey) {
3105
+ if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) {
3106
+ return { id: cmd.id, ok: false, error: "Missing or empty files array" };
3107
+ }
3108
+ const cmdTabId = await resolveCommandTabId(cmd);
3109
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3110
+ try {
3111
+ await setFileInputFiles(tabId, cmd.files, cmd.selector);
3112
+ return pageScopedResult(cmd.id, tabId, { count: cmd.files.length });
3113
+ } catch (err) {
3114
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3115
+ }
3116
+ }
3117
+ async function handleInsertText(cmd, leaseKey) {
3118
+ if (typeof cmd.text !== "string") {
3119
+ return { id: cmd.id, ok: false, error: "Missing text payload" };
3120
+ }
3121
+ const cmdTabId = await resolveCommandTabId(cmd);
3122
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3123
+ try {
3124
+ await insertText(tabId, cmd.text);
3125
+ return pageScopedResult(cmd.id, tabId, { inserted: true });
3126
+ } catch (err) {
3127
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3128
+ }
3129
+ }
3130
+ async function handleNetworkCaptureStart(cmd, leaseKey) {
3131
+ const cmdTabId = await resolveCommandTabId(cmd);
3132
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3133
+ try {
3134
+ await startNetworkCapture(tabId, cmd.pattern);
3135
+ return pageScopedResult(cmd.id, tabId, { started: true });
3136
+ } catch (err) {
3137
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3138
+ }
3139
+ }
3140
+ function errorCodeOf(err) {
3141
+ const c = err?.code;
3142
+ return typeof c === "string" ? c : void 0;
3143
+ }
3144
+ async function handleNetworkCaptureRead(cmd, leaseKey) {
3145
+ const cmdTabId = await resolveCommandTabId(cmd);
3146
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3147
+ try {
3148
+ const data = await readNetworkCapture(tabId, cmd.targetFrameUrl ? { targetFrameUrl: cmd.targetFrameUrl } : void 0);
3149
+ return pageScopedResult(cmd.id, tabId, data);
3150
+ } catch (err) {
3151
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err), errorCode: errorCodeOf(err) };
3152
+ }
3153
+ }
3154
+ async function handleImaAuthStart(cmd, leaseKey) {
3155
+ const cmdTabId = await resolveCommandTabId(cmd);
3156
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3157
+ await startImaReaderAuthCapture(tabId);
3158
+ return pageScopedResult(cmd.id, tabId, { started: true });
3159
+ }
3160
+ async function handleImaAuthRead(cmd, leaseKey) {
3161
+ const cmdTabId = await resolveCommandTabId(cmd);
3162
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3163
+ return pageScopedResult(cmd.id, tabId, readImaReaderAuth(tabId));
3164
+ }
3165
+ async function handleImaReaderRequest(cmd, leaseKey) {
3166
+ if (!cmd.authId || !cmd.readerPath || !cmd.readerBody) {
3167
+ return { id: cmd.id, ok: false, error: "Missing ima reader request payload" };
3168
+ }
3169
+ const cmdTabId = await resolveCommandTabId(cmd);
3170
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3171
+ const data = await requestImaReader(tabId, cmd.authId, cmd.readerPath, cmd.readerBody);
3172
+ return pageScopedResult(cmd.id, tabId, data);
3173
+ }
3174
+ async function handleImaMediaRequest(cmd, leaseKey) {
3175
+ if (!cmd.authId || !cmd.mediaBody) return { id: cmd.id, ok: false, error: "Missing ima media request payload" };
3176
+ const cmdTabId = await resolveCommandTabId(cmd);
3177
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3178
+ try {
3179
+ const data = await requestImaMedia(tabId, cmd.authId, cmd.mediaBody);
3180
+ return pageScopedResult(cmd.id, tabId, data);
3181
+ } catch (err) {
3182
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3183
+ }
3184
+ }
3185
+ async function handleImaAuthRelease(cmd, leaseKey) {
3186
+ if (!cmd.authId) return { id: cmd.id, ok: false, error: "Missing ima reader auth ID" };
3187
+ const cmdTabId = await resolveCommandTabId(cmd);
3188
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3189
+ releaseImaReaderAuth(cmd.authId);
3190
+ return pageScopedResult(cmd.id, tabId, { released: true });
3191
+ }
3192
+ async function handleUiCaptureStart(cmd, leaseKey) {
3193
+ const cmdTabId = await resolveCommandTabId(cmd);
3194
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3195
+ try {
3196
+ await startUiCapture(tabId);
3197
+ return pageScopedResult(cmd.id, tabId, { started: true });
3198
+ } catch (err) {
3199
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3200
+ }
3201
+ }
3202
+ async function handleUiCaptureRead(cmd, leaseKey) {
3203
+ const cmdTabId = await resolveCommandTabId(cmd);
3204
+ const tabId = await resolveTabId(cmdTabId, leaseKey);
3205
+ try {
3206
+ const data = await readUiCapture(tabId, cmd.targetFrameUrl ? { targetFrameUrl: cmd.targetFrameUrl } : void 0);
3207
+ return pageScopedResult(cmd.id, tabId, data);
3208
+ } catch (err) {
3209
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err), errorCode: errorCodeOf(err) };
3210
+ }
3211
+ }
3212
+ async function handleWaitDownload(cmd) {
3213
+ try {
3214
+ const data = await waitForDownload(
3215
+ cmd.pattern ?? "",
3216
+ cmd.timeoutMs ?? 3e4,
3217
+ {
3218
+ includeRecent: cmd.includeRecent ?? true,
3219
+ ...cmd.startedAfterMs === void 0 ? {} : { startedAfterMs: cmd.startedAfterMs }
3220
+ }
3221
+ );
3222
+ return { id: cmd.id, ok: true, data };
3223
+ } catch (err) {
3224
+ return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
3225
+ }
3226
+ }
3227
+ async function releaseLease(leaseKey, reason = "released") {
3228
+ return withLeaseLock(leaseKey, () => releaseLeaseUnlocked(leaseKey, reason));
3229
+ }
3230
+ async function releaseLeaseUnlocked(leaseKey, reason) {
3231
+ const session = automationSessions.get(leaseKey);
3232
+ if (!session) {
3233
+ sessionTimeoutOverrides.delete(leaseKey);
3234
+ sessionWindowModeOverrides.delete(leaseKey);
3235
+ sessionLifecycleOverrides.delete(leaseKey);
3236
+ cancelIdleExpiry(leaseKey);
3237
+ await persistRuntimeState();
3238
+ return;
3239
+ }
3240
+ if (session.idleTimer) clearTimeout(session.idleTimer);
3241
+ cancelIdleExpiry(leaseKey);
3242
+ if (session.owned) {
3243
+ const tabId = session.preferredTabId;
3244
+ if (tabId !== null) {
3245
+ const hasOtherOwnedLease = [...automationSessions.entries()].some(
3246
+ ([otherLease, otherSession]) => otherLease !== leaseKey && otherSession.owned && otherSession.windowId === session.windowId && otherSession.preferredTabId !== null
3247
+ );
3248
+ await safeDetach(tabId);
3249
+ evictTab(tabId);
3250
+ if (hasOtherOwnedLease) {
3251
+ await chrome.tabs.remove(tabId).catch(() => {
3252
+ });
3253
+ console.log(`[bycli] Released owned tab lease ${tabId} (session=${session.session}, surface=${session.surface}, ${reason})`);
3254
+ } else {
3255
+ try {
3256
+ const tab = await chrome.tabs.update(tabId, {
3257
+ url: BLANK_PAGE,
3258
+ active: getWindowModeForOwnedPage(leaseKey, BLANK_PAGE) === "foreground"
3259
+ });
3260
+ await ensureOwnedContainerTabGroup(getOwnedWindowRole(leaseKey), session.windowId, [tab.id ?? tabId]);
3261
+ console.log(`[bycli] Released owned tab lease ${tabId} as reusable placeholder (session=${session.session}, surface=${session.surface}, ${reason})`);
3262
+ } catch {
3263
+ await chrome.tabs.remove(tabId).catch(() => {
3264
+ });
3265
+ console.log(`[bycli] Released owned tab lease ${tabId} (session=${session.session}, surface=${session.surface}, ${reason})`);
3266
+ }
3267
+ }
3268
+ } else {
3269
+ console.log(`[bycli] Released legacy owned window lease ${session.windowId} without closing container (session=${session.session}, surface=${session.surface}, ${reason})`);
3270
+ }
3271
+ } else if (session.preferredTabId !== null) {
3272
+ await safeDetach(session.preferredTabId);
3273
+ console.log(`[bycli] Detached borrowed tab lease ${session.preferredTabId} (session=${session.session}, surface=${session.surface}, ${reason})`);
3274
+ }
3275
+ if (automationSessions.get(leaseKey) === session) {
3276
+ deleteAutomationSession(leaseKey);
3277
+ sessionTimeoutOverrides.delete(leaseKey);
3278
+ sessionWindowModeOverrides.delete(leaseKey);
3279
+ sessionLifecycleOverrides.delete(leaseKey);
3280
+ }
3281
+ await persistRuntimeState();
3282
+ }
3283
+ async function reconcileTargetLeaseRegistry() {
3284
+ const registry = await readRegistry();
3285
+ for (const role of Object.keys(ownedContainers)) {
3286
+ ownedContainers[role].windowId = registry.ownedContainers[role]?.windowId ?? null;
3287
+ ownedContainers[role].groupId = registry.ownedContainers[role]?.groupId ?? null;
3288
+ const windowId = ownedContainers[role].windowId;
3289
+ if (windowId !== null) {
3290
+ try {
3291
+ await chrome.windows.get(windowId);
3292
+ } catch {
3293
+ ownedContainers[role].windowId = null;
3294
+ ownedContainers[role].groupId = null;
3295
+ }
3296
+ }
3297
+ }
3298
+ clearAutomationSessions();
3299
+ for (const [leaseKey, stored] of Object.entries(registry.leases)) {
3300
+ const tabId = stored.preferredTabId;
3301
+ if (tabId === null) continue;
3302
+ try {
3303
+ const tab = await chrome.tabs.get(tabId);
3304
+ if (!isDebuggableUrl(tab.url)) continue;
3305
+ if (stored.lifecycle === "ephemeral" || stored.lifecycle === "persistent" || stored.lifecycle === "pinned") {
3306
+ sessionLifecycleOverrides.set(leaseKey, stored.lifecycle);
3307
+ }
3308
+ const session = makeSession(leaseKey, {
3309
+ session: typeof stored.session === "string" ? stored.session : getSessionFromKey(leaseKey),
3310
+ surface: stored.surface === "adapter" ? "adapter" : getSurfaceFromKey(leaseKey),
3311
+ kind: stored.kind === "bound" || stored.owned === false ? "bound" : "owned",
3312
+ windowId: tab.windowId,
3313
+ owned: stored.owned,
3314
+ preferredTabId: tabId
3315
+ });
3316
+ const timeout = getIdleTimeout(leaseKey);
3317
+ storeAutomationSession(leaseKey, {
3318
+ ...session,
3319
+ idleTimer: null,
3320
+ idleDeadlineAt: stored.idleDeadlineAt
3321
+ });
3322
+ if (session.owned) {
3323
+ const role = getOwnedWindowRole(leaseKey);
3324
+ if (ownedContainers[role].windowId === null) ownedContainers[role].windowId = tab.windowId;
3325
+ await ensureOwnedContainerTabGroup(role, tab.windowId, [tabId]);
3326
+ }
3327
+ const remaining = stored.idleDeadlineAt > 0 ? stored.idleDeadlineAt - Date.now() : timeout;
3328
+ if (timeout > 0) {
3329
+ if (remaining <= 0) {
3330
+ await releaseLease(leaseKey, "reconciled idle expiry");
3331
+ } else {
3332
+ resetWindowIdleTimer(leaseKey);
3333
+ }
3334
+ }
3335
+ } catch {
3336
+ }
3337
+ }
3338
+ await persistRuntimeState();
3339
+ }
3340
+ async function handleBind(cmd, leaseKey) {
3341
+ return withLeaseLock(leaseKey, () => handleBindUnlocked(cmd, leaseKey));
3342
+ }
3343
+ async function handleBindUnlocked(cmd, leaseKey) {
3344
+ const existing = automationSessions.get(leaseKey);
3345
+ if (existing?.owned) {
3346
+ await releaseLeaseUnlocked(leaseKey, "rebind");
3347
+ }
3348
+ const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
3349
+ const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
3350
+ const boundTab = activeTabs.find((tab) => isDebuggableUrl(tab.url)) ?? fallbackTabs.find((tab) => isDebuggableUrl(tab.url));
3351
+ if (!boundTab?.id) {
3352
+ return {
3353
+ id: cmd.id,
3354
+ ok: false,
3355
+ errorCode: "bound_tab_not_found",
3356
+ error: "No debuggable tab found in the current window",
3357
+ errorHint: "Focus the target Chrome tab/window, then retry bind."
3358
+ };
3359
+ }
3360
+ const current = automationSessions.get(leaseKey);
3361
+ if (current && !current.owned && current.preferredTabId !== null && current.preferredTabId !== boundTab.id) {
3362
+ await detach(current.preferredTabId).catch(() => {
3363
+ });
3364
+ }
3365
+ setLeaseSession(leaseKey, {
3366
+ session: getSessionFromKey(leaseKey),
3367
+ surface: getSurfaceFromKey(leaseKey),
3368
+ kind: "bound",
3369
+ windowId: boundTab.windowId,
3370
+ owned: false,
3371
+ preferredTabId: boundTab.id
3372
+ });
3373
+ resetWindowIdleTimer(leaseKey);
3374
+ console.log(`[bycli] Session ${getSessionFromKey(leaseKey)} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
3375
+ return pageScopedResult(cmd.id, boundTab.id, {
3376
+ url: boundTab.url,
3377
+ title: boundTab.title,
3378
+ session: getSessionFromKey(leaseKey)
3379
+ });
3380
+ }