browserclaw 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -823,6 +823,19 @@ var require_ipaddr = __commonJS({
823
823
  })(exports$1);
824
824
  }
825
825
  });
826
+ function killProcessTree(proc, signal) {
827
+ if (process.platform !== "win32" && proc.pid !== void 0) {
828
+ try {
829
+ process.kill(-proc.pid, signal);
830
+ return;
831
+ } catch {
832
+ }
833
+ }
834
+ try {
835
+ proc.kill(signal);
836
+ } catch {
837
+ }
838
+ }
826
839
  var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
827
840
  "com.google.Chrome",
828
841
  "com.google.Chrome.beta",
@@ -1178,17 +1191,30 @@ function resolveBrowserExecutable(opts) {
1178
1191
  if (platform === "win32") return detectDefaultChromiumWindows() ?? findChromeWindows();
1179
1192
  return null;
1180
1193
  }
1181
- async function ensurePortAvailable(port) {
1182
- await new Promise((resolve2, reject) => {
1183
- const tester = net.createServer().once("error", (err) => {
1184
- if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
1185
- else reject(err);
1186
- }).once("listening", () => {
1187
- tester.close(() => {
1188
- resolve2();
1194
+ async function ensurePortAvailable(port, retries = 2) {
1195
+ for (let attempt = 0; attempt <= retries; attempt++) {
1196
+ try {
1197
+ await new Promise((resolve2, reject) => {
1198
+ const tester = net.createServer().once("error", (err) => {
1199
+ tester.close(() => {
1200
+ if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
1201
+ else reject(err);
1202
+ });
1203
+ }).once("listening", () => {
1204
+ tester.close(() => {
1205
+ resolve2();
1206
+ });
1207
+ }).listen(port);
1189
1208
  });
1190
- }).listen(port);
1191
- });
1209
+ return;
1210
+ } catch (err) {
1211
+ if (attempt < retries) {
1212
+ await new Promise((r) => setTimeout(r, 100));
1213
+ continue;
1214
+ }
1215
+ throw err;
1216
+ }
1217
+ }
1192
1218
  }
1193
1219
  function safeReadJson(filePath) {
1194
1220
  try {
@@ -1461,7 +1487,7 @@ async function launchChrome(opts = {}) {
1461
1487
  const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
1462
1488
  const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
1463
1489
  fs.mkdirSync(userDataDir, { recursive: true });
1464
- const spawnChrome = () => {
1490
+ const spawnChrome = (spawnOpts) => {
1465
1491
  const args = [
1466
1492
  `--remote-debugging-port=${String(cdpPort)}`,
1467
1493
  "--remote-debugging-address=127.0.0.1",
@@ -1492,33 +1518,29 @@ async function launchChrome(opts = {}) {
1492
1518
  args.push("about:blank");
1493
1519
  return spawn(exe.path, args, {
1494
1520
  stdio: "pipe",
1495
- env: { ...process.env, HOME: os.homedir() }
1521
+ env: { ...process.env, HOME: os.homedir() },
1522
+ ...spawnOpts
1496
1523
  });
1497
1524
  };
1498
1525
  const startedAt = Date.now();
1499
1526
  const localStatePath = path.join(userDataDir, "Local State");
1500
1527
  const preferencesPath = path.join(userDataDir, "Default", "Preferences");
1501
1528
  if (!fileExists(localStatePath) || !fileExists(preferencesPath)) {
1502
- const bootstrap = spawnChrome();
1529
+ const useDetached = process.platform !== "win32";
1530
+ const bootstrap = spawnChrome(useDetached ? { detached: true } : void 0);
1503
1531
  const deadline = Date.now() + 1e4;
1504
1532
  while (Date.now() < deadline) {
1505
1533
  if (fileExists(localStatePath) && fileExists(preferencesPath)) break;
1506
1534
  await new Promise((r) => setTimeout(r, 100));
1507
1535
  }
1508
- try {
1509
- bootstrap.kill("SIGTERM");
1510
- } catch {
1511
- }
1536
+ killProcessTree(bootstrap, "SIGTERM");
1512
1537
  const exitDeadline = Date.now() + 5e3;
1513
1538
  while (Date.now() < exitDeadline) {
1514
1539
  if (bootstrap.exitCode != null) break;
1515
1540
  await new Promise((r) => setTimeout(r, 50));
1516
1541
  }
1517
1542
  if (bootstrap.exitCode == null) {
1518
- try {
1519
- bootstrap.kill("SIGKILL");
1520
- } catch {
1521
- }
1543
+ killProcessTree(bootstrap, "SIGKILL");
1522
1544
  }
1523
1545
  }
1524
1546
  try {
@@ -1537,9 +1559,11 @@ async function launchChrome(opts = {}) {
1537
1559
  };
1538
1560
  proc.stderr.on("data", onStderr);
1539
1561
  const readyDeadline = Date.now() + 15e3;
1562
+ let pollDelay = 200;
1540
1563
  while (Date.now() < readyDeadline) {
1541
1564
  if (await isChromeCdpReady(cdpUrl, 500)) break;
1542
- await new Promise((r) => setTimeout(r, 200));
1565
+ await new Promise((r) => setTimeout(r, pollDelay));
1566
+ pollDelay = Math.min(pollDelay + 100, 1e3);
1543
1567
  }
1544
1568
  if (!await isChromeCdpReady(cdpUrl, 500)) {
1545
1569
  const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
@@ -1551,6 +1575,11 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1551
1575
  proc.kill("SIGKILL");
1552
1576
  } catch {
1553
1577
  }
1578
+ try {
1579
+ const lockFile = path.join(userDataDir, "SingletonLock");
1580
+ if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile);
1581
+ } catch {
1582
+ }
1554
1583
  throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
1555
1584
  }
1556
1585
  proc.stderr.off("data", onStderr);
@@ -1569,19 +1598,13 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1569
1598
  async function stopChrome(running, timeoutMs = 2500) {
1570
1599
  const proc = running.proc;
1571
1600
  if (proc.exitCode !== null) return;
1572
- try {
1573
- proc.kill("SIGTERM");
1574
- } catch {
1575
- }
1601
+ killProcessTree(proc, "SIGTERM");
1576
1602
  const start = Date.now();
1577
1603
  while (Date.now() - start < timeoutMs) {
1578
1604
  if (proc.exitCode !== null) return;
1579
1605
  await new Promise((r) => setTimeout(r, 100));
1580
1606
  }
1581
- try {
1582
- proc.kill("SIGKILL");
1583
- } catch {
1584
- }
1607
+ killProcessTree(proc, "SIGKILL");
1585
1608
  }
1586
1609
 
1587
1610
  // src/stealth.ts
@@ -1654,6 +1677,9 @@ var STEALTH_SCRIPT = `(function() {
1654
1677
  });
1655
1678
 
1656
1679
  // \u2500\u2500 4. window.chrome \u2500\u2500
1680
+ // Stub the chrome.runtime API surface that detection scripts probe for.
1681
+ // Only applied when the real chrome.runtime.connect is absent (headless/CDP mode).
1682
+ // The stubs are intentionally non-functional \u2014 they exist solely to pass presence checks.
1657
1683
  p(function() {
1658
1684
  if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;
1659
1685
 
@@ -1704,6 +1730,9 @@ var STEALTH_SCRIPT = `(function() {
1704
1730
  });
1705
1731
 
1706
1732
  // \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500
1733
+ // Hardcoded to Intel Iris \u2014 the most common discrete GPU on macOS. These strings
1734
+ // are fingerprinting targets; a more sophisticated approach would randomize per-session,
1735
+ // but static values are sufficient to avoid the default "Google SwiftShader" headless signal.
1707
1736
  p(function() {
1708
1737
  var h = {
1709
1738
  apply: function(target, self, args) {
@@ -1839,7 +1868,7 @@ function ensurePageState(page) {
1839
1868
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1840
1869
  location: msg.location()
1841
1870
  });
1842
- if (state.console.length > MAX_CONSOLE_MESSAGES) state.console.shift();
1871
+ if (state.console.length > MAX_CONSOLE_MESSAGES + 50) state.console.splice(0, 50);
1843
1872
  });
1844
1873
  page.on("pageerror", (err) => {
1845
1874
  state.errors.push({
@@ -1848,7 +1877,7 @@ function ensurePageState(page) {
1848
1877
  stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
1849
1878
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1850
1879
  });
1851
- if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
1880
+ if (state.errors.length > MAX_PAGE_ERRORS + 20) state.errors.splice(0, 20);
1852
1881
  });
1853
1882
  page.on("request", (req) => {
1854
1883
  state.nextRequestId += 1;
@@ -1861,7 +1890,7 @@ function ensurePageState(page) {
1861
1890
  url: req.url(),
1862
1891
  resourceType: req.resourceType()
1863
1892
  });
1864
- if (state.requests.length > MAX_NETWORK_REQUESTS) state.requests.shift();
1893
+ if (state.requests.length > MAX_NETWORK_REQUESTS + 50) state.requests.splice(0, 50);
1865
1894
  });
1866
1895
  page.on("response", (resp) => {
1867
1896
  const req = resp.request();
@@ -1935,31 +1964,40 @@ function setDialogHandlerOnPage(page, handler) {
1935
1964
  const state = ensurePageState(page);
1936
1965
  state.dialogHandler = handler;
1937
1966
  }
1938
- function applyStealthToPage(page) {
1939
- page.evaluate(STEALTH_SCRIPT).catch((e) => {
1967
+ async function applyStealthToPage(page) {
1968
+ try {
1969
+ await page.evaluate(STEALTH_SCRIPT);
1970
+ } catch (e) {
1940
1971
  if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1941
1972
  console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
1942
- });
1973
+ }
1943
1974
  }
1944
- function observeContext(context) {
1975
+ async function observeContext(context) {
1945
1976
  if (observedContexts.has(context)) return;
1946
1977
  observedContexts.add(context);
1947
1978
  ensureContextState(context);
1948
- context.addInitScript(STEALTH_SCRIPT).catch((e) => {
1979
+ try {
1980
+ await context.addInitScript(STEALTH_SCRIPT);
1981
+ } catch (e) {
1949
1982
  if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1950
1983
  console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
1951
- });
1984
+ }
1952
1985
  for (const page of context.pages()) {
1953
1986
  ensurePageState(page);
1954
- applyStealthToPage(page);
1987
+ await applyStealthToPage(page);
1955
1988
  }
1956
- context.on("page", (page) => {
1989
+ const onPage = (page) => {
1957
1990
  ensurePageState(page);
1958
- applyStealthToPage(page);
1991
+ applyStealthToPage(page).catch(() => {
1992
+ });
1993
+ };
1994
+ context.on("page", onPage);
1995
+ context.once("close", () => {
1996
+ context.off("page", onPage);
1959
1997
  });
1960
1998
  }
1961
- function observeBrowser(browser) {
1962
- for (const context of browser.contexts()) observeContext(context);
1999
+ async function observeBrowser(browser) {
2000
+ for (const context of browser.contexts()) await observeContext(context);
1963
2001
  }
1964
2002
  function toAIFriendlyError(error, selector) {
1965
2003
  const message = error instanceof Error ? error.message : String(error);
@@ -1994,6 +2032,7 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
1994
2032
  }
1995
2033
 
1996
2034
  // src/ref-resolver.ts
2035
+ var REFS_STALENESS_THRESHOLD_MS = 3e4;
1997
2036
  var roleRefsByTarget = /* @__PURE__ */ new Map();
1998
2037
  var MAX_ROLE_REFS_CACHE = 50;
1999
2038
  function normalizeCdpUrl(raw) {
@@ -2008,7 +2047,8 @@ function rememberRoleRefsForTarget(opts) {
2008
2047
  roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
2009
2048
  refs: opts.refs,
2010
2049
  ...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
2011
- ...opts.mode !== void 0 ? { mode: opts.mode } : {}
2050
+ ...opts.mode !== void 0 ? { mode: opts.mode } : {},
2051
+ storedAt: Date.now()
2012
2052
  });
2013
2053
  while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
2014
2054
  const first = roleRefsByTarget.keys().next();
@@ -2021,6 +2061,7 @@ function storeRoleRefsForTarget(opts) {
2021
2061
  state.roleRefs = opts.refs;
2022
2062
  state.roleRefsFrameSelector = opts.frameSelector;
2023
2063
  state.roleRefsMode = opts.mode;
2064
+ state.roleRefsStoredAt = Date.now();
2024
2065
  if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
2025
2066
  rememberRoleRefsForTarget({
2026
2067
  cdpUrl: opts.cdpUrl,
@@ -2030,17 +2071,6 @@ function storeRoleRefsForTarget(opts) {
2030
2071
  mode: opts.mode
2031
2072
  });
2032
2073
  }
2033
- function restoreRoleRefsForTarget(opts) {
2034
- const targetId = opts.targetId?.trim() ?? "";
2035
- if (targetId === "") return;
2036
- const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
2037
- if (!entry) return;
2038
- const state = ensurePageState(opts.page);
2039
- if (state.roleRefs) return;
2040
- state.roleRefs = entry.refs;
2041
- state.roleRefsFrameSelector = entry.frameSelector;
2042
- state.roleRefsMode = entry.mode;
2043
- }
2044
2074
  function clearRoleRefsForCdpUrl(cdpUrl) {
2045
2075
  const normalized = normalizeCdpUrl(cdpUrl);
2046
2076
  for (const key of roleRefsByTarget.keys()) {
@@ -2079,6 +2109,14 @@ function refLocator(page, ref) {
2079
2109
  if (normalized.trim() === "") throw new Error("ref is required");
2080
2110
  if (/^e\d+$/.test(normalized)) {
2081
2111
  const state = getPageState(page);
2112
+ if (state?.roleRefsStoredAt !== void 0) {
2113
+ const ageMs = Date.now() - state.roleRefsStoredAt;
2114
+ if (ageMs > REFS_STALENESS_THRESHOLD_MS) {
2115
+ console.warn(
2116
+ `[browserclaw] refs are ${String(Math.round(ageMs / 1e3))}s old \u2014 consider re-snapshotting for fresh refs`
2117
+ );
2118
+ }
2119
+ }
2082
2120
  if (state?.roleRefsMode === "aria") {
2083
2121
  return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2084
2122
  }
@@ -2127,14 +2165,16 @@ function appendCdpPath2(cdpUrl, cdpPath) {
2127
2165
  }
2128
2166
  async function withPlaywrightPageCdpSession(page, fn) {
2129
2167
  const CDP_SESSION_TIMEOUT_MS = 1e4;
2168
+ let timer;
2130
2169
  const session = await Promise.race([
2131
2170
  page.context().newCDPSession(page),
2132
2171
  new Promise((_, reject) => {
2133
- setTimeout(() => {
2172
+ timer = setTimeout(() => {
2134
2173
  reject(new Error("newCDPSession timed out after 10s"));
2135
2174
  }, CDP_SESSION_TIMEOUT_MS);
2136
2175
  })
2137
2176
  ]);
2177
+ clearTimeout(timer);
2138
2178
  try {
2139
2179
  return await fn(session);
2140
2180
  } finally {
@@ -2160,8 +2200,10 @@ function isLoopbackCdpUrl(url) {
2160
2200
  }
2161
2201
  }
2162
2202
  var envMutexPromise = Promise.resolve();
2203
+ var envMutexDepth = 0;
2163
2204
  async function withNoProxyForCdpUrl(url, fn) {
2164
2205
  if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return fn();
2206
+ if (envMutexDepth > 0) return fn();
2165
2207
  const prev = envMutexPromise;
2166
2208
  let release = () => {
2167
2209
  };
@@ -2182,9 +2224,11 @@ async function withNoProxyForCdpUrl(url, fn) {
2182
2224
  const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
2183
2225
  process.env.NO_PROXY = applied;
2184
2226
  process.env.no_proxy = applied;
2227
+ envMutexDepth += 1;
2185
2228
  try {
2186
2229
  return await fn();
2187
2230
  } finally {
2231
+ envMutexDepth -= 1;
2188
2232
  if (process.env.NO_PROXY === applied) {
2189
2233
  if (savedNoProxy !== void 0) process.env.NO_PROXY = savedNoProxy;
2190
2234
  else delete process.env.NO_PROXY;
@@ -2215,12 +2259,28 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
2215
2259
  }
2216
2260
  var cachedByCdpUrl = /* @__PURE__ */ new Map();
2217
2261
  var connectingByCdpUrl = /* @__PURE__ */ new Map();
2262
+ var connectionMutex = Promise.resolve();
2263
+ async function withConnectionLock(fn) {
2264
+ const prev = connectionMutex;
2265
+ let release = () => {
2266
+ };
2267
+ connectionMutex = new Promise((r) => {
2268
+ release = r;
2269
+ });
2270
+ await prev;
2271
+ try {
2272
+ return await fn();
2273
+ } finally {
2274
+ release();
2275
+ }
2276
+ }
2218
2277
  var BlockedBrowserTargetError = class extends Error {
2219
2278
  constructor() {
2220
2279
  super("Browser target is unavailable after SSRF policy blocked its navigation.");
2221
2280
  this.name = "BlockedBrowserTargetError";
2222
2281
  }
2223
2282
  };
2283
+ var MAX_BLOCKED_TARGETS = 200;
2224
2284
  var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
2225
2285
  var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
2226
2286
  function blockedTargetKey(cdpUrl, targetId) {
@@ -2235,6 +2295,10 @@ function markTargetBlocked(cdpUrl, targetId) {
2235
2295
  const normalized = targetId?.trim() ?? "";
2236
2296
  if (normalized === "") return;
2237
2297
  blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
2298
+ if (blockedTargetsByCdpUrl.size > MAX_BLOCKED_TARGETS) {
2299
+ const first = blockedTargetsByCdpUrl.values().next();
2300
+ if (first.done !== true) blockedTargetsByCdpUrl.delete(first.value);
2301
+ }
2238
2302
  }
2239
2303
  function clearBlockedTarget(cdpUrl, targetId) {
2240
2304
  const normalized = targetId?.trim() ?? "";
@@ -2248,6 +2312,16 @@ function hasBlockedTargetsForCdpUrl(cdpUrl) {
2248
2312
  }
2249
2313
  return false;
2250
2314
  }
2315
+ function clearBlockedTargetsForCdpUrl(cdpUrl) {
2316
+ if (cdpUrl === void 0) {
2317
+ blockedTargetsByCdpUrl.clear();
2318
+ return;
2319
+ }
2320
+ const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
2321
+ for (const key of blockedTargetsByCdpUrl) {
2322
+ if (key.startsWith(prefix)) blockedTargetsByCdpUrl.delete(key);
2323
+ }
2324
+ }
2251
2325
  function blockedPageRefsForCdpUrl(cdpUrl) {
2252
2326
  const normalized = normalizeCdpUrl(cdpUrl);
2253
2327
  const existing = blockedPageRefsByCdpUrl.get(normalized);
@@ -2262,6 +2336,13 @@ function isBlockedPageRef(cdpUrl, page) {
2262
2336
  function markPageRefBlocked(cdpUrl, page) {
2263
2337
  blockedPageRefsForCdpUrl(cdpUrl).add(page);
2264
2338
  }
2339
+ function clearBlockedPageRefsForCdpUrl(cdpUrl) {
2340
+ if (cdpUrl === void 0) {
2341
+ blockedPageRefsByCdpUrl.clear();
2342
+ return;
2343
+ }
2344
+ blockedPageRefsByCdpUrl.delete(normalizeCdpUrl(cdpUrl));
2345
+ }
2265
2346
  function clearBlockedPageRef(cdpUrl, page) {
2266
2347
  blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
2267
2348
  }
@@ -2275,64 +2356,98 @@ async function connectBrowser(cdpUrl, authToken) {
2275
2356
  if (existing_cached) return existing_cached;
2276
2357
  const existing = connectingByCdpUrl.get(normalized);
2277
2358
  if (existing) return await existing;
2278
- const connectWithRetry = async () => {
2279
- let lastErr;
2280
- for (let attempt = 0; attempt < 3; attempt++) {
2281
- try {
2282
- const timeout = 5e3 + attempt * 2e3;
2283
- const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
2284
- const headers = getHeadersWithAuth(endpoint);
2285
- if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
2286
- headers.Authorization = `Bearer ${authToken}`;
2287
- const browser = await withNoProxyForCdpUrl(
2288
- endpoint,
2289
- () => chromium.connectOverCDP(endpoint, { timeout, headers })
2290
- );
2291
- const onDisconnected = () => {
2292
- if (cachedByCdpUrl.get(normalized)?.browser === browser) {
2293
- cachedByCdpUrl.delete(normalized);
2294
- clearRoleRefsForCdpUrl(normalized);
2359
+ return withConnectionLock(async () => {
2360
+ const rechecked = cachedByCdpUrl.get(normalized);
2361
+ if (rechecked) return rechecked;
2362
+ const recheckPending = connectingByCdpUrl.get(normalized);
2363
+ if (recheckPending) return await recheckPending;
2364
+ const connectWithRetry = async () => {
2365
+ let lastErr;
2366
+ for (let attempt = 0; attempt < 3; attempt++) {
2367
+ try {
2368
+ const timeout = 5e3 + attempt * 2e3;
2369
+ const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
2370
+ const headers = getHeadersWithAuth(endpoint);
2371
+ if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
2372
+ headers.Authorization = `Bearer ${authToken}`;
2373
+ const browser = await withNoProxyForCdpUrl(
2374
+ endpoint,
2375
+ () => chromium.connectOverCDP(endpoint, { timeout, headers })
2376
+ );
2377
+ const onDisconnected = () => {
2378
+ if (cachedByCdpUrl.get(normalized)?.browser === browser) {
2379
+ cachedByCdpUrl.delete(normalized);
2380
+ clearRoleRefsForCdpUrl(normalized);
2381
+ }
2382
+ };
2383
+ const connected = { browser, cdpUrl: normalized, onDisconnected };
2384
+ cachedByCdpUrl.set(normalized, connected);
2385
+ await observeBrowser(browser);
2386
+ browser.on("disconnected", onDisconnected);
2387
+ return connected;
2388
+ } catch (err) {
2389
+ lastErr = err;
2390
+ if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) {
2391
+ await new Promise((r) => setTimeout(r, 1e3 + attempt * 1e3));
2392
+ continue;
2295
2393
  }
2296
- };
2297
- const connected = { browser, cdpUrl: normalized, onDisconnected };
2298
- cachedByCdpUrl.set(normalized, connected);
2299
- observeBrowser(browser);
2300
- browser.on("disconnected", onDisconnected);
2301
- return connected;
2302
- } catch (err) {
2303
- lastErr = err;
2304
- if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) break;
2305
- await new Promise((r) => setTimeout(r, 250 + attempt * 250));
2394
+ await new Promise((r) => setTimeout(r, 250 + attempt * 250));
2395
+ }
2306
2396
  }
2307
- }
2308
- throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
2309
- };
2310
- const promise = connectWithRetry().finally(() => {
2311
- connectingByCdpUrl.delete(normalized);
2397
+ throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
2398
+ };
2399
+ const promise = connectWithRetry().finally(() => {
2400
+ connectingByCdpUrl.delete(normalized);
2401
+ });
2402
+ connectingByCdpUrl.set(normalized, promise);
2403
+ return await promise;
2312
2404
  });
2313
- connectingByCdpUrl.set(normalized, promise);
2314
- return await promise;
2315
2405
  }
2316
2406
  async function disconnectBrowser() {
2317
- if (connectingByCdpUrl.size) {
2318
- for (const p of connectingByCdpUrl.values()) {
2319
- try {
2320
- await p;
2321
- } catch (err) {
2322
- console.warn(
2323
- `[browserclaw] disconnectBrowser: pending connect failed: ${err instanceof Error ? err.message : String(err)}`
2324
- );
2407
+ return withConnectionLock(async () => {
2408
+ if (connectingByCdpUrl.size) {
2409
+ for (const p of connectingByCdpUrl.values()) {
2410
+ try {
2411
+ await p;
2412
+ } catch (err) {
2413
+ console.warn(
2414
+ `[browserclaw] disconnectBrowser: pending connect failed: ${err instanceof Error ? err.message : String(err)}`
2415
+ );
2416
+ }
2325
2417
  }
2326
2418
  }
2327
- }
2328
- for (const cur of cachedByCdpUrl.values()) {
2329
- clearRoleRefsForCdpUrl(cur.cdpUrl);
2330
- if (cur.onDisconnected && typeof cur.browser.off === "function")
2331
- cur.browser.off("disconnected", cur.onDisconnected);
2332
- await cur.browser.close().catch(() => {
2419
+ for (const cur of cachedByCdpUrl.values()) {
2420
+ clearRoleRefsForCdpUrl(cur.cdpUrl);
2421
+ if (cur.onDisconnected && typeof cur.browser.off === "function")
2422
+ cur.browser.off("disconnected", cur.onDisconnected);
2423
+ await cur.browser.close().catch(() => {
2424
+ });
2425
+ }
2426
+ cachedByCdpUrl.clear();
2427
+ clearBlockedTargetsForCdpUrl();
2428
+ clearBlockedPageRefsForCdpUrl();
2429
+ });
2430
+ }
2431
+ async function closePlaywrightBrowserConnection(opts) {
2432
+ if (opts?.cdpUrl !== void 0 && opts.cdpUrl !== "") {
2433
+ return withConnectionLock(async () => {
2434
+ const cdpUrl = opts.cdpUrl;
2435
+ if (cdpUrl === void 0 || cdpUrl === "") return;
2436
+ const normalized = normalizeCdpUrl(cdpUrl);
2437
+ clearBlockedTargetsForCdpUrl(normalized);
2438
+ clearBlockedPageRefsForCdpUrl(normalized);
2439
+ const cur = cachedByCdpUrl.get(normalized);
2440
+ cachedByCdpUrl.delete(normalized);
2441
+ connectingByCdpUrl.delete(normalized);
2442
+ if (!cur) return;
2443
+ if (cur.onDisconnected && typeof cur.browser.off === "function")
2444
+ cur.browser.off("disconnected", cur.onDisconnected);
2445
+ await cur.browser.close().catch(() => {
2446
+ });
2333
2447
  });
2448
+ } else {
2449
+ await disconnectBrowser();
2334
2450
  }
2335
- cachedByCdpUrl.clear();
2336
2451
  }
2337
2452
  function cdpSocketNeedsAttach(wsUrl) {
2338
2453
  try {
@@ -2441,7 +2556,7 @@ async function forceDisconnectPlaywrightConnection(opts) {
2441
2556
  await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
2442
2557
  });
2443
2558
  }
2444
- cur.browser.close().catch(() => {
2559
+ await cur.browser.close().catch(() => {
2445
2560
  });
2446
2561
  }
2447
2562
  var forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightConnection;
@@ -2452,17 +2567,13 @@ var pageTargetIdCache = /* @__PURE__ */ new WeakMap();
2452
2567
  async function pageTargetId(page) {
2453
2568
  const cached = pageTargetIdCache.get(page);
2454
2569
  if (cached !== void 0) return cached;
2455
- const session = await page.context().newCDPSession(page);
2456
- try {
2570
+ return withPlaywrightPageCdpSession(page, async (session) => {
2457
2571
  const info = await session.send("Target.getTargetInfo");
2458
2572
  const targetInfo = info.targetInfo;
2459
2573
  const id = (targetInfo?.targetId ?? "").trim() || null;
2460
2574
  if (id !== null) pageTargetIdCache.set(page, id);
2461
2575
  return id;
2462
- } finally {
2463
- await session.detach().catch(() => {
2464
- });
2465
- }
2576
+ });
2466
2577
  }
2467
2578
  function matchPageByTargetList(pages, targets, targetId) {
2468
2579
  const target = targets.find((entry) => entry.id === targetId);
@@ -2498,7 +2609,6 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
2498
2609
  }
2499
2610
  })
2500
2611
  );
2501
- const resolvedViaCdp = results.some(({ tid }) => tid !== null);
2502
2612
  const matched = results.find(({ tid }) => tid !== null && tid !== "" && tid === targetId);
2503
2613
  if (matched) return matched.page;
2504
2614
  if (cdpUrl !== void 0 && cdpUrl !== "") {
@@ -2507,7 +2617,6 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
2507
2617
  } catch {
2508
2618
  }
2509
2619
  }
2510
- if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
2511
2620
  return null;
2512
2621
  }
2513
2622
  async function partitionAccessiblePages(opts) {
@@ -2550,12 +2659,14 @@ async function getPageForTargetId(opts) {
2550
2659
  if (opts.targetId === void 0 || opts.targetId === "") return first;
2551
2660
  const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
2552
2661
  if (!found) {
2553
- if (pages.length === 1) return first;
2554
2662
  throw new BrowserTabNotFoundError(
2555
2663
  `Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
2556
2664
  );
2557
2665
  }
2558
2666
  if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
2667
+ const foundTargetId = await pageTargetId(found).catch(() => null);
2668
+ if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
2669
+ throw new BlockedBrowserTargetError();
2559
2670
  return found;
2560
2671
  }
2561
2672
  async function resolvePageByTargetIdOrThrow(opts) {
@@ -2567,7 +2678,6 @@ async function resolvePageByTargetIdOrThrow(opts) {
2567
2678
  async function getRestoredPageForTarget(opts) {
2568
2679
  const page = await getPageForTargetId(opts);
2569
2680
  ensurePageState(page);
2570
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2571
2681
  return page;
2572
2682
  }
2573
2683
 
@@ -2621,10 +2731,11 @@ var BROWSER_EVALUATOR = new Function(
2621
2731
  " catch (_) { candidate = (0, eval)(fnBody); }",
2622
2732
  ' var result = typeof candidate === "function" ? candidate() : candidate;',
2623
2733
  ' if (result && typeof result.then === "function") {',
2734
+ " var tid;",
2624
2735
  " return Promise.race([",
2625
- " result,",
2736
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2626
2737
  " new Promise(function(_, reject) {",
2627
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2738
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2628
2739
  " })",
2629
2740
  " ]);",
2630
2741
  " }",
@@ -2646,10 +2757,11 @@ var ELEMENT_EVALUATOR = new Function(
2646
2757
  " catch (_) { candidate = (0, eval)(fnBody); }",
2647
2758
  ' var result = typeof candidate === "function" ? candidate(el) : candidate;',
2648
2759
  ' if (result && typeof result.then === "function") {',
2760
+ " var tid;",
2649
2761
  " return Promise.race([",
2650
- " result,",
2762
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2651
2763
  " new Promise(function(_, reject) {",
2652
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2764
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2653
2765
  " })",
2654
2766
  " ]);",
2655
2767
  " }",
@@ -2664,10 +2776,8 @@ async function evaluateViaPlaywright(opts) {
2664
2776
  if (!fnText) throw new Error("function is required");
2665
2777
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2666
2778
  ensurePageState(page);
2667
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2668
2779
  const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2669
- let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
2670
- evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
2780
+ const evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 1e3));
2671
2781
  const signal = opts.signal;
2672
2782
  let abortListener;
2673
2783
  let abortReject;
@@ -2681,10 +2791,16 @@ async function evaluateViaPlaywright(opts) {
2681
2791
  }
2682
2792
  if (signal !== void 0) {
2683
2793
  const disconnect = () => {
2684
- forceDisconnectPlaywrightConnection({
2685
- cdpUrl: opts.cdpUrl,
2686
- targetId: opts.targetId}).catch(() => {
2687
- });
2794
+ const targetId = opts.targetId?.trim() ?? "";
2795
+ if (targetId !== "") {
2796
+ tryTerminateExecutionViaCdp(opts.cdpUrl, targetId).catch(() => {
2797
+ });
2798
+ } else {
2799
+ console.warn("[browserclaw] evaluate abort: no targetId, forcing full disconnect");
2800
+ forceDisconnectPlaywrightConnection({
2801
+ cdpUrl: opts.cdpUrl}).catch(() => {
2802
+ });
2803
+ }
2688
2804
  };
2689
2805
  if (signal.aborted) {
2690
2806
  disconnect();
@@ -2720,6 +2836,8 @@ async function evaluateViaPlaywright(opts) {
2720
2836
  );
2721
2837
  } finally {
2722
2838
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
2839
+ abortReject = void 0;
2840
+ abortListener = void 0;
2723
2841
  }
2724
2842
  }
2725
2843
 
@@ -2912,7 +3030,7 @@ function isBlockedHostnameOrIp(hostname, policy) {
2912
3030
  function isPrivateIpAddress(address, policy) {
2913
3031
  let normalized = address.trim().toLowerCase();
2914
3032
  if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
2915
- if (!normalized) return false;
3033
+ if (!normalized) return true;
2916
3034
  const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
2917
3035
  const strictIp = parseCanonicalIpAddress(normalized);
2918
3036
  if (strictIp) {
@@ -2999,6 +3117,25 @@ function createPinnedLookup(params) {
2999
3117
  cb(null, chosen.address, chosen.family);
3000
3118
  });
3001
3119
  }
3120
+ var DNS_CACHE_TTL_MS = 3e4;
3121
+ var MAX_DNS_CACHE_SIZE = 100;
3122
+ var dnsCache = /* @__PURE__ */ new Map();
3123
+ function getCachedDnsResult(hostname) {
3124
+ const entry = dnsCache.get(hostname);
3125
+ if (!entry) return void 0;
3126
+ if (Date.now() > entry.expiresAt) {
3127
+ dnsCache.delete(hostname);
3128
+ return void 0;
3129
+ }
3130
+ return entry.result;
3131
+ }
3132
+ function cacheDnsResult(hostname, result) {
3133
+ dnsCache.set(hostname, { result, expiresAt: Date.now() + DNS_CACHE_TTL_MS });
3134
+ if (dnsCache.size > MAX_DNS_CACHE_SIZE) {
3135
+ const first = dnsCache.keys().next();
3136
+ if (first.done !== true) dnsCache.delete(first.value);
3137
+ }
3138
+ }
3002
3139
  async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3003
3140
  const normalized = normalizeHostname(hostname);
3004
3141
  if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
@@ -3017,6 +3154,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3017
3154
  );
3018
3155
  }
3019
3156
  }
3157
+ const cached = getCachedDnsResult(normalized);
3158
+ if (cached) return cached;
3020
3159
  const lookupFn = params.lookupFn ?? lookup$1;
3021
3160
  let results;
3022
3161
  try {
@@ -3046,11 +3185,13 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3046
3185
  `Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
3047
3186
  );
3048
3187
  }
3049
- return {
3188
+ const pinned = {
3050
3189
  hostname: normalized,
3051
3190
  addresses,
3052
3191
  lookup: createPinnedLookup({ hostname: normalized, addresses })
3053
3192
  };
3193
+ cacheDnsResult(normalized, pinned);
3194
+ return pinned;
3054
3195
  }
3055
3196
  async function assertBrowserNavigationAllowed(opts) {
3056
3197
  const rawUrl = opts.url.trim();
@@ -3204,13 +3345,27 @@ function buildSiblingTempPath(targetPath) {
3204
3345
  return join(dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
3205
3346
  }
3206
3347
  async function writeViaSiblingTempPath(params) {
3207
- const rootDir = await realpath(resolve(params.rootDir)).catch(() => resolve(params.rootDir));
3348
+ let rootDir;
3349
+ try {
3350
+ rootDir = await realpath(resolve(params.rootDir));
3351
+ } catch {
3352
+ console.warn(`[browserclaw] writeViaSiblingTempPath: rootDir realpath failed, using lexical resolve`);
3353
+ rootDir = resolve(params.rootDir);
3354
+ }
3208
3355
  const requestedTargetPath = resolve(params.targetPath);
3209
3356
  const targetPath = await realpath(dirname(requestedTargetPath)).then((realDir) => join(realDir, basename(requestedTargetPath))).catch(() => requestedTargetPath);
3210
3357
  const relativeTargetPath = relative(rootDir, targetPath);
3211
3358
  if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${sep}`) || isAbsolute(relativeTargetPath)) {
3212
3359
  throw new Error("Target path is outside the allowed root");
3213
3360
  }
3361
+ try {
3362
+ const stat = await lstat(targetPath);
3363
+ if (stat.isSymbolicLink()) {
3364
+ throw new Error(`Unsafe output path: "${params.targetPath}" is a symbolic link.`);
3365
+ }
3366
+ } catch (e) {
3367
+ if (e.code !== "ENOENT") throw e;
3368
+ }
3214
3369
  const tempPath = buildSiblingTempPath(targetPath);
3215
3370
  let renameSucceeded = false;
3216
3371
  try {
@@ -3232,6 +3387,9 @@ async function assertBrowserNavigationResultAllowed(opts) {
3232
3387
  } catch {
3233
3388
  return;
3234
3389
  }
3390
+ if (parsed.protocol === "data:" || parsed.protocol === "blob:") {
3391
+ throw new InvalidBrowserNavigationUrlError(`Navigation result blocked: "${parsed.protocol}" URLs are not allowed.`);
3392
+ }
3235
3393
  if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) {
3236
3394
  await assertBrowserNavigationAllowed(opts);
3237
3395
  }
@@ -3349,9 +3507,10 @@ async function clickViaPlaywright(opts) {
3349
3507
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3350
3508
  const POLL_INTERVAL_MS = 50;
3351
3509
  const POLL_TIMEOUT_MS = 500;
3510
+ const ATTR_TIMEOUT_MS = Math.min(timeout, POLL_TIMEOUT_MS);
3352
3511
  let changed = false;
3353
3512
  for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
3354
- const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3513
+ const current = await locator.getAttribute("aria-checked", { timeout: ATTR_TIMEOUT_MS }).catch(() => void 0);
3355
3514
  if (current === void 0 || current !== ariaCheckedBefore) {
3356
3515
  changed = true;
3357
3516
  break;
@@ -3428,6 +3587,7 @@ async function dragViaPlaywright(opts) {
3428
3587
  async function fillFormViaPlaywright(opts) {
3429
3588
  const page = await getRestoredPageForTarget(opts);
3430
3589
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3590
+ let filledCount = 0;
3431
3591
  for (const field of opts.fields) {
3432
3592
  const ref = field.ref.trim();
3433
3593
  const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
@@ -3446,16 +3606,24 @@ async function fillFormViaPlaywright(opts) {
3446
3606
  try {
3447
3607
  await setCheckedViaEvaluate(locator, checked);
3448
3608
  } catch (err) {
3449
- throw toAIFriendlyError(err, ref);
3609
+ const friendly = toAIFriendlyError(err, ref);
3610
+ throw new Error(
3611
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3612
+ );
3450
3613
  }
3451
3614
  }
3615
+ filledCount += 1;
3452
3616
  continue;
3453
3617
  }
3454
3618
  try {
3455
3619
  await locator.fill(value, { timeout });
3456
3620
  } catch (err) {
3457
- throw toAIFriendlyError(err, ref);
3621
+ const friendly = toAIFriendlyError(err, ref);
3622
+ throw new Error(
3623
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3624
+ );
3458
3625
  }
3626
+ filledCount += 1;
3459
3627
  }
3460
3628
  }
3461
3629
  async function scrollIntoViewViaPlaywright(opts) {
@@ -3521,16 +3689,22 @@ async function armDialogViaPlaywright(opts) {
3521
3689
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3522
3690
  state.armIdDialog = bumpDialogArmId(state);
3523
3691
  const armId = state.armIdDialog;
3692
+ const resetArm = () => {
3693
+ if (state.armIdDialog === armId) state.armIdDialog = 0;
3694
+ };
3695
+ page.once("close", resetArm);
3524
3696
  page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
3525
3697
  if (state.armIdDialog !== armId) return;
3526
3698
  try {
3527
3699
  if (opts.accept) await dialog.accept(opts.promptText);
3528
3700
  else await dialog.dismiss();
3529
3701
  } finally {
3530
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3702
+ resetArm();
3703
+ page.off("close", resetArm);
3531
3704
  }
3532
3705
  }).catch(() => {
3533
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3706
+ resetArm();
3707
+ page.off("close", resetArm);
3534
3708
  });
3535
3709
  }
3536
3710
  async function armFileUploadViaPlaywright(opts) {
@@ -3539,6 +3713,10 @@ async function armFileUploadViaPlaywright(opts) {
3539
3713
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3540
3714
  state.armIdUpload = bumpUploadArmId(state);
3541
3715
  const armId = state.armIdUpload;
3716
+ const resetArm = () => {
3717
+ if (state.armIdUpload === armId) state.armIdUpload = 0;
3718
+ };
3719
+ page.once("close", resetArm);
3542
3720
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3543
3721
  if (state.armIdUpload !== armId) return;
3544
3722
  if (opts.paths === void 0 || opts.paths.length === 0) {
@@ -3570,9 +3748,18 @@ async function armFileUploadViaPlaywright(opts) {
3570
3748
  el.dispatchEvent(new Event("change", { bubbles: true }));
3571
3749
  });
3572
3750
  }
3573
- } catch {
3751
+ } catch (e) {
3752
+ console.warn(
3753
+ `[browserclaw] armFileUpload: dispatch events failed: ${e instanceof Error ? e.message : String(e)}`
3754
+ );
3574
3755
  }
3575
- }).catch(() => {
3756
+ }).catch((e) => {
3757
+ console.warn(
3758
+ `[browserclaw] armFileUpload: filechooser wait failed: ${e instanceof Error ? e.message : String(e)}`
3759
+ );
3760
+ }).finally(() => {
3761
+ resetArm();
3762
+ page.off("close", resetArm);
3576
3763
  });
3577
3764
  }
3578
3765
 
@@ -3592,7 +3779,7 @@ function clearRecordingContext(cdpUrl) {
3592
3779
  }
3593
3780
  async function createRecordingContext(browser, cdpUrl, recordVideo) {
3594
3781
  const context = await browser.newContext({ recordVideo });
3595
- observeContext(context);
3782
+ await observeContext(context);
3596
3783
  recordingContexts.set(cdpUrl, context);
3597
3784
  context.on("close", () => recordingContexts.delete(cdpUrl));
3598
3785
  return context;
@@ -3647,6 +3834,11 @@ async function gotoPageWithNavigationGuard(opts) {
3647
3834
  await route.continue();
3648
3835
  return;
3649
3836
  }
3837
+ const isRedirect = request.redirectedFrom() !== null;
3838
+ if (!isRedirect && request.url() !== opts.url) {
3839
+ await route.continue();
3840
+ return;
3841
+ }
3650
3842
  try {
3651
3843
  await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
3652
3844
  } catch (err) {
@@ -3698,6 +3890,7 @@ async function navigateViaPlaywright(opts) {
3698
3890
  response = await navigate();
3699
3891
  } catch (err) {
3700
3892
  if (!isRetryableNavigateError(err)) throw err;
3893
+ recordingContexts.delete(opts.cdpUrl);
3701
3894
  await forceDisconnectPlaywrightConnection({
3702
3895
  cdpUrl: opts.cdpUrl,
3703
3896
  targetId: opts.targetId}).catch(() => {
@@ -3757,6 +3950,7 @@ async function createPageViaPlaywright(opts) {
3757
3950
  });
3758
3951
  } catch (err) {
3759
3952
  if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
3953
+ console.warn(`[browserclaw] createPage navigation failed: ${err instanceof Error ? err.message : String(err)}`);
3760
3954
  }
3761
3955
  await assertPageNavigationCompletedSafely({
3762
3956
  cdpUrl: opts.cdpUrl,
@@ -3843,39 +4037,46 @@ var MAX_WAIT_TIME_MS = 3e4;
3843
4037
  async function waitForViaPlaywright(opts) {
3844
4038
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3845
4039
  ensurePageState(page);
3846
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4040
+ const totalTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4041
+ const deadline = Date.now() + totalTimeout;
4042
+ const remaining = () => Math.max(500, deadline - Date.now());
3847
4043
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3848
4044
  await page.waitForTimeout(resolveBoundedDelayMs(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3849
4045
  }
3850
4046
  if (opts.text !== void 0 && opts.text !== "") {
3851
- await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
4047
+ await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, {
4048
+ timeout: remaining()
4049
+ });
3852
4050
  }
3853
4051
  if (opts.textGone !== void 0 && opts.textGone !== "") {
3854
- await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, { timeout });
4052
+ await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, {
4053
+ timeout: remaining()
4054
+ });
3855
4055
  }
3856
4056
  if (opts.selector !== void 0 && opts.selector !== "") {
3857
4057
  const selector = opts.selector.trim();
3858
- if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
4058
+ if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout: remaining() });
3859
4059
  }
3860
4060
  if (opts.url !== void 0 && opts.url !== "") {
3861
4061
  const url = opts.url.trim();
3862
- if (url !== "") await page.waitForURL(url, { timeout });
4062
+ if (url !== "") await page.waitForURL(url, { timeout: remaining() });
3863
4063
  }
3864
4064
  if (opts.loadState !== void 0) {
3865
- await page.waitForLoadState(opts.loadState, { timeout });
4065
+ await page.waitForLoadState(opts.loadState, { timeout: remaining() });
3866
4066
  }
3867
4067
  if (opts.fn !== void 0) {
3868
4068
  if (typeof opts.fn === "function") {
3869
- await page.waitForFunction(opts.fn, opts.arg, { timeout });
4069
+ await page.waitForFunction(opts.fn, opts.arg, { timeout: remaining() });
3870
4070
  } else {
3871
4071
  const fn = opts.fn.trim();
3872
- if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
4072
+ if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout: remaining() });
3873
4073
  }
3874
4074
  }
3875
4075
  }
3876
4076
 
3877
4077
  // src/actions/batch.ts
3878
4078
  var MAX_BATCH_DEPTH = 5;
4079
+ var MAX_BATCH_TIMEOUT_MS = 3e5;
3879
4080
  var MAX_BATCH_ACTIONS = 100;
3880
4081
  async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
3881
4082
  if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
@@ -4023,13 +4224,19 @@ async function batchViaPlaywright(opts) {
4023
4224
  throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
4024
4225
  const results = [];
4025
4226
  const evaluateEnabled = opts.evaluateEnabled !== false;
4227
+ const deadline = Date.now() + MAX_BATCH_TIMEOUT_MS;
4026
4228
  for (const action of opts.actions) {
4229
+ if (Date.now() > deadline) {
4230
+ results.push({ ok: false, error: "Batch timeout exceeded" });
4231
+ break;
4232
+ }
4027
4233
  try {
4028
4234
  await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
4029
4235
  results.push({ ok: true });
4030
4236
  } catch (err) {
4031
4237
  const message = err instanceof Error ? err.message : String(err);
4032
4238
  results.push({ ok: false, error: message });
4239
+ if (err instanceof BrowserTabNotFoundError || err instanceof BlockedBrowserTargetError) break;
4033
4240
  if (opts.stopOnError !== false) break;
4034
4241
  }
4035
4242
  }
@@ -4047,8 +4254,10 @@ function createPageDownloadWaiter(page, timeoutMs) {
4047
4254
  handler = void 0;
4048
4255
  }
4049
4256
  };
4257
+ let rejectPromise;
4050
4258
  return {
4051
4259
  promise: new Promise((resolve2, reject) => {
4260
+ rejectPromise = reject;
4052
4261
  handler = (download) => {
4053
4262
  if (done) return;
4054
4263
  done = true;
@@ -4067,6 +4276,7 @@ function createPageDownloadWaiter(page, timeoutMs) {
4067
4276
  if (done) return;
4068
4277
  done = true;
4069
4278
  cleanup();
4279
+ rejectPromise?.(new Error("Download waiter cancelled"));
4070
4280
  }
4071
4281
  };
4072
4282
  }
@@ -4098,12 +4308,11 @@ async function downloadViaPlaywright(opts) {
4098
4308
  await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
4099
4309
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4100
4310
  const state = ensurePageState(page);
4101
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4102
4311
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4103
4312
  const outPath = opts.path.trim();
4104
4313
  if (!outPath) throw new Error("path is required");
4105
- state.armIdDownload = bumpDownloadArmId(state);
4106
- const armId = state.armIdDownload;
4314
+ const armId = bumpDownloadArmId(state);
4315
+ state.armIdDownload = armId;
4107
4316
  const waiter = createPageDownloadWaiter(page, timeout);
4108
4317
  try {
4109
4318
  const locator = refLocator(page, opts.ref);
@@ -4150,12 +4359,6 @@ async function setDeviceViaPlaywright(opts) {
4150
4359
  }
4151
4360
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4152
4361
  ensurePageState(page);
4153
- if (device.viewport !== null) {
4154
- await page.setViewportSize({
4155
- width: device.viewport.width,
4156
- height: device.viewport.height
4157
- });
4158
- }
4159
4362
  await withPageScopedCdpClient({
4160
4363
  cdpUrl: opts.cdpUrl,
4161
4364
  page,
@@ -4183,11 +4386,24 @@ async function setDeviceViaPlaywright(opts) {
4183
4386
  }
4184
4387
  }
4185
4388
  });
4389
+ if (device.viewport !== null) {
4390
+ await page.setViewportSize({
4391
+ width: device.viewport.width,
4392
+ height: device.viewport.height
4393
+ });
4394
+ }
4186
4395
  }
4187
4396
  async function setExtraHTTPHeadersViaPlaywright(opts) {
4188
4397
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4189
4398
  ensurePageState(page);
4190
- await page.context().setExtraHTTPHeaders(opts.headers);
4399
+ await withPageScopedCdpClient({
4400
+ cdpUrl: opts.cdpUrl,
4401
+ page,
4402
+ targetId: opts.targetId,
4403
+ fn: async (send) => {
4404
+ await send("Network.setExtraHTTPHeaders", { headers: opts.headers });
4405
+ }
4406
+ });
4191
4407
  }
4192
4408
  async function setGeolocationViaPlaywright(opts) {
4193
4409
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -4195,7 +4411,8 @@ async function setGeolocationViaPlaywright(opts) {
4195
4411
  const context = page.context();
4196
4412
  if (opts.clear === true) {
4197
4413
  await context.setGeolocation(null);
4198
- await context.clearPermissions().catch(() => {
4414
+ await context.clearPermissions().catch((err) => {
4415
+ console.warn(`[browserclaw] clearPermissions failed: ${err instanceof Error ? err.message : String(err)}`);
4199
4416
  });
4200
4417
  return;
4201
4418
  }
@@ -4497,7 +4714,6 @@ async function waitForRequestViaPlaywright(opts) {
4497
4714
  async function takeScreenshotViaPlaywright(opts) {
4498
4715
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4499
4716
  ensurePageState(page);
4500
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4501
4717
  const type = opts.type ?? "png";
4502
4718
  if (opts.ref !== void 0 && opts.ref !== "") {
4503
4719
  if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
@@ -4512,7 +4728,6 @@ async function takeScreenshotViaPlaywright(opts) {
4512
4728
  async function screenshotWithLabelsViaPlaywright(opts) {
4513
4729
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4514
4730
  ensurePageState(page);
4515
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4516
4731
  const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
4517
4732
  const type = opts.type ?? "png";
4518
4733
  const refs = opts.refs.slice(0, maxLabels);
@@ -4576,7 +4791,8 @@ async function screenshotWithLabelsViaPlaywright(opts) {
4576
4791
  document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
4577
4792
  el.remove();
4578
4793
  });
4579
- }).catch(() => {
4794
+ }).catch((err) => {
4795
+ console.warn(`[browserclaw] label overlay cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
4580
4796
  });
4581
4797
  }
4582
4798
  }
@@ -4604,17 +4820,14 @@ async function traceStopViaPlaywright(opts) {
4604
4820
  if (!ctxState.traceActive) {
4605
4821
  throw new Error("No active trace. Start a trace before stopping it.");
4606
4822
  }
4607
- try {
4608
- await writeViaSiblingTempPath({
4609
- rootDir: dirname(opts.path),
4610
- targetPath: opts.path,
4611
- writeTemp: async (tempPath) => {
4612
- await context.tracing.stop({ path: tempPath });
4613
- }
4614
- });
4615
- } finally {
4616
- ctxState.traceActive = false;
4617
- }
4823
+ ctxState.traceActive = false;
4824
+ await writeViaSiblingTempPath({
4825
+ rootDir: dirname(opts.path),
4826
+ targetPath: opts.path,
4827
+ writeTemp: async (tempPath) => {
4828
+ await context.tracing.stop({ path: tempPath });
4829
+ }
4830
+ });
4618
4831
  }
4619
4832
 
4620
4833
  // src/snapshot/ref-map.ts
@@ -5175,17 +5388,27 @@ async function storageClearViaPlaywright(opts) {
5175
5388
  // src/browser.ts
5176
5389
  var CrawlPage = class {
5177
5390
  cdpUrl;
5178
- targetId;
5391
+ _targetId;
5179
5392
  ssrfPolicy;
5180
5393
  /** @internal */
5181
5394
  constructor(cdpUrl, targetId, ssrfPolicy) {
5182
5395
  this.cdpUrl = cdpUrl;
5183
- this.targetId = targetId;
5396
+ this._targetId = targetId;
5184
5397
  this.ssrfPolicy = ssrfPolicy;
5185
5398
  }
5186
5399
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
5187
5400
  get id() {
5188
- return this.targetId;
5401
+ return this._targetId;
5402
+ }
5403
+ /**
5404
+ * Refresh the target ID by re-resolving the page from the browser.
5405
+ * Useful after reconnection when the old target ID may be stale.
5406
+ */
5407
+ async refreshTargetId() {
5408
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5409
+ const newId = await pageTargetId(page);
5410
+ if (newId !== null && newId !== "") this._targetId = newId;
5411
+ return this._targetId;
5189
5412
  }
5190
5413
  // ── Snapshot ──────────────────────────────────────────────────
5191
5414
  /**
@@ -5213,7 +5436,7 @@ var CrawlPage = class {
5213
5436
  if (opts?.mode === "role") {
5214
5437
  return snapshotRole({
5215
5438
  cdpUrl: this.cdpUrl,
5216
- targetId: this.targetId,
5439
+ targetId: this._targetId,
5217
5440
  selector: opts.selector,
5218
5441
  frameSelector: opts.frameSelector,
5219
5442
  refsMode: opts.refsMode,
@@ -5232,7 +5455,7 @@ var CrawlPage = class {
5232
5455
  }
5233
5456
  return snapshotAi({
5234
5457
  cdpUrl: this.cdpUrl,
5235
- targetId: this.targetId,
5458
+ targetId: this._targetId,
5236
5459
  maxChars: opts?.maxChars,
5237
5460
  options: {
5238
5461
  interactive: opts?.interactive,
@@ -5251,7 +5474,7 @@ var CrawlPage = class {
5251
5474
  * @returns Array of accessibility tree nodes
5252
5475
  */
5253
5476
  async ariaSnapshot(opts) {
5254
- return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
5477
+ return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this._targetId, limit: opts?.limit });
5255
5478
  }
5256
5479
  // ── Interactions ─────────────────────────────────────────────
5257
5480
  /**
@@ -5271,7 +5494,7 @@ var CrawlPage = class {
5271
5494
  async click(ref, opts) {
5272
5495
  return clickViaPlaywright({
5273
5496
  cdpUrl: this.cdpUrl,
5274
- targetId: this.targetId,
5497
+ targetId: this._targetId,
5275
5498
  ref,
5276
5499
  doubleClick: opts?.doubleClick,
5277
5500
  button: opts?.button,
@@ -5298,7 +5521,7 @@ var CrawlPage = class {
5298
5521
  async clickBySelector(selector, opts) {
5299
5522
  return clickViaPlaywright({
5300
5523
  cdpUrl: this.cdpUrl,
5301
- targetId: this.targetId,
5524
+ targetId: this._targetId,
5302
5525
  selector,
5303
5526
  doubleClick: opts?.doubleClick,
5304
5527
  button: opts?.button,
@@ -5327,7 +5550,7 @@ var CrawlPage = class {
5327
5550
  async mouseClick(x, y, opts) {
5328
5551
  return mouseClickViaPlaywright({
5329
5552
  cdpUrl: this.cdpUrl,
5330
- targetId: this.targetId,
5553
+ targetId: this._targetId,
5331
5554
  x,
5332
5555
  y,
5333
5556
  button: opts?.button,
@@ -5354,7 +5577,7 @@ var CrawlPage = class {
5354
5577
  async pressAndHold(x, y, opts) {
5355
5578
  return pressAndHoldViaCdp({
5356
5579
  cdpUrl: this.cdpUrl,
5357
- targetId: this.targetId,
5580
+ targetId: this._targetId,
5358
5581
  x,
5359
5582
  y,
5360
5583
  delay: opts?.delay,
@@ -5378,7 +5601,7 @@ var CrawlPage = class {
5378
5601
  async clickByText(text, opts) {
5379
5602
  return clickByTextViaPlaywright({
5380
5603
  cdpUrl: this.cdpUrl,
5381
- targetId: this.targetId,
5604
+ targetId: this._targetId,
5382
5605
  text,
5383
5606
  exact: opts?.exact,
5384
5607
  button: opts?.button,
@@ -5405,7 +5628,7 @@ var CrawlPage = class {
5405
5628
  async clickByRole(role, name, opts) {
5406
5629
  return clickByRoleViaPlaywright({
5407
5630
  cdpUrl: this.cdpUrl,
5408
- targetId: this.targetId,
5631
+ targetId: this._targetId,
5409
5632
  role,
5410
5633
  name,
5411
5634
  index: opts?.index,
@@ -5434,7 +5657,7 @@ var CrawlPage = class {
5434
5657
  async type(ref, text, opts) {
5435
5658
  return typeViaPlaywright({
5436
5659
  cdpUrl: this.cdpUrl,
5437
- targetId: this.targetId,
5660
+ targetId: this._targetId,
5438
5661
  ref,
5439
5662
  text,
5440
5663
  submit: opts?.submit,
@@ -5451,7 +5674,7 @@ var CrawlPage = class {
5451
5674
  async hover(ref, opts) {
5452
5675
  return hoverViaPlaywright({
5453
5676
  cdpUrl: this.cdpUrl,
5454
- targetId: this.targetId,
5677
+ targetId: this._targetId,
5455
5678
  ref,
5456
5679
  timeoutMs: opts?.timeoutMs
5457
5680
  });
@@ -5471,7 +5694,7 @@ var CrawlPage = class {
5471
5694
  async select(ref, ...values) {
5472
5695
  return selectOptionViaPlaywright({
5473
5696
  cdpUrl: this.cdpUrl,
5474
- targetId: this.targetId,
5697
+ targetId: this._targetId,
5475
5698
  ref,
5476
5699
  values
5477
5700
  });
@@ -5486,7 +5709,7 @@ var CrawlPage = class {
5486
5709
  async drag(startRef, endRef, opts) {
5487
5710
  return dragViaPlaywright({
5488
5711
  cdpUrl: this.cdpUrl,
5489
- targetId: this.targetId,
5712
+ targetId: this._targetId,
5490
5713
  startRef,
5491
5714
  endRef,
5492
5715
  timeoutMs: opts?.timeoutMs
@@ -5511,7 +5734,7 @@ var CrawlPage = class {
5511
5734
  async fill(fields) {
5512
5735
  return fillFormViaPlaywright({
5513
5736
  cdpUrl: this.cdpUrl,
5514
- targetId: this.targetId,
5737
+ targetId: this._targetId,
5515
5738
  fields
5516
5739
  });
5517
5740
  }
@@ -5524,7 +5747,7 @@ var CrawlPage = class {
5524
5747
  async scrollIntoView(ref, opts) {
5525
5748
  return scrollIntoViewViaPlaywright({
5526
5749
  cdpUrl: this.cdpUrl,
5527
- targetId: this.targetId,
5750
+ targetId: this._targetId,
5528
5751
  ref,
5529
5752
  timeoutMs: opts?.timeoutMs
5530
5753
  });
@@ -5537,7 +5760,7 @@ var CrawlPage = class {
5537
5760
  async highlight(ref) {
5538
5761
  return highlightViaPlaywright({
5539
5762
  cdpUrl: this.cdpUrl,
5540
- targetId: this.targetId,
5763
+ targetId: this._targetId,
5541
5764
  ref
5542
5765
  });
5543
5766
  }
@@ -5550,7 +5773,7 @@ var CrawlPage = class {
5550
5773
  async uploadFile(ref, paths) {
5551
5774
  return setInputFilesViaPlaywright({
5552
5775
  cdpUrl: this.cdpUrl,
5553
- targetId: this.targetId,
5776
+ targetId: this._targetId,
5554
5777
  ref,
5555
5778
  paths
5556
5779
  });
@@ -5573,7 +5796,7 @@ var CrawlPage = class {
5573
5796
  async armDialog(opts) {
5574
5797
  return armDialogViaPlaywright({
5575
5798
  cdpUrl: this.cdpUrl,
5576
- targetId: this.targetId,
5799
+ targetId: this._targetId,
5577
5800
  accept: opts.accept,
5578
5801
  promptText: opts.promptText,
5579
5802
  timeoutMs: opts.timeoutMs
@@ -5617,7 +5840,7 @@ var CrawlPage = class {
5617
5840
  async onDialog(handler) {
5618
5841
  return setDialogHandler({
5619
5842
  cdpUrl: this.cdpUrl,
5620
- targetId: this.targetId,
5843
+ targetId: this._targetId,
5621
5844
  handler: handler ?? void 0
5622
5845
  });
5623
5846
  }
@@ -5639,7 +5862,7 @@ var CrawlPage = class {
5639
5862
  async armFileUpload(paths, opts) {
5640
5863
  return armFileUploadViaPlaywright({
5641
5864
  cdpUrl: this.cdpUrl,
5642
- targetId: this.targetId,
5865
+ targetId: this._targetId,
5643
5866
  paths,
5644
5867
  timeoutMs: opts?.timeoutMs
5645
5868
  });
@@ -5654,7 +5877,7 @@ var CrawlPage = class {
5654
5877
  async batch(actions, opts) {
5655
5878
  return batchViaPlaywright({
5656
5879
  cdpUrl: this.cdpUrl,
5657
- targetId: this.targetId,
5880
+ targetId: this._targetId,
5658
5881
  actions,
5659
5882
  stopOnError: opts?.stopOnError,
5660
5883
  evaluateEnabled: opts?.evaluateEnabled
@@ -5679,7 +5902,7 @@ var CrawlPage = class {
5679
5902
  async press(key, opts) {
5680
5903
  return pressKeyViaPlaywright({
5681
5904
  cdpUrl: this.cdpUrl,
5682
- targetId: this.targetId,
5905
+ targetId: this._targetId,
5683
5906
  key,
5684
5907
  delayMs: opts?.delayMs
5685
5908
  });
@@ -5689,14 +5912,14 @@ var CrawlPage = class {
5689
5912
  * Get the current URL of the page.
5690
5913
  */
5691
5914
  async url() {
5692
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5915
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5693
5916
  return page.url();
5694
5917
  }
5695
5918
  /**
5696
5919
  * Get the page title.
5697
5920
  */
5698
5921
  async title() {
5699
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5922
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5700
5923
  return page.title();
5701
5924
  }
5702
5925
  /**
@@ -5709,7 +5932,7 @@ var CrawlPage = class {
5709
5932
  async goto(url, opts) {
5710
5933
  return navigateViaPlaywright({
5711
5934
  cdpUrl: this.cdpUrl,
5712
- targetId: this.targetId,
5935
+ targetId: this._targetId,
5713
5936
  url,
5714
5937
  timeoutMs: opts?.timeoutMs,
5715
5938
  ssrfPolicy: this.ssrfPolicy
@@ -5721,7 +5944,7 @@ var CrawlPage = class {
5721
5944
  * @param opts - Timeout options
5722
5945
  */
5723
5946
  async reload(opts) {
5724
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5947
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5725
5948
  ensurePageState(page);
5726
5949
  await page.reload({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5727
5950
  }
@@ -5731,7 +5954,7 @@ var CrawlPage = class {
5731
5954
  * @param opts - Timeout options
5732
5955
  */
5733
5956
  async goBack(opts) {
5734
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5957
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5735
5958
  ensurePageState(page);
5736
5959
  await page.goBack({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5737
5960
  }
@@ -5741,7 +5964,7 @@ var CrawlPage = class {
5741
5964
  * @param opts - Timeout options
5742
5965
  */
5743
5966
  async goForward(opts) {
5744
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5967
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5745
5968
  ensurePageState(page);
5746
5969
  await page.goForward({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5747
5970
  }
@@ -5764,7 +5987,7 @@ var CrawlPage = class {
5764
5987
  async waitFor(opts) {
5765
5988
  return waitForViaPlaywright({
5766
5989
  cdpUrl: this.cdpUrl,
5767
- targetId: this.targetId,
5990
+ targetId: this._targetId,
5768
5991
  ...opts
5769
5992
  });
5770
5993
  }
@@ -5789,7 +6012,7 @@ var CrawlPage = class {
5789
6012
  async evaluate(fn, opts) {
5790
6013
  return evaluateViaPlaywright({
5791
6014
  cdpUrl: this.cdpUrl,
5792
- targetId: this.targetId,
6015
+ targetId: this._targetId,
5793
6016
  fn,
5794
6017
  ref: opts?.ref,
5795
6018
  timeoutMs: opts?.timeoutMs,
@@ -5816,7 +6039,7 @@ var CrawlPage = class {
5816
6039
  async evaluateInAllFrames(fn) {
5817
6040
  return evaluateInAllFramesViaPlaywright({
5818
6041
  cdpUrl: this.cdpUrl,
5819
- targetId: this.targetId,
6042
+ targetId: this._targetId,
5820
6043
  fn
5821
6044
  });
5822
6045
  }
@@ -5837,7 +6060,7 @@ var CrawlPage = class {
5837
6060
  async screenshot(opts) {
5838
6061
  const result = await takeScreenshotViaPlaywright({
5839
6062
  cdpUrl: this.cdpUrl,
5840
- targetId: this.targetId,
6063
+ targetId: this._targetId,
5841
6064
  fullPage: opts?.fullPage,
5842
6065
  ref: opts?.ref,
5843
6066
  element: opts?.element,
@@ -5853,7 +6076,7 @@ var CrawlPage = class {
5853
6076
  * @returns PDF document as a Buffer
5854
6077
  */
5855
6078
  async pdf() {
5856
- const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6079
+ const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5857
6080
  return result.buffer;
5858
6081
  }
5859
6082
  /**
@@ -5874,7 +6097,7 @@ var CrawlPage = class {
5874
6097
  async screenshotWithLabels(refs, opts) {
5875
6098
  return screenshotWithLabelsViaPlaywright({
5876
6099
  cdpUrl: this.cdpUrl,
5877
- targetId: this.targetId,
6100
+ targetId: this._targetId,
5878
6101
  refs,
5879
6102
  maxLabels: opts?.maxLabels,
5880
6103
  type: opts?.type
@@ -5891,7 +6114,7 @@ var CrawlPage = class {
5891
6114
  async traceStart(opts) {
5892
6115
  return traceStartViaPlaywright({
5893
6116
  cdpUrl: this.cdpUrl,
5894
- targetId: this.targetId,
6117
+ targetId: this._targetId,
5895
6118
  screenshots: opts?.screenshots,
5896
6119
  snapshots: opts?.snapshots,
5897
6120
  sources: opts?.sources
@@ -5906,7 +6129,7 @@ var CrawlPage = class {
5906
6129
  async traceStop(path2, opts) {
5907
6130
  return traceStopViaPlaywright({
5908
6131
  cdpUrl: this.cdpUrl,
5909
- targetId: this.targetId,
6132
+ targetId: this._targetId,
5910
6133
  path: path2,
5911
6134
  allowedOutputRoots: opts?.allowedOutputRoots
5912
6135
  });
@@ -5927,7 +6150,7 @@ var CrawlPage = class {
5927
6150
  async responseBody(url, opts) {
5928
6151
  return responseBodyViaPlaywright({
5929
6152
  cdpUrl: this.cdpUrl,
5930
- targetId: this.targetId,
6153
+ targetId: this._targetId,
5931
6154
  url,
5932
6155
  timeoutMs: opts?.timeoutMs,
5933
6156
  maxChars: opts?.maxChars
@@ -5955,7 +6178,7 @@ var CrawlPage = class {
5955
6178
  async waitForRequest(url, opts) {
5956
6179
  return waitForRequestViaPlaywright({
5957
6180
  cdpUrl: this.cdpUrl,
5958
- targetId: this.targetId,
6181
+ targetId: this._targetId,
5959
6182
  url,
5960
6183
  method: opts?.method,
5961
6184
  timeoutMs: opts?.timeoutMs,
@@ -5973,7 +6196,7 @@ var CrawlPage = class {
5973
6196
  async consoleLogs(opts) {
5974
6197
  return getConsoleMessagesViaPlaywright({
5975
6198
  cdpUrl: this.cdpUrl,
5976
- targetId: this.targetId,
6199
+ targetId: this._targetId,
5977
6200
  level: opts?.level,
5978
6201
  clear: opts?.clear
5979
6202
  });
@@ -5987,7 +6210,7 @@ var CrawlPage = class {
5987
6210
  async pageErrors(opts) {
5988
6211
  const result = await getPageErrorsViaPlaywright({
5989
6212
  cdpUrl: this.cdpUrl,
5990
- targetId: this.targetId,
6213
+ targetId: this._targetId,
5991
6214
  clear: opts?.clear
5992
6215
  });
5993
6216
  return result.errors;
@@ -6008,7 +6231,7 @@ var CrawlPage = class {
6008
6231
  async networkRequests(opts) {
6009
6232
  const result = await getNetworkRequestsViaPlaywright({
6010
6233
  cdpUrl: this.cdpUrl,
6011
- targetId: this.targetId,
6234
+ targetId: this._targetId,
6012
6235
  filter: opts?.filter,
6013
6236
  clear: opts?.clear
6014
6237
  });
@@ -6024,7 +6247,7 @@ var CrawlPage = class {
6024
6247
  async resize(width, height) {
6025
6248
  return resizeViewportViaPlaywright({
6026
6249
  cdpUrl: this.cdpUrl,
6027
- targetId: this.targetId,
6250
+ targetId: this._targetId,
6028
6251
  width,
6029
6252
  height
6030
6253
  });
@@ -6036,7 +6259,7 @@ var CrawlPage = class {
6036
6259
  * @returns Array of cookie objects
6037
6260
  */
6038
6261
  async cookies() {
6039
- const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6262
+ const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6040
6263
  return result.cookies;
6041
6264
  }
6042
6265
  /**
@@ -6054,11 +6277,11 @@ var CrawlPage = class {
6054
6277
  * ```
6055
6278
  */
6056
6279
  async setCookie(cookie) {
6057
- return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
6280
+ return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId, cookie });
6058
6281
  }
6059
6282
  /** Clear all cookies in the browser context. */
6060
6283
  async clearCookies() {
6061
- return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6284
+ return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6062
6285
  }
6063
6286
  /**
6064
6287
  * Get values from localStorage or sessionStorage.
@@ -6070,7 +6293,7 @@ var CrawlPage = class {
6070
6293
  async storageGet(kind, key) {
6071
6294
  const result = await storageGetViaPlaywright({
6072
6295
  cdpUrl: this.cdpUrl,
6073
- targetId: this.targetId,
6296
+ targetId: this._targetId,
6074
6297
  kind,
6075
6298
  key
6076
6299
  });
@@ -6086,7 +6309,7 @@ var CrawlPage = class {
6086
6309
  async storageSet(kind, key, value) {
6087
6310
  return storageSetViaPlaywright({
6088
6311
  cdpUrl: this.cdpUrl,
6089
- targetId: this.targetId,
6312
+ targetId: this._targetId,
6090
6313
  kind,
6091
6314
  key,
6092
6315
  value
@@ -6100,7 +6323,7 @@ var CrawlPage = class {
6100
6323
  async storageClear(kind) {
6101
6324
  return storageClearViaPlaywright({
6102
6325
  cdpUrl: this.cdpUrl,
6103
- targetId: this.targetId,
6326
+ targetId: this._targetId,
6104
6327
  kind
6105
6328
  });
6106
6329
  }
@@ -6122,7 +6345,7 @@ var CrawlPage = class {
6122
6345
  async download(ref, path2, opts) {
6123
6346
  return downloadViaPlaywright({
6124
6347
  cdpUrl: this.cdpUrl,
6125
- targetId: this.targetId,
6348
+ targetId: this._targetId,
6126
6349
  ref,
6127
6350
  path: path2,
6128
6351
  timeoutMs: opts?.timeoutMs,
@@ -6140,7 +6363,7 @@ var CrawlPage = class {
6140
6363
  async waitForDownload(opts) {
6141
6364
  return waitForDownloadViaPlaywright({
6142
6365
  cdpUrl: this.cdpUrl,
6143
- targetId: this.targetId,
6366
+ targetId: this._targetId,
6144
6367
  path: opts?.path,
6145
6368
  timeoutMs: opts?.timeoutMs,
6146
6369
  allowedOutputRoots: opts?.allowedOutputRoots
@@ -6155,7 +6378,7 @@ var CrawlPage = class {
6155
6378
  async setOffline(offline) {
6156
6379
  return setOfflineViaPlaywright({
6157
6380
  cdpUrl: this.cdpUrl,
6158
- targetId: this.targetId,
6381
+ targetId: this._targetId,
6159
6382
  offline
6160
6383
  });
6161
6384
  }
@@ -6172,7 +6395,7 @@ var CrawlPage = class {
6172
6395
  async setExtraHeaders(headers) {
6173
6396
  return setExtraHTTPHeadersViaPlaywright({
6174
6397
  cdpUrl: this.cdpUrl,
6175
- targetId: this.targetId,
6398
+ targetId: this._targetId,
6176
6399
  headers
6177
6400
  });
6178
6401
  }
@@ -6184,7 +6407,7 @@ var CrawlPage = class {
6184
6407
  async setHttpCredentials(opts) {
6185
6408
  return setHttpCredentialsViaPlaywright({
6186
6409
  cdpUrl: this.cdpUrl,
6187
- targetId: this.targetId,
6410
+ targetId: this._targetId,
6188
6411
  username: opts.username,
6189
6412
  password: opts.password,
6190
6413
  clear: opts.clear
@@ -6204,7 +6427,7 @@ var CrawlPage = class {
6204
6427
  async setGeolocation(opts) {
6205
6428
  return setGeolocationViaPlaywright({
6206
6429
  cdpUrl: this.cdpUrl,
6207
- targetId: this.targetId,
6430
+ targetId: this._targetId,
6208
6431
  latitude: opts.latitude,
6209
6432
  longitude: opts.longitude,
6210
6433
  accuracy: opts.accuracy,
@@ -6225,7 +6448,7 @@ var CrawlPage = class {
6225
6448
  async emulateMedia(opts) {
6226
6449
  return emulateMediaViaPlaywright({
6227
6450
  cdpUrl: this.cdpUrl,
6228
- targetId: this.targetId,
6451
+ targetId: this._targetId,
6229
6452
  colorScheme: opts.colorScheme
6230
6453
  });
6231
6454
  }
@@ -6237,7 +6460,7 @@ var CrawlPage = class {
6237
6460
  async setLocale(locale) {
6238
6461
  return setLocaleViaPlaywright({
6239
6462
  cdpUrl: this.cdpUrl,
6240
- targetId: this.targetId,
6463
+ targetId: this._targetId,
6241
6464
  locale
6242
6465
  });
6243
6466
  }
@@ -6249,7 +6472,7 @@ var CrawlPage = class {
6249
6472
  async setTimezone(timezoneId) {
6250
6473
  return setTimezoneViaPlaywright({
6251
6474
  cdpUrl: this.cdpUrl,
6252
- targetId: this.targetId,
6475
+ targetId: this._targetId,
6253
6476
  timezoneId
6254
6477
  });
6255
6478
  }
@@ -6266,7 +6489,7 @@ var CrawlPage = class {
6266
6489
  async setDevice(name) {
6267
6490
  return setDeviceViaPlaywright({
6268
6491
  cdpUrl: this.cdpUrl,
6269
- targetId: this.targetId,
6492
+ targetId: this._targetId,
6270
6493
  name
6271
6494
  });
6272
6495
  }
@@ -6287,7 +6510,7 @@ var CrawlPage = class {
6287
6510
  * ```
6288
6511
  */
6289
6512
  async detectChallenge() {
6290
- return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6513
+ return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6291
6514
  }
6292
6515
  /**
6293
6516
  * Wait for an anti-bot challenge to resolve on its own.
@@ -6312,7 +6535,7 @@ var CrawlPage = class {
6312
6535
  async waitForChallenge(opts) {
6313
6536
  return waitForChallengeViaPlaywright({
6314
6537
  cdpUrl: this.cdpUrl,
6315
- targetId: this.targetId,
6538
+ targetId: this._targetId,
6316
6539
  timeoutMs: opts?.timeoutMs,
6317
6540
  pollMs: opts?.pollMs
6318
6541
  });
@@ -6349,8 +6572,24 @@ var CrawlPage = class {
6349
6572
  */
6350
6573
  async isAuthenticated(rules) {
6351
6574
  if (!rules.length) return { authenticated: true, checks: [] };
6352
- const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6575
+ const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6353
6576
  const checks = [];
6577
+ const needsBodyText = rules.some((r) => r.text !== void 0 || r.textGone !== void 0);
6578
+ let bodyText = null;
6579
+ if (needsBodyText) {
6580
+ try {
6581
+ const raw = await evaluateViaPlaywright({
6582
+ cdpUrl: this.cdpUrl,
6583
+ targetId: this._targetId,
6584
+ fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6585
+ });
6586
+ bodyText = typeof raw === "string" ? raw : null;
6587
+ } catch (err) {
6588
+ console.warn(
6589
+ `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6590
+ );
6591
+ }
6592
+ }
6354
6593
  for (const rule of rules) {
6355
6594
  if (rule.url !== void 0) {
6356
6595
  const currentUrl = page.url();
@@ -6383,19 +6622,6 @@ var CrawlPage = class {
6383
6622
  }
6384
6623
  }
6385
6624
  if (rule.text !== void 0 || rule.textGone !== void 0) {
6386
- let bodyText = null;
6387
- try {
6388
- const raw = await evaluateViaPlaywright({
6389
- cdpUrl: this.cdpUrl,
6390
- targetId: this.targetId,
6391
- fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6392
- });
6393
- bodyText = typeof raw === "string" ? raw : null;
6394
- } catch (err) {
6395
- console.warn(
6396
- `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6397
- );
6398
- }
6399
6625
  if (rule.text !== void 0) {
6400
6626
  if (bodyText === null) {
6401
6627
  checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
@@ -6425,7 +6651,7 @@ var CrawlPage = class {
6425
6651
  try {
6426
6652
  const result = await evaluateViaPlaywright({
6427
6653
  cdpUrl: this.cdpUrl,
6428
- targetId: this.targetId,
6654
+ targetId: this._targetId,
6429
6655
  fn: rule.fn
6430
6656
  });
6431
6657
  const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
@@ -6474,7 +6700,7 @@ var CrawlPage = class {
6474
6700
  * ```
6475
6701
  */
6476
6702
  async playwrightPage() {
6477
- return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6703
+ return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6478
6704
  }
6479
6705
  /**
6480
6706
  * Create a Playwright `Locator` for a CSS selector on this page.
@@ -6497,7 +6723,7 @@ var CrawlPage = class {
6497
6723
  * ```
6498
6724
  */
6499
6725
  async locator(selector) {
6500
- const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6726
+ const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6501
6727
  return pwPage.locator(selector);
6502
6728
  }
6503
6729
  };
@@ -6541,21 +6767,27 @@ var BrowserClaw = class _BrowserClaw {
6541
6767
  static async launch(opts = {}) {
6542
6768
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6543
6769
  const chrome = await launchChrome(opts);
6544
- const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6545
- const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6546
- const telemetry = {
6547
- launchMs: chrome.launchMs,
6548
- timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6549
- };
6550
- const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6551
- if (opts.url !== void 0 && opts.url !== "") {
6552
- const page = await browser.currentPage();
6553
- const navT0 = Date.now();
6554
- await page.goto(opts.url);
6555
- telemetry.navMs = Date.now() - navT0;
6556
- telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6770
+ try {
6771
+ const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6772
+ const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6773
+ const telemetry = {
6774
+ launchMs: chrome.launchMs,
6775
+ timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6776
+ };
6777
+ const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6778
+ if (opts.url !== void 0 && opts.url !== "") {
6779
+ const page = await browser.currentPage();
6780
+ const navT0 = Date.now();
6781
+ await page.goto(opts.url);
6782
+ telemetry.navMs = Date.now() - navT0;
6783
+ telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6784
+ }
6785
+ return browser;
6786
+ } catch (err) {
6787
+ await stopChrome(chrome).catch(() => {
6788
+ });
6789
+ throw err;
6557
6790
  }
6558
- return browser;
6559
6791
  }
6560
6792
  /**
6561
6793
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -6712,7 +6944,7 @@ var BrowserClaw = class _BrowserClaw {
6712
6944
  if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
6713
6945
  try {
6714
6946
  clearRecordingContext(this.cdpUrl);
6715
- await disconnectBrowser();
6947
+ await closePlaywrightBrowserConnection({ cdpUrl: this.cdpUrl });
6716
6948
  if (this.chrome) {
6717
6949
  await stopChrome(this.chrome);
6718
6950
  this.chrome = null;