@xbrowser/cli 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{browser-XNU7JQYC.js → browser-AQ2AT2YZ.js} +2 -2
  2. package/dist/{browser-Q5APBNF6.js → browser-OMGJMSBB.js} +1 -1
  3. package/dist/{browser-V3JIWSTR.js → browser-T7OVLAEC.js} +4 -3
  4. package/dist/{cdp-driver-XFTTZI5O.js → cdp-driver-ALDNOZYD.js} +1176 -13
  5. package/dist/{cdp-driver-GD6YBBGE.js → cdp-driver-HYCLE4YP.js} +2 -1
  6. package/dist/{cdp-driver-VEK6BNN6.js → cdp-driver-VS7Z7W7Y.js} +1 -1
  7. package/dist/chunk-A6LPGFAL.js +437 -0
  8. package/dist/{chunk-JEDP4PJW.js → chunk-BY5TT6WZ.js} +912 -688
  9. package/dist/{chunk-XKQVUFUS.js → chunk-CW7L53JE.js} +2 -2
  10. package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
  11. package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
  12. package/dist/chunk-HD4FX2N5.js +1255 -0
  13. package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
  14. package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
  15. package/dist/{chunk-7OFM755Z.js → chunk-R2POF2GP.js} +236 -13
  16. package/dist/{chunk-WHPBNUKB.js → chunk-SH6OTPXN.js} +42 -46
  17. package/dist/{chunk-XQ4HRPDJ.js → chunk-T2CNKWPV.js} +1177 -13
  18. package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
  19. package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
  20. package/dist/cli.js +1075 -569
  21. package/dist/{daemon-client-MMDRYCF5.js → daemon-client-7D6EZNOE.js} +42 -46
  22. package/dist/{daemon-client-Y2YSZCGK.js → daemon-client-HEGAXWGK.js} +1 -1
  23. package/dist/daemon-main.js +950 -499
  24. package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
  25. package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
  26. package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +1086 -578
  29. package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
  30. package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
  31. package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
  32. package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
  33. package/dist/{session-replayer-WIUYVN5J.js → session-replayer-UVH5FGY2.js} +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-HVPUVVSA.js +0 -2194
@@ -0,0 +1,1255 @@
1
+ import {
2
+ createRuleEngine,
3
+ launch
4
+ } from "./chunk-T2CNKWPV.js";
5
+ import {
6
+ errMsg
7
+ } from "./chunk-GDKLH7ZY.js";
8
+
9
+ // src/browser.ts
10
+ import { randomUUID } from "crypto";
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+
15
+ // src/cdp-interceptor/proxy.ts
16
+ import { WebSocketServer, WebSocket } from "ws";
17
+
18
+ // src/cdp-interceptor/logger.ts
19
+ function createLogger(config) {
20
+ const buffer = [];
21
+ const MAX_BUFFER = 2e3;
22
+ return {
23
+ info(message, meta) {
24
+ if (!config.enableLogging) return;
25
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
26
+ if (meta) {
27
+ console.log(`[CDPInterceptor ${ts}] ${message}`, JSON.stringify(meta));
28
+ } else {
29
+ console.log(`[CDPInterceptor ${ts}] ${message}`);
30
+ }
31
+ },
32
+ log(method, direction, sessionId, payload, decision) {
33
+ const entry = {
34
+ timestamp: Date.now(),
35
+ direction,
36
+ sessionId,
37
+ method,
38
+ payload: sanitizePayload(payload),
39
+ decision: decision ?? void 0
40
+ };
41
+ if (config.enableLogging) {
42
+ buffer.push(entry);
43
+ if (buffer.length > MAX_BUFFER) buffer.shift();
44
+ const tag = decision ? decision.action === "block" ? "\u{1F6AB}BLOCK" : decision.action === "transform" ? "\u{1F504}XFMR" : "\u2705" : " ";
45
+ const reason = decision ? ` [${decision.severity}] ${decision.reason}` : "";
46
+ console.log(`[CDP] ${tag} ${direction} ${method}${reason}`);
47
+ }
48
+ return entry;
49
+ },
50
+ getRecent(count) {
51
+ return buffer.slice(-count);
52
+ },
53
+ flush() {
54
+ buffer.length = 0;
55
+ }
56
+ };
57
+ }
58
+ function sanitizePayload(payload) {
59
+ if (typeof payload !== "object" || payload === null) return { raw: String(payload) };
60
+ const obj = payload;
61
+ const cleaned = {};
62
+ for (const [key, value] of Object.entries(obj)) {
63
+ if (key === "data" && typeof value === "string" && value.length > 200) {
64
+ cleaned[key] = `<binary: ${value.length} chars>`;
65
+ } else if (key === "expression" && typeof value === "string" && value.length > 500) {
66
+ cleaned[key] = value.substring(0, 500) + "...";
67
+ } else {
68
+ cleaned[key] = value;
69
+ }
70
+ }
71
+ return cleaned;
72
+ }
73
+
74
+ // src/cdp-interceptor/advisor.ts
75
+ function formatBlockMessage(decision, method) {
76
+ const adv = advise(decision, method);
77
+ const lines = [
78
+ `[CDP-FIREWALL-BLOCK] rule=${decision.ruleId}`,
79
+ `method=${method}`,
80
+ `reason=${decision.reason}`,
81
+ `suggestion=${decision.suggestion ?? adv.detail}`
82
+ ];
83
+ if (adv.codeExample) {
84
+ lines.push(`code-example=`);
85
+ lines.push(adv.codeExample);
86
+ }
87
+ return lines.join("\n");
88
+ }
89
+ function advise(decision, originalMethod) {
90
+ const baseAdvice = getBaseAdvice(decision, originalMethod);
91
+ return {
92
+ ruleId: decision.ruleId,
93
+ title: baseAdvice.title,
94
+ detail: decision.suggestion ?? baseAdvice.detail,
95
+ codeExample: baseAdvice.codeExample
96
+ };
97
+ }
98
+ function getBaseAdvice(decision, method) {
99
+ switch (decision.ruleId) {
100
+ case "dom-mutation":
101
+ return {
102
+ title: "Direct DOM property mutation blocked",
103
+ detail: `Your ${method} call tried to set a DOM property directly. In React/Vue/Angular, this bypasses the framework's virtual DOM completely meaning onChange/onInput never fires. The website CAN detect this mismatch as automation.`,
104
+ codeExample: [
105
+ "# \u274C BLOCKED \u2014 what you tried to do:",
106
+ `page.evaluate(\`el.value = 'hello'\`) # triggers isTrusted=false`,
107
+ "",
108
+ "# \u2705 USE INSTEAD \u2014 proper CDP input dispatch:",
109
+ "page.fill('#selector', 'hello') # Playwright: dispatches input+change events",
110
+ "page.type('#selector', 'hello', {delay}) # Playwright: real keystrokes",
111
+ "page.locator('#selector').fill('hello') # Playwright: recommended API"
112
+ ].join("\n")
113
+ };
114
+ case "mouse-trajectory":
115
+ return {
116
+ title: "Unnatural mouse trajectory blocked",
117
+ detail: `Your ${method} sequence formed a perfectly linear path. Human hands have micro-tremors (1-3px variation at any point along the arc), acceleration curves, and never draw straight lines between distant points.`,
118
+ codeExample: [
119
+ "# \u274C BLOCKED \u2014 linear interpolation:",
120
+ "for i in range(20):",
121
+ " page.mouse.move(x0 + (x1-x0)*(i/20), y0 + (y1-y0)*(i/20))",
122
+ "",
123
+ "# \u2705 USE INSTEAD \u2014 Bezier curves with overshoot:",
124
+ '# Use "ghost-cursor" or similar library',
125
+ "from ghost_cursor import path_to",
126
+ "path_to(page, (x1, y1))"
127
+ ].join("\n")
128
+ };
129
+ case "input-keystroke":
130
+ if (method === "Input.insertText") {
131
+ return {
132
+ title: "Input.insertText detected (logged, not blocked)",
133
+ detail: "Input.insertText bypasses native keyDown\u2192keyPress\u2192input\u2192keyUp events. Playwright uses this internally for page.fill(). Logged for observability."
134
+ };
135
+ }
136
+ return {
137
+ title: "Unnatural keystroke timing blocked",
138
+ detail: `Your ${method} calls have unnaturally constant timing (e.g., exact 50ms intervals). Human typing always has variation (CV > 0.2).`,
139
+ codeExample: [
140
+ "# \u274C BLOCKED \u2014 exact constant delay:",
141
+ "page.type('#input', 'hello', {delay: 50}) # every keystroke exactly 50ms apart",
142
+ "",
143
+ "# \u2705 USE INSTEAD \u2014 variable delay (human-like):",
144
+ "page.fill('#input', 'hello') # recommended, dispatches events properly",
145
+ "# OR: type with randomized delay",
146
+ "page.type('#input', 'hello', {delay: 50 + Math.floor(Math.random() * 80)})"
147
+ ].join("\n")
148
+ };
149
+ case "automation-signals":
150
+ return {
151
+ title: "Browser automation marker detected",
152
+ detail: `Your ${method} call accessed a property/marker that anti-crawler systems check to detect automation. These markers (navigator.webdriver, window.__playwright, etc.) are the #1 detection vector.`,
153
+ codeExample: [
154
+ "# \u274C BLOCKED \u2014 don't check for automation markers:",
155
+ "navigator.webdriver # NEVER check this",
156
+ "window.__playwright # NEVER check this",
157
+ "chrome.runtime # NEVER check this",
158
+ "",
159
+ "# \u2705 Just go about your business normally.",
160
+ "# Anti-detection is handled by the CDP firewall automatically."
161
+ ].join("\n")
162
+ };
163
+ case "fingerprinting":
164
+ return {
165
+ title: "Browser fingerprinting API access blocked",
166
+ detail: `Your ${method} call accessed a known fingerprinting API. These APIs (canvas.toDataURL, WebGL getParameter, AudioContext, etc.) are used by anti-crawler systems to build a unique device fingerprint.`,
167
+ codeExample: [
168
+ "# \u274C BLOCKED \u2014 fingerprinting vector:",
169
+ "canvas.toDataURL() # returns unique browser hash",
170
+ 'gl.getParameter(gl.VENDOR) # returns "SwiftShader" in headless',
171
+ "screen.availWidth - screen.availHeight # no OS chrome in headless",
172
+ "",
173
+ "# \u2705 Avoid accessing these APIs. They are only used for fingerprinting."
174
+ ].join("\n")
175
+ };
176
+ case "event-simulation":
177
+ return {
178
+ title: "Synthetic event simulation blocked",
179
+ detail: `Your ${method} call simulated user interaction via el.click() or dispatchEvent(new Event(...)). These produce isTrusted=false events, which are 100% detectable by any anti-crawler that checks isTrusted on critical events.`,
180
+ codeExample: [
181
+ "# \u274C BLOCKED \u2014 synthetic events (isTrusted=false):",
182
+ "el.click() # isTrusted=false",
183
+ 'el.dispatchEvent(new Event("click")) # isTrusted=false',
184
+ "el.focus() # isTrusted=false",
185
+ "",
186
+ "# \u2705 USE INSTEAD \u2014 CDP-level input dispatch (isTrusted=true):",
187
+ "page.click(selector) # uses Input.dispatchMouseEvent",
188
+ "page.fill(selector, value) # dispatches real input events"
189
+ ].join("\n")
190
+ };
191
+ case "emulation-override":
192
+ return {
193
+ title: "CDP emulation override blocked",
194
+ detail: `Your ${method} call overrides browser behavior in a way that creates detectable inconsistencies. Anti-crawler systems cross-check multiple sources (e.g., navigator.userAgent vs HTTP User-Agent header) to catch these mismatches.`,
195
+ codeExample: [
196
+ "# \u274C BLOCKED \u2014 detectable emulation override:",
197
+ "Emulation.setUserAgentOverride(...) # JS vs HTTP header mismatch",
198
+ "Emulation.setGeolocationOverride(...) # IP geo vs overridden geo mismatch",
199
+ "Emulation.setDeviceMetricsOverride(...) # matchMedia vs actual viewport",
200
+ "",
201
+ "# \u2705 These are handled automatically by the CDP firewall.",
202
+ "# Do NOT call them manually."
203
+ ].join("\n")
204
+ };
205
+ case "network-anomaly":
206
+ return {
207
+ title: "Network anomaly detected",
208
+ detail: `Your ${method} call triggered a network pattern that is characteristic of scrapers: blocking URLs, clearing caches, or intercepting requests.`,
209
+ codeExample: [
210
+ "# \u274C BLOCKED \u2014 scraper optimization:",
211
+ "Network.clearBrowserCache() # natural users never do this",
212
+ 'Network.setBlockedURLs(["*fonts*"]) # blocking resources is detectable',
213
+ "Fetch.enable() # MITM-style interception",
214
+ "",
215
+ "# \u2705 Let the browser manage its own cache and network normally."
216
+ ].join("\n")
217
+ };
218
+ case "page-lifecycle":
219
+ return {
220
+ title: "Suspicious page lifecycle pattern blocked",
221
+ detail: `Your ${method} call reveals an unnatural page interaction sequence: navigating too fast, taking screenshots before the page renders, or generating PDFs (a telltale scraper giveaway).`,
222
+ codeExample: [
223
+ "# \u274C BLOCKED \u2014 unnatural lifecycle:",
224
+ "page.goto(url); page.pdf() # PDF = scraper giveaway",
225
+ "page.goto(url); page.screenshot() <500ms # screenshot before render",
226
+ "page.goto(url) 3x in <100ms # rapid navigation barrage",
227
+ "",
228
+ "# \u2705 Add proper waits between actions:",
229
+ 'page.goto(url, {waitUntil: "networkidle"})',
230
+ 'page.waitForSelector("body")',
231
+ "page.screenshot() # after rendering"
232
+ ].join("\n")
233
+ };
234
+ default:
235
+ return {
236
+ title: decision.reason,
237
+ detail: decision.suggestion ?? `The CDP call "${method}" was blocked by rule "${decision.ruleId}".`
238
+ };
239
+ }
240
+ }
241
+
242
+ // src/cdp-interceptor/proxy.ts
243
+ function makeCompoundId(cdpSessionId, rawSessionId) {
244
+ return `${cdpSessionId ?? "nil"}::${rawSessionId ?? "nil"}`;
245
+ }
246
+ var CDPInterceptorProxy = class {
247
+ wss = null;
248
+ engine;
249
+ config;
250
+ logger;
251
+ started = false;
252
+ stats = {
253
+ totalMessages: 0,
254
+ blockedMessages: 0,
255
+ transformedMessages: 0,
256
+ passedMessages: 0,
257
+ byRule: {}
258
+ };
259
+ constructor(config) {
260
+ this.config = config;
261
+ this.engine = createRuleEngine(config.rules);
262
+ this.logger = createLogger({
263
+ enableLogging: config.enableLogging ?? true,
264
+ logDir: config.logDir
265
+ });
266
+ }
267
+ /** The port the proxy is listening on (only valid after start()) */
268
+ get port() {
269
+ const addr = this.wss?.address();
270
+ if (addr && typeof addr === "object") return addr.port;
271
+ return 0;
272
+ }
273
+ /** Start the proxy server */
274
+ async start() {
275
+ if (this.started) return this.port;
276
+ return new Promise((resolve, reject) => {
277
+ this.wss = new WebSocketServer({ port: this.config.listenPort ?? 0 }, () => {
278
+ const port = this.port;
279
+ this.engine.start();
280
+ this.started = true;
281
+ this.logger.info("CDP interceptor proxy started", { port, endpoint: this.config.cdpEndpoint });
282
+ resolve(port);
283
+ });
284
+ this.wss.on("error", reject);
285
+ this.wss.on("connection", (clientWs, _req) => {
286
+ this.handleConnection(clientWs);
287
+ });
288
+ });
289
+ }
290
+ /** Stop the proxy server */
291
+ async stop() {
292
+ this.engine.stop();
293
+ this.logger.flush();
294
+ this.started = false;
295
+ return new Promise((resolve) => {
296
+ if (!this.wss) return resolve();
297
+ this.wss.close(() => resolve());
298
+ this.wss = null;
299
+ });
300
+ }
301
+ /** Get accumulated statistics */
302
+ getStats() {
303
+ return { ...this.stats };
304
+ }
305
+ /** Get recent log entries (for inspection) */
306
+ getRecentLogs(count = 50) {
307
+ return this.logger.getRecent(count);
308
+ }
309
+ // ── Connection handling ──────────────────────────────────────
310
+ handleConnection(clientWs) {
311
+ let browserWs = null;
312
+ let isAlive = true;
313
+ const pendingMessages = [];
314
+ browserWs = new WebSocket(this.config.cdpEndpoint);
315
+ clientWs.on("message", (raw) => {
316
+ if (browserWs && browserWs.readyState === WebSocket.OPEN) {
317
+ this.handleClientMessage(clientWs, browserWs, raw);
318
+ } else {
319
+ pendingMessages.push(raw);
320
+ }
321
+ });
322
+ browserWs.on("open", () => {
323
+ for (const buf of pendingMessages) {
324
+ this.handleClientMessage(clientWs, browserWs, buf);
325
+ }
326
+ pendingMessages.length = 0;
327
+ });
328
+ browserWs.on("error", (err) => {
329
+ this.logger.info("Browser WebSocket error", { error: String(err) });
330
+ });
331
+ browserWs.on("close", (code, reason) => {
332
+ if (isAlive && clientWs.readyState === WebSocket.OPEN) {
333
+ this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
334
+ clientWs.close();
335
+ }
336
+ });
337
+ browserWs.on("message", (raw) => {
338
+ this.handleBrowserMessage(clientWs, browserWs, raw);
339
+ });
340
+ const cleanup = () => {
341
+ isAlive = false;
342
+ if (browserWs && browserWs.readyState === WebSocket.OPEN) {
343
+ browserWs.close();
344
+ }
345
+ };
346
+ clientWs.on("close", cleanup);
347
+ clientWs.on("error", cleanup);
348
+ browserWs.on("close", () => {
349
+ if (isAlive && clientWs.readyState === WebSocket.OPEN) {
350
+ clientWs.close();
351
+ }
352
+ });
353
+ browserWs.on("error", cleanup);
354
+ }
355
+ // ── Message processing ───────────────────────────────────────
356
+ handleClientMessage(clientWs, browserWs, raw) {
357
+ const msg = this.parseMessage(raw);
358
+ if (!msg) return;
359
+ this.stats.totalMessages++;
360
+ if (!("method" in msg)) {
361
+ browserWs.send(raw.toString());
362
+ return;
363
+ }
364
+ const request = msg;
365
+ const ctx = {
366
+ method: request.method,
367
+ params: request.params ?? {},
368
+ sessionId: makeCompoundId("_cdpSession" in browserWs ? browserWs._cdpSession : void 0, request.sessionId),
369
+ direction: "client\u2192browser"
370
+ };
371
+ const decision = this.engine.evaluate(ctx);
372
+ this.logger.log(ctx.method, "client\u2192browser", ctx.sessionId, { method: ctx.method, params: ctx.params }, decision);
373
+ if (decision) {
374
+ this.recordDecision(decision);
375
+ }
376
+ if (decision?.action === "block") {
377
+ const blockMsg = formatBlockMessage(decision, ctx.method);
378
+ const errorResponse = {
379
+ id: request.id,
380
+ error: {
381
+ code: decision.errorCode ?? -32e3,
382
+ message: blockMsg
383
+ },
384
+ sessionId: request.sessionId
385
+ };
386
+ this.stats.blockedMessages++;
387
+ console.error(`
388
+ ${blockMsg}
389
+ `);
390
+ clientWs.send(JSON.stringify(errorResponse));
391
+ return;
392
+ }
393
+ if (decision?.action === "transform" && decision.transformedParams) {
394
+ const transformed = { ...request, params: decision.transformedParams };
395
+ this.stats.transformedMessages++;
396
+ browserWs.send(JSON.stringify(transformed));
397
+ return;
398
+ }
399
+ this.stats.passedMessages++;
400
+ browserWs.send(raw.toString());
401
+ }
402
+ handleBrowserMessage(clientWs, _browserWs, raw) {
403
+ const msg = this.parseMessage(raw);
404
+ if (!msg) {
405
+ clientWs.send(raw.toString());
406
+ return;
407
+ }
408
+ if ("method" in msg) {
409
+ const event = msg;
410
+ const ctx = {
411
+ method: event.method,
412
+ params: event.params ?? {},
413
+ sessionId: event.sessionId ?? "browser",
414
+ direction: "browser\u2192client"
415
+ };
416
+ const decision = this.engine.evaluate(ctx);
417
+ if (decision?.action === "block") {
418
+ return;
419
+ }
420
+ }
421
+ clientWs.send(raw.toString());
422
+ }
423
+ // ── Utilities ────────────────────────────────────────────────
424
+ parseMessage(raw) {
425
+ try {
426
+ return JSON.parse(raw.toString());
427
+ } catch {
428
+ return null;
429
+ }
430
+ }
431
+ recordDecision(decision) {
432
+ if (!this.stats.byRule[decision.ruleId]) {
433
+ this.stats.byRule[decision.ruleId] = { matched: 0, blocked: 0, transformed: 0 };
434
+ }
435
+ this.stats.byRule[decision.ruleId].matched++;
436
+ if (decision.action === "block") {
437
+ this.stats.byRule[decision.ruleId].blocked++;
438
+ } else if (decision.action === "transform") {
439
+ this.stats.byRule[decision.ruleId].transformed++;
440
+ }
441
+ }
442
+ };
443
+
444
+ // src/utils/cdp.ts
445
+ async function fetchNoProxy(url) {
446
+ const savedProxy = {
447
+ http_proxy: process.env.http_proxy,
448
+ https_proxy: process.env.https_proxy,
449
+ HTTP_PROXY: process.env.HTTP_PROXY,
450
+ HTTPS_PROXY: process.env.HTTPS_PROXY,
451
+ all_proxy: process.env.all_proxy,
452
+ ALL_PROXY: process.env.ALL_PROXY
453
+ };
454
+ for (const key of Object.keys(savedProxy)) delete process.env[key];
455
+ try {
456
+ return await fetch(url);
457
+ } finally {
458
+ for (const [key, val] of Object.entries(savedProxy)) {
459
+ if (val !== void 0) process.env[key] = val;
460
+ }
461
+ }
462
+ }
463
+ async function resolveCDPEndpoint(raw) {
464
+ if (raw === "auto") {
465
+ const ports = [9222, 9221, 9223, 9224];
466
+ for (const port of ports) {
467
+ try {
468
+ const httpResp = await fetchNoProxy(`http://localhost:${port}/json/version`);
469
+ if (httpResp.ok) {
470
+ const data = await httpResp.json();
471
+ if (data.webSocketDebuggerUrl) {
472
+ return data.webSocketDebuggerUrl;
473
+ }
474
+ }
475
+ } catch {
476
+ }
477
+ }
478
+ throw new Error(
479
+ `Could not auto-discover CDP endpoint. Tried ports: ${ports.join(", ")}.
480
+ \u53EF\u80FD\u539F\u56E0\uFF1A\u6CA1\u6709 Chrome \u4EE5 --remote-debugging-port \u542F\u52A8\u3002
481
+ \u89E3\u51B3\u65B9\u6CD5\uFF1A
482
+ 1. \u542F\u52A8 Chrome: google-chrome --remote-debugging-port=9222
483
+ 2. \u6216\u7528 cdp-tunnel: npx cdp-tunnel setup
484
+ 3. \u6216\u6307\u5B9A\u7AEF\u53E3: --cdp <port>`
485
+ );
486
+ }
487
+ if (/^\d+$/.test(raw)) {
488
+ const port = raw;
489
+ const httpResp = await fetchNoProxy(`http://localhost:${port}/json/version`);
490
+ const data = await httpResp.json();
491
+ if (!data.webSocketDebuggerUrl) {
492
+ throw new Error(`Could not discover CDP endpoint from localhost:${port}`);
493
+ }
494
+ return data.webSocketDebuggerUrl;
495
+ }
496
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
497
+ try {
498
+ const httpResp = await fetchNoProxy(`${raw}/json/version`);
499
+ const data = await httpResp.json();
500
+ if (!data.webSocketDebuggerUrl) {
501
+ throw new Error(`Could not discover CDP endpoint from ${raw}`);
502
+ }
503
+ return data.webSocketDebuggerUrl;
504
+ } catch (error) {
505
+ console.warn(`Failed to fetch WebSocket URL from ${raw}, using endpoint directly: ${error instanceof Error ? error.message : String(error)}`);
506
+ return raw;
507
+ }
508
+ }
509
+ return raw;
510
+ }
511
+
512
+ // src/browser.ts
513
+ import { SessionStore } from "@dyyz1993/xcli-core";
514
+ function logSessionEvent(event, details) {
515
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
516
+ const pid = process.pid;
517
+ console.error(`[SESSION] ${ts} [PID:${pid}] ${event} | ${details}`);
518
+ }
519
+ var SESSION_DIR = join(homedir(), ".xbrowser", "sessions");
520
+ function sessionFile(name) {
521
+ return join(SESSION_DIR, `${name}.json`);
522
+ }
523
+ function ensureSessionDir() {
524
+ mkdirSync(SESSION_DIR, { recursive: true });
525
+ }
526
+ var sessions = new SessionStore();
527
+ var _sharedBrowser = null;
528
+ var _sharedCdpProxy = null;
529
+ var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
530
+ var idleTimer = null;
531
+ function resetIdleTimer() {
532
+ if (idleTimer) clearTimeout(idleTimer);
533
+ idleTimer = setTimeout(async () => {
534
+ const now = Date.now();
535
+ let allIdle = true;
536
+ const idleSessions = [];
537
+ for (const s of sessions) {
538
+ if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
539
+ allIdle = false;
540
+ } else {
541
+ idleSessions.push(`${s.name}(${(now - s.lastActivityAt) / 1e3}s idle)`);
542
+ }
543
+ }
544
+ if (allIdle && (sessions.size > 0 || _sharedBrowser)) {
545
+ logSessionEvent("idle_timeout", `Sessions idle for >${IDLE_TIMEOUT_MS / 6e4}min. Sessions: ${idleSessions.join(", ") || "all"}. Calling destroyBrowser()`);
546
+ await destroyBrowser().catch(() => {
547
+ });
548
+ }
549
+ }, IDLE_TIMEOUT_MS);
550
+ if (idleTimer && typeof idleTimer.unref === "function") {
551
+ idleTimer.unref();
552
+ }
553
+ }
554
+ function touchSession(id) {
555
+ const s = sessions.get(id);
556
+ if (s) s.lastActivityAt = Date.now();
557
+ resetIdleTimer();
558
+ }
559
+ process.on("exit", () => {
560
+ for (const session of sessions.list()) {
561
+ if (session.isCDP) {
562
+ logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
563
+ } else {
564
+ logSessionEvent("process_exit", `Session "${session.name}": Closing self-launched browser.`);
565
+ try {
566
+ session.browser?.close();
567
+ } catch {
568
+ }
569
+ }
570
+ }
571
+ if (_sharedBrowser) {
572
+ logSessionEvent("process_exit", "Closing shared browser (self-launched only, external CDP is safe).");
573
+ try {
574
+ _sharedBrowser.close();
575
+ } catch {
576
+ }
577
+ _sharedBrowser = null;
578
+ }
579
+ if (_sharedCdpProxy) {
580
+ try {
581
+ _sharedCdpProxy.stop();
582
+ } catch {
583
+ }
584
+ _sharedCdpProxy = null;
585
+ }
586
+ sessions.clear();
587
+ });
588
+ async function getCDPTargets(cdpEndpoint) {
589
+ try {
590
+ const ep = String(cdpEndpoint);
591
+ let host = "localhost";
592
+ let port = "9222";
593
+ if (ep.startsWith("http://") || ep.startsWith("https://")) {
594
+ const u = new URL(ep);
595
+ host = u.hostname;
596
+ port = u.port || "9222";
597
+ } else if (/^\d+$/.test(ep)) {
598
+ port = ep;
599
+ }
600
+ const url = `http://${host}:${port}/json/list`;
601
+ const resp = await fetch(url);
602
+ return await resp.json();
603
+ } catch {
604
+ return [];
605
+ }
606
+ }
607
+ async function findTargetPage(cdpEndpoint, target) {
608
+ const targets = await getCDPTargets(cdpEndpoint);
609
+ const pages = targets.filter((t) => t.url && !t.url.startsWith("about:blank") && !t.url.startsWith("chrome://") && !t.url.startsWith("chrome-untrusted://") && !t.url.startsWith("chrome-error://"));
610
+ const byId = pages.find((t) => t.id === target);
611
+ if (byId) return { pageId: byId.id, wsUrl: byId.webSocketDebuggerUrl, title: byId.title, url: byId.url };
612
+ const lowerTarget = target.toLowerCase();
613
+ const byTitle = pages.find((t) => t.title && t.title.toLowerCase().includes(lowerTarget));
614
+ if (byTitle) return { pageId: byTitle.id, wsUrl: byTitle.webSocketDebuggerUrl, title: byTitle.title, url: byTitle.url };
615
+ const byUrl = pages.find((t) => t.url.toLowerCase().includes(lowerTarget));
616
+ if (byUrl) return { pageId: byUrl.id, wsUrl: byUrl.webSocketDebuggerUrl, title: byUrl.title, url: byUrl.url };
617
+ return null;
618
+ }
619
+ function resolveLaunchOpts(ctx) {
620
+ if (ctx.cdpEndpoint) {
621
+ return { cdpEndpoint: ctx.cdpEndpoint };
622
+ }
623
+ return { headless: true };
624
+ }
625
+ var CHROMIUM_CANDIDATES = [
626
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
627
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
628
+ "/usr/bin/chromium-browser",
629
+ "/usr/bin/chromium",
630
+ "/usr/bin/google-chrome"
631
+ ];
632
+ function discoverChromiumPath() {
633
+ for (const p of CHROMIUM_CANDIDATES) {
634
+ if (existsSync(p)) return p;
635
+ }
636
+ return void 0;
637
+ }
638
+ async function createBrowser(options) {
639
+ if (options?.cdpEndpoint) {
640
+ const realEndpoint = await resolveCDPEndpoint(options.cdpEndpoint);
641
+ if (options.intercept) {
642
+ const config = typeof options.intercept === "object" ? { ...options.intercept, cdpEndpoint: realEndpoint } : { cdpEndpoint: realEndpoint };
643
+ _sharedCdpProxy = new CDPInterceptorProxy(config);
644
+ const proxyPort = await _sharedCdpProxy.start();
645
+ console.error(`[CDP Interceptor] Proxy running on ws://localhost:${proxyPort}, forwarding to ${realEndpoint}`);
646
+ const { browser: browser3 } = await launch({ cdpEndpoint: `ws://localhost:${proxyPort}` });
647
+ return browser3;
648
+ }
649
+ const { browser: browser2 } = await launch({ cdpEndpoint: realEndpoint });
650
+ await browser2.discoverContexts().catch((err) => {
651
+ console.error(`[browser] discoverContexts failed: ${errMsg(err)}`);
652
+ });
653
+ return browser2;
654
+ }
655
+ const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
656
+ const { browser } = await launch({ executablePath, headless: options?.headless ?? true });
657
+ return browser;
658
+ }
659
+ async function getBrowser(options) {
660
+ if (_sharedBrowser) return _sharedBrowser;
661
+ _sharedBrowser = await createBrowser(options);
662
+ if (options?.cdpEndpoint && options.intercept) {
663
+ }
664
+ return _sharedBrowser;
665
+ }
666
+ function findSession(name) {
667
+ return sessions.find(name);
668
+ }
669
+ function getSessionById(id) {
670
+ return sessions.get(id);
671
+ }
672
+ function setActivePage(session, page) {
673
+ session.page = page;
674
+ session.lastActivityAt = Date.now();
675
+ }
676
+ function saveSessionDiskMeta(name, data) {
677
+ ensureSessionDir();
678
+ const file = sessionFile(name);
679
+ let existing = {};
680
+ try {
681
+ existing = JSON.parse(readFileSync(file, "utf8"));
682
+ } catch {
683
+ }
684
+ Object.assign(existing, data, { name });
685
+ writeFileSync(file, JSON.stringify(existing, null, 2));
686
+ }
687
+ function readSessionDiskMeta(name) {
688
+ const file = sessionFile(name);
689
+ try {
690
+ return JSON.parse(readFileSync(file, "utf8"));
691
+ } catch {
692
+ return null;
693
+ }
694
+ }
695
+ function deleteSessionDiskMeta(name) {
696
+ const file = sessionFile(name);
697
+ try {
698
+ unlinkSync(file);
699
+ } catch {
700
+ }
701
+ }
702
+ async function isSessionPageAlive(session) {
703
+ const page = session.page;
704
+ if (!page || typeof page.evaluate !== "function") return false;
705
+ for (let attempt = 0; attempt < 3; attempt++) {
706
+ try {
707
+ await Promise.race([
708
+ page.evaluate("1"),
709
+ new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
710
+ ]);
711
+ return true;
712
+ } catch {
713
+ await new Promise((r) => setTimeout(r, 400));
714
+ }
715
+ }
716
+ return false;
717
+ }
718
+ async function findOrRestoreSession(name, cdpEndpoint) {
719
+ const inMem = findSession(name);
720
+ if (inMem) {
721
+ if (await isSessionPageAlive(inMem)) return inMem;
722
+ logSessionEvent("stale_session", `name="${name}" \u2014 page \u63A2\u9488\u5931\u8D25\uFF08\u6D4F\u89C8\u5668\u53EF\u80FD\u5DF2\u91CD\u542F\uFF09\uFF0C\u5C31\u5730\u91CD\u5EFA`);
723
+ try {
724
+ await closeSessionByName(name);
725
+ } catch {
726
+ }
727
+ }
728
+ const meta = readSessionDiskMeta(name);
729
+ if (!meta) return void 0;
730
+ const ep = cdpEndpoint || meta.cdpEndpoint;
731
+ if (!ep) return void 0;
732
+ try {
733
+ const b = await createBrowser({ cdpEndpoint: ep });
734
+ await new Promise((r) => setTimeout(r, 500));
735
+ let contexts = b.contexts();
736
+ if (contexts.length === 0) {
737
+ await new Promise((r) => setTimeout(r, 500));
738
+ contexts = b.contexts();
739
+ }
740
+ const context = contexts[0] || await b.newContext();
741
+ const savedUrl = meta.conversationUrl || meta.url;
742
+ const targetHostname = savedUrl ? (() => {
743
+ try {
744
+ return new URL(savedUrl).hostname;
745
+ } catch {
746
+ return "";
747
+ }
748
+ })() : "";
749
+ let page = null;
750
+ let fallbackPage = null;
751
+ for (const ctx of contexts) {
752
+ const pages = ctx.pages();
753
+ for (const p of pages) {
754
+ const pUrl = p.url();
755
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && !pUrl.startsWith("chrome-untrusted://") && !pUrl.startsWith("chrome-error://")) {
756
+ if (targetHostname && pUrl.includes(targetHostname)) {
757
+ page = p;
758
+ break;
759
+ }
760
+ if (!fallbackPage) {
761
+ fallbackPage = p;
762
+ }
763
+ }
764
+ }
765
+ if (page) break;
766
+ }
767
+ page = page || fallbackPage;
768
+ if (!page) {
769
+ const targets = await getCDPTargets(ep);
770
+ const matchTarget = targets.find(
771
+ (t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && !t.url.startsWith("chrome-untrusted://") && !t.url.startsWith("chrome-error://") && (targetHostname ? t.url.includes(targetHostname) : true)
772
+ );
773
+ if (matchTarget && matchTarget.url) {
774
+ page = await context.newPage();
775
+ await page.goto(matchTarget.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
776
+ });
777
+ }
778
+ }
779
+ if (!page) {
780
+ const pages = context.pages();
781
+ page = pages.length > 0 ? pages[0] : await context.newPage();
782
+ }
783
+ try {
784
+ await Promise.race([
785
+ page.evaluate(() => true),
786
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
787
+ ]);
788
+ } catch {
789
+ console.log(`[Session] "${name}" restored page unresponsive, creating fresh session`);
790
+ deleteSessionDiskMeta(name);
791
+ return void 0;
792
+ }
793
+ const targetUrl = meta.conversationUrl || meta.url;
794
+ if (targetUrl && page.url() !== targetUrl) {
795
+ try {
796
+ if (!page.url().includes(new URL(targetUrl).hostname)) {
797
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
798
+ });
799
+ }
800
+ } catch {
801
+ }
802
+ }
803
+ const session = {
804
+ id: meta.id || randomUUID(),
805
+ name,
806
+ context,
807
+ page,
808
+ browser: b,
809
+ createdAt: meta.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
810
+ lastActivityAt: Date.now(),
811
+ isCDP: true,
812
+ cdpEndpoint: ep
813
+ };
814
+ for (const existingSession of sessions.list()) {
815
+ if (existingSession.name === name) {
816
+ logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingSession.id}" during restore`);
817
+ sessions.removeById(existingSession.id);
818
+ }
819
+ }
820
+ sessions.set(session);
821
+ resetIdleTimer();
822
+ await installNetworkCapture(page, name);
823
+ return session;
824
+ } catch (e) {
825
+ console.error(`[Session Restore] Failed for "${name}":`, errMsg(e));
826
+ deleteSessionDiskMeta(name);
827
+ return void 0;
828
+ }
829
+ }
830
+ async function createEphemeralContext(options) {
831
+ if (options?.cdpEndpoint) {
832
+ const endpoint = await resolveCDPEndpoint(options.cdpEndpoint);
833
+ const { browser: b2 } = await launch({ cdpEndpoint: endpoint });
834
+ const contexts = b2.contexts();
835
+ const ctx = contexts[0] || await b2.newContext();
836
+ const allPages = ctx.pages();
837
+ const existingPages = allPages.filter((p) => {
838
+ const url = p.url();
839
+ return url !== "about:blank" && !url.startsWith("chrome://") && !url.startsWith("chrome-untrusted://") && !url.startsWith("chrome-error://");
840
+ });
841
+ const page2 = existingPages.length > 0 ? existingPages[0] : allPages.length > 0 ? allPages[0] : await ctx.newPage();
842
+ resetIdleTimer();
843
+ ephemeralConnections.set(page2, b2);
844
+ return { context: ctx, page: page2 };
845
+ }
846
+ const b = await getBrowser(options);
847
+ const context = await b.newContext();
848
+ const page = await context.newPage();
849
+ resetIdleTimer();
850
+ return { context, page };
851
+ }
852
+ var ephemeralConnections = /* @__PURE__ */ new WeakMap();
853
+ async function closeEphemeralContext(context) {
854
+ try {
855
+ const pages = context.pages();
856
+ for (const p of pages) {
857
+ const conn = ephemeralConnections.get(p);
858
+ if (conn) {
859
+ ephemeralConnections.delete(p);
860
+ await conn.close();
861
+ break;
862
+ }
863
+ }
864
+ await context.close();
865
+ } catch {
866
+ }
867
+ if (sessions.size === 0 && idleTimer) {
868
+ clearTimeout(idleTimer);
869
+ idleTimer = null;
870
+ }
871
+ }
872
+ function getAllSessions() {
873
+ return sessions.list();
874
+ }
875
+ async function installNetworkCapture(page, sessionName) {
876
+ if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
877
+ const { networkStore } = await import("./network-store-XGZ25FFC.js");
878
+ const requestData = /* @__PURE__ */ new Map();
879
+ const responseMeta = /* @__PURE__ */ new Map();
880
+ const xbPage = page;
881
+ xbPage.on("request", (params) => {
882
+ try {
883
+ const p = params;
884
+ requestData.set(p.requestId, {
885
+ method: p.request.method,
886
+ headers: p.request.headers,
887
+ postData: p.request.postData ?? null,
888
+ resourceType: p.type
889
+ });
890
+ } catch {
891
+ }
892
+ });
893
+ xbPage.on("response", (params) => {
894
+ try {
895
+ const p = params;
896
+ responseMeta.set(p.requestId, {
897
+ status: p.response.status,
898
+ url: p.response.url,
899
+ headers: p.response.headers,
900
+ mimeType: p.response.mimeType,
901
+ type: p.type
902
+ });
903
+ } catch {
904
+ }
905
+ });
906
+ xbPage.on("requestfinished", async (params) => {
907
+ try {
908
+ const p = params;
909
+ const meta = responseMeta.get(p.requestId);
910
+ if (!meta) return;
911
+ const req = requestData.get(p.requestId);
912
+ const method = req?.method ?? "GET";
913
+ const contentType = meta.headers["content-type"] || meta.headers["Content-Type"] || "";
914
+ const resourceType = req?.resourceType ?? meta.type;
915
+ const requestHeaders = req?.headers ?? {};
916
+ let requestBody = void 0;
917
+ const isPostLike = ["POST", "PATCH", "PUT"].includes(method);
918
+ if (isPostLike && requestHeaders["content-type"]?.includes("application/json")) {
919
+ const postData = req?.postData;
920
+ if (postData) {
921
+ try {
922
+ requestBody = JSON.parse(postData);
923
+ } catch {
924
+ requestBody = postData;
925
+ }
926
+ }
927
+ }
928
+ let responseBody = void 0;
929
+ let size = 0;
930
+ const isJsonish = contentType.includes("json") || contentType.includes("javascript") || contentType.includes("text/");
931
+ if (isJsonish) {
932
+ try {
933
+ const bodyResult = await xbPage._cdpSend(
934
+ "Network.getResponseBody",
935
+ { requestId: p.requestId }
936
+ );
937
+ const text = bodyResult.body ?? "";
938
+ size = text.length;
939
+ if (size <= 10240) {
940
+ try {
941
+ responseBody = JSON.parse(text);
942
+ } catch {
943
+ responseBody = text.slice(0, 200);
944
+ }
945
+ }
946
+ } catch {
947
+ }
948
+ } else {
949
+ try {
950
+ const bodyResult = await xbPage._cdpSend(
951
+ "Network.getResponseBody",
952
+ { requestId: p.requestId }
953
+ );
954
+ size = bodyResult.body?.length ?? 0;
955
+ } catch {
956
+ size = 0;
957
+ }
958
+ }
959
+ networkStore.add(sessionName, {
960
+ timestamp: Date.now(),
961
+ method,
962
+ url: meta.url,
963
+ path: new URL(meta.url).pathname,
964
+ status: meta.status,
965
+ contentType,
966
+ size,
967
+ headers: meta.headers,
968
+ body: responseBody,
969
+ requestHeaders,
970
+ requestBody,
971
+ resourceType
972
+ });
973
+ requestData.delete(p.requestId);
974
+ responseMeta.delete(p.requestId);
975
+ } catch {
976
+ }
977
+ });
978
+ }
979
+ async function createSession(name, url, options) {
980
+ const existing = findSession(name);
981
+ if (existing) {
982
+ logSessionEvent("replace_session", `name="${name}" id="${existing.id}" \u2014 closing existing session before creating new one`);
983
+ await closeSessionByName(name);
984
+ }
985
+ const b = await createBrowser(options);
986
+ const isCDP = !!options?.cdpEndpoint;
987
+ let context;
988
+ let page;
989
+ if (isCDP) {
990
+ const targetHostname = url ? (() => {
991
+ try {
992
+ return new URL(url).hostname;
993
+ } catch {
994
+ return "";
995
+ }
996
+ })() : "";
997
+ const isRealPageUrl = (u) => !!u && u !== "about:blank" && !u.startsWith("chrome://") && !u.startsWith("chrome-untrusted://") && !u.startsWith("chrome-error://");
998
+ const resolvePageUrl = async (p) => {
999
+ const u = p.url();
1000
+ if (u && u !== "about:blank") return u;
1001
+ try {
1002
+ return await p.evaluate("location.href");
1003
+ } catch {
1004
+ return u ?? "";
1005
+ }
1006
+ };
1007
+ const pollStart = Date.now();
1008
+ for (; ; ) {
1009
+ const ctxs = b.contexts();
1010
+ let hostHit = false;
1011
+ let anyHit = false;
1012
+ for (const ctx of ctxs) {
1013
+ for (const p of ctx.pages()) {
1014
+ const u = await resolvePageUrl(p);
1015
+ if (!isRealPageUrl(u)) continue;
1016
+ anyHit = true;
1017
+ if (targetHostname && u.includes(targetHostname)) {
1018
+ hostHit = true;
1019
+ break;
1020
+ }
1021
+ }
1022
+ if (hostHit) break;
1023
+ }
1024
+ if (process.env.XB_DEBUG_SESSION_POLL) {
1025
+ console.error(`[poll] t=${Date.now() - pollStart}ms ctxs=${ctxs.length} hostHit=${hostHit} anyHit=${anyHit}`);
1026
+ }
1027
+ if (hostHit) break;
1028
+ if (anyHit && Date.now() - pollStart >= 2e3) break;
1029
+ if (Date.now() - pollStart >= 5e3) break;
1030
+ await new Promise((r) => setTimeout(r, 200));
1031
+ }
1032
+ let contexts = b.contexts();
1033
+ if (contexts.length === 0) {
1034
+ await new Promise((r) => setTimeout(r, 500));
1035
+ contexts = b.contexts();
1036
+ }
1037
+ context = contexts[0] || await b.newContext();
1038
+ let targetPage = null;
1039
+ if (targetHostname) {
1040
+ for (const ctx of contexts) {
1041
+ const pages = ctx.pages();
1042
+ for (const p of pages) {
1043
+ const pUrl = await resolvePageUrl(p);
1044
+ if (isRealPageUrl(pUrl) && pUrl.includes(targetHostname)) {
1045
+ targetPage = p;
1046
+ break;
1047
+ }
1048
+ }
1049
+ if (targetPage) break;
1050
+ }
1051
+ }
1052
+ if (!targetPage) {
1053
+ for (const ctx of contexts) {
1054
+ const pages = ctx.pages();
1055
+ for (const p of pages) {
1056
+ const pUrl = await resolvePageUrl(p);
1057
+ if (isRealPageUrl(pUrl)) {
1058
+ targetPage = p;
1059
+ break;
1060
+ }
1061
+ }
1062
+ if (targetPage) break;
1063
+ }
1064
+ }
1065
+ if (!targetPage && options?.cdpEndpoint) {
1066
+ const targets = await getCDPTargets(options.cdpEndpoint);
1067
+ const matchTarget = targets.find(
1068
+ (t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && !t.url.startsWith("chrome-untrusted://") && !t.url.startsWith("chrome-error://") && (url ? t.url.includes(new URL(url).hostname) : true)
1069
+ );
1070
+ if (matchTarget && matchTarget.url) {
1071
+ targetPage = await context.newPage();
1072
+ await targetPage.goto(matchTarget.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
1073
+ });
1074
+ }
1075
+ }
1076
+ if (!targetPage) {
1077
+ const pages = context.pages();
1078
+ if (pages.length > 0) {
1079
+ targetPage = pages[0];
1080
+ } else {
1081
+ targetPage = await context.newPage();
1082
+ }
1083
+ }
1084
+ page = targetPage;
1085
+ } else {
1086
+ context = await b.newContext({ viewport: { width: 1920, height: 1080 } });
1087
+ page = await context.newPage();
1088
+ }
1089
+ if (url && page.url() !== url) {
1090
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
1091
+ });
1092
+ }
1093
+ const session = {
1094
+ id: randomUUID(),
1095
+ name,
1096
+ context,
1097
+ page,
1098
+ browser: b,
1099
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1100
+ lastActivityAt: Date.now(),
1101
+ isCDP,
1102
+ cdpEndpoint: options?.cdpEndpoint
1103
+ };
1104
+ sessions.set(session);
1105
+ logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
1106
+ resetIdleTimer();
1107
+ await installNetworkCapture(page, name);
1108
+ return session;
1109
+ }
1110
+ async function closeSessionByName(name) {
1111
+ for (const session of sessions) {
1112
+ if (session.name === name || session.id === name) {
1113
+ logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
1114
+ if (session.isCDP) {
1115
+ if (session.browser) {
1116
+ await session.browser.close().catch(() => {
1117
+ });
1118
+ }
1119
+ } else {
1120
+ await session.context.close();
1121
+ if (session.browser) {
1122
+ await session.browser.close().catch(() => {
1123
+ });
1124
+ }
1125
+ }
1126
+ sessions.removeById(session.id);
1127
+ const file2 = sessionFile(session.name);
1128
+ try {
1129
+ unlinkSync(file2);
1130
+ } catch {
1131
+ }
1132
+ try {
1133
+ const { networkStore, commandLogStore } = await import("./network-store-XGZ25FFC.js");
1134
+ networkStore.clear(session.name);
1135
+ commandLogStore.clear(session.name);
1136
+ } catch {
1137
+ }
1138
+ try {
1139
+ const { SessionRecorder } = await import("./session-recorder-3BEVWHOK.js");
1140
+ SessionRecorder.cleanup(session.name);
1141
+ } catch {
1142
+ }
1143
+ return true;
1144
+ }
1145
+ }
1146
+ const file = sessionFile(name);
1147
+ try {
1148
+ unlinkSync(file);
1149
+ } catch {
1150
+ }
1151
+ return false;
1152
+ }
1153
+ async function closeAllSessions() {
1154
+ const names = sessions.list().map((s) => `${s.name}(${s.page.url()})`).join(", ");
1155
+ if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
1156
+ for (const session of sessions.list()) {
1157
+ try {
1158
+ if (session.isCDP) {
1159
+ if (session.browser) {
1160
+ await session.browser.close().catch(() => {
1161
+ });
1162
+ }
1163
+ } else {
1164
+ await session.context.close();
1165
+ if (session.browser) {
1166
+ await session.browser.close().catch(() => {
1167
+ });
1168
+ }
1169
+ }
1170
+ sessions.removeById(session.id);
1171
+ } catch {
1172
+ sessions.removeById(session.id);
1173
+ }
1174
+ }
1175
+ }
1176
+ async function destroyBrowser() {
1177
+ logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all sessions.`);
1178
+ if (idleTimer) {
1179
+ clearTimeout(idleTimer);
1180
+ idleTimer = null;
1181
+ }
1182
+ await closeAllSessions();
1183
+ if (_sharedBrowser) {
1184
+ await _sharedBrowser.close().catch(() => {
1185
+ });
1186
+ _sharedBrowser = null;
1187
+ }
1188
+ if (_sharedCdpProxy) {
1189
+ await _sharedCdpProxy.stop().catch(() => {
1190
+ });
1191
+ _sharedCdpProxy = null;
1192
+ }
1193
+ }
1194
+ function resetForTesting() {
1195
+ sessions.clear();
1196
+ _sharedBrowser = null;
1197
+ _sharedCdpProxy = null;
1198
+ try {
1199
+ for (const f of readdirSync(SESSION_DIR)) {
1200
+ unlinkSync(join(SESSION_DIR, f));
1201
+ }
1202
+ } catch {
1203
+ }
1204
+ }
1205
+ async function ensureProcessCanExit() {
1206
+ if (idleTimer) {
1207
+ clearTimeout(idleTimer);
1208
+ idleTimer = null;
1209
+ }
1210
+ for (const session of sessions.list()) {
1211
+ if (session.browser) {
1212
+ if (session.isCDP) {
1213
+ } else {
1214
+ await session.browser.close().catch(() => {
1215
+ });
1216
+ }
1217
+ }
1218
+ }
1219
+ sessions.clear();
1220
+ if (_sharedBrowser) {
1221
+ await _sharedBrowser.close().catch(() => {
1222
+ });
1223
+ _sharedBrowser = null;
1224
+ }
1225
+ if (_sharedCdpProxy) {
1226
+ await _sharedCdpProxy.stop().catch(() => {
1227
+ });
1228
+ _sharedCdpProxy = null;
1229
+ }
1230
+ }
1231
+
1232
+ export {
1233
+ resolveCDPEndpoint,
1234
+ touchSession,
1235
+ findTargetPage,
1236
+ resolveLaunchOpts,
1237
+ createBrowser,
1238
+ getBrowser,
1239
+ findSession,
1240
+ getSessionById,
1241
+ setActivePage,
1242
+ saveSessionDiskMeta,
1243
+ readSessionDiskMeta,
1244
+ deleteSessionDiskMeta,
1245
+ findOrRestoreSession,
1246
+ createEphemeralContext,
1247
+ closeEphemeralContext,
1248
+ getAllSessions,
1249
+ createSession,
1250
+ closeSessionByName,
1251
+ closeAllSessions,
1252
+ destroyBrowser,
1253
+ resetForTesting,
1254
+ ensureProcessCanExit
1255
+ };