browserclaw 0.12.2 → 0.12.3

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.cjs CHANGED
@@ -834,6 +834,19 @@ var require_ipaddr = __commonJS({
834
834
  })(exports$1);
835
835
  }
836
836
  });
837
+ function killProcessTree(proc, signal) {
838
+ if (process.platform !== "win32" && proc.pid !== void 0) {
839
+ try {
840
+ process.kill(-proc.pid, signal);
841
+ return;
842
+ } catch {
843
+ }
844
+ }
845
+ try {
846
+ proc.kill(signal);
847
+ } catch {
848
+ }
849
+ }
837
850
  var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
838
851
  "com.google.Chrome",
839
852
  "com.google.Chrome.beta",
@@ -1189,17 +1202,30 @@ function resolveBrowserExecutable(opts) {
1189
1202
  if (platform === "win32") return detectDefaultChromiumWindows() ?? findChromeWindows();
1190
1203
  return null;
1191
1204
  }
1192
- async function ensurePortAvailable(port) {
1193
- await new Promise((resolve2, reject) => {
1194
- const tester = net__default.default.createServer().once("error", (err) => {
1195
- if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
1196
- else reject(err);
1197
- }).once("listening", () => {
1198
- tester.close(() => {
1199
- resolve2();
1205
+ async function ensurePortAvailable(port, retries = 2) {
1206
+ for (let attempt = 0; attempt <= retries; attempt++) {
1207
+ try {
1208
+ await new Promise((resolve2, reject) => {
1209
+ const tester = net__default.default.createServer().once("error", (err) => {
1210
+ tester.close(() => {
1211
+ if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
1212
+ else reject(err);
1213
+ });
1214
+ }).once("listening", () => {
1215
+ tester.close(() => {
1216
+ resolve2();
1217
+ });
1218
+ }).listen(port);
1200
1219
  });
1201
- }).listen(port);
1202
- });
1220
+ return;
1221
+ } catch (err) {
1222
+ if (attempt < retries) {
1223
+ await new Promise((r) => setTimeout(r, 100));
1224
+ continue;
1225
+ }
1226
+ throw err;
1227
+ }
1228
+ }
1203
1229
  }
1204
1230
  function safeReadJson(filePath) {
1205
1231
  try {
@@ -1472,7 +1498,7 @@ async function launchChrome(opts = {}) {
1472
1498
  const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
1473
1499
  const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
1474
1500
  fs__default.default.mkdirSync(userDataDir, { recursive: true });
1475
- const spawnChrome = () => {
1501
+ const spawnChrome = (spawnOpts) => {
1476
1502
  const args = [
1477
1503
  `--remote-debugging-port=${String(cdpPort)}`,
1478
1504
  "--remote-debugging-address=127.0.0.1",
@@ -1503,33 +1529,29 @@ async function launchChrome(opts = {}) {
1503
1529
  args.push("about:blank");
1504
1530
  return child_process.spawn(exe.path, args, {
1505
1531
  stdio: "pipe",
1506
- env: { ...process.env, HOME: os__default.default.homedir() }
1532
+ env: { ...process.env, HOME: os__default.default.homedir() },
1533
+ ...spawnOpts
1507
1534
  });
1508
1535
  };
1509
1536
  const startedAt = Date.now();
1510
1537
  const localStatePath = path__default.default.join(userDataDir, "Local State");
1511
1538
  const preferencesPath = path__default.default.join(userDataDir, "Default", "Preferences");
1512
1539
  if (!fileExists(localStatePath) || !fileExists(preferencesPath)) {
1513
- const bootstrap = spawnChrome();
1540
+ const useDetached = process.platform !== "win32";
1541
+ const bootstrap = spawnChrome(useDetached ? { detached: true } : void 0);
1514
1542
  const deadline = Date.now() + 1e4;
1515
1543
  while (Date.now() < deadline) {
1516
1544
  if (fileExists(localStatePath) && fileExists(preferencesPath)) break;
1517
1545
  await new Promise((r) => setTimeout(r, 100));
1518
1546
  }
1519
- try {
1520
- bootstrap.kill("SIGTERM");
1521
- } catch {
1522
- }
1547
+ killProcessTree(bootstrap, "SIGTERM");
1523
1548
  const exitDeadline = Date.now() + 5e3;
1524
1549
  while (Date.now() < exitDeadline) {
1525
1550
  if (bootstrap.exitCode != null) break;
1526
1551
  await new Promise((r) => setTimeout(r, 50));
1527
1552
  }
1528
1553
  if (bootstrap.exitCode == null) {
1529
- try {
1530
- bootstrap.kill("SIGKILL");
1531
- } catch {
1532
- }
1554
+ killProcessTree(bootstrap, "SIGKILL");
1533
1555
  }
1534
1556
  }
1535
1557
  try {
@@ -1548,9 +1570,11 @@ async function launchChrome(opts = {}) {
1548
1570
  };
1549
1571
  proc.stderr.on("data", onStderr);
1550
1572
  const readyDeadline = Date.now() + 15e3;
1573
+ let pollDelay = 200;
1551
1574
  while (Date.now() < readyDeadline) {
1552
1575
  if (await isChromeCdpReady(cdpUrl, 500)) break;
1553
- await new Promise((r) => setTimeout(r, 200));
1576
+ await new Promise((r) => setTimeout(r, pollDelay));
1577
+ pollDelay = Math.min(pollDelay + 100, 1e3);
1554
1578
  }
1555
1579
  if (!await isChromeCdpReady(cdpUrl, 500)) {
1556
1580
  const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
@@ -1562,6 +1586,11 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1562
1586
  proc.kill("SIGKILL");
1563
1587
  } catch {
1564
1588
  }
1589
+ try {
1590
+ const lockFile = path__default.default.join(userDataDir, "SingletonLock");
1591
+ if (fs__default.default.existsSync(lockFile)) fs__default.default.unlinkSync(lockFile);
1592
+ } catch {
1593
+ }
1565
1594
  throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
1566
1595
  }
1567
1596
  proc.stderr.off("data", onStderr);
@@ -1580,19 +1609,13 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1580
1609
  async function stopChrome(running, timeoutMs = 2500) {
1581
1610
  const proc = running.proc;
1582
1611
  if (proc.exitCode !== null) return;
1583
- try {
1584
- proc.kill("SIGTERM");
1585
- } catch {
1586
- }
1612
+ killProcessTree(proc, "SIGTERM");
1587
1613
  const start = Date.now();
1588
1614
  while (Date.now() - start < timeoutMs) {
1589
1615
  if (proc.exitCode !== null) return;
1590
1616
  await new Promise((r) => setTimeout(r, 100));
1591
1617
  }
1592
- try {
1593
- proc.kill("SIGKILL");
1594
- } catch {
1595
- }
1618
+ killProcessTree(proc, "SIGKILL");
1596
1619
  }
1597
1620
 
1598
1621
  // src/stealth.ts
@@ -1665,6 +1688,9 @@ var STEALTH_SCRIPT = `(function() {
1665
1688
  });
1666
1689
 
1667
1690
  // \u2500\u2500 4. window.chrome \u2500\u2500
1691
+ // Stub the chrome.runtime API surface that detection scripts probe for.
1692
+ // Only applied when the real chrome.runtime.connect is absent (headless/CDP mode).
1693
+ // The stubs are intentionally non-functional \u2014 they exist solely to pass presence checks.
1668
1694
  p(function() {
1669
1695
  if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;
1670
1696
 
@@ -1715,6 +1741,9 @@ var STEALTH_SCRIPT = `(function() {
1715
1741
  });
1716
1742
 
1717
1743
  // \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500
1744
+ // Hardcoded to Intel Iris \u2014 the most common discrete GPU on macOS. These strings
1745
+ // are fingerprinting targets; a more sophisticated approach would randomize per-session,
1746
+ // but static values are sufficient to avoid the default "Google SwiftShader" headless signal.
1718
1747
  p(function() {
1719
1748
  var h = {
1720
1749
  apply: function(target, self, args) {
@@ -1850,7 +1879,7 @@ function ensurePageState(page) {
1850
1879
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1851
1880
  location: msg.location()
1852
1881
  });
1853
- if (state.console.length > MAX_CONSOLE_MESSAGES) state.console.shift();
1882
+ if (state.console.length > MAX_CONSOLE_MESSAGES + 50) state.console.splice(0, 50);
1854
1883
  });
1855
1884
  page.on("pageerror", (err) => {
1856
1885
  state.errors.push({
@@ -1859,7 +1888,7 @@ function ensurePageState(page) {
1859
1888
  stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
1860
1889
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1861
1890
  });
1862
- if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
1891
+ if (state.errors.length > MAX_PAGE_ERRORS + 20) state.errors.splice(0, 20);
1863
1892
  });
1864
1893
  page.on("request", (req) => {
1865
1894
  state.nextRequestId += 1;
@@ -1872,7 +1901,7 @@ function ensurePageState(page) {
1872
1901
  url: req.url(),
1873
1902
  resourceType: req.resourceType()
1874
1903
  });
1875
- if (state.requests.length > MAX_NETWORK_REQUESTS) state.requests.shift();
1904
+ if (state.requests.length > MAX_NETWORK_REQUESTS + 50) state.requests.splice(0, 50);
1876
1905
  });
1877
1906
  page.on("response", (resp) => {
1878
1907
  const req = resp.request();
@@ -1946,31 +1975,40 @@ function setDialogHandlerOnPage(page, handler) {
1946
1975
  const state = ensurePageState(page);
1947
1976
  state.dialogHandler = handler;
1948
1977
  }
1949
- function applyStealthToPage(page) {
1950
- page.evaluate(STEALTH_SCRIPT).catch((e) => {
1978
+ async function applyStealthToPage(page) {
1979
+ try {
1980
+ await page.evaluate(STEALTH_SCRIPT);
1981
+ } catch (e) {
1951
1982
  if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1952
1983
  console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
1953
- });
1984
+ }
1954
1985
  }
1955
- function observeContext(context) {
1986
+ async function observeContext(context) {
1956
1987
  if (observedContexts.has(context)) return;
1957
1988
  observedContexts.add(context);
1958
1989
  ensureContextState(context);
1959
- context.addInitScript(STEALTH_SCRIPT).catch((e) => {
1990
+ try {
1991
+ await context.addInitScript(STEALTH_SCRIPT);
1992
+ } catch (e) {
1960
1993
  if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1961
1994
  console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
1962
- });
1995
+ }
1963
1996
  for (const page of context.pages()) {
1964
1997
  ensurePageState(page);
1965
- applyStealthToPage(page);
1998
+ await applyStealthToPage(page);
1966
1999
  }
1967
- context.on("page", (page) => {
2000
+ const onPage = (page) => {
1968
2001
  ensurePageState(page);
1969
- applyStealthToPage(page);
2002
+ applyStealthToPage(page).catch(() => {
2003
+ });
2004
+ };
2005
+ context.on("page", onPage);
2006
+ context.once("close", () => {
2007
+ context.off("page", onPage);
1970
2008
  });
1971
2009
  }
1972
- function observeBrowser(browser) {
1973
- for (const context of browser.contexts()) observeContext(context);
2010
+ async function observeBrowser(browser) {
2011
+ for (const context of browser.contexts()) await observeContext(context);
1974
2012
  }
1975
2013
  function toAIFriendlyError(error, selector) {
1976
2014
  const message = error instanceof Error ? error.message : String(error);
@@ -2005,6 +2043,7 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2005
2043
  }
2006
2044
 
2007
2045
  // src/ref-resolver.ts
2046
+ var REFS_STALENESS_THRESHOLD_MS = 3e4;
2008
2047
  var roleRefsByTarget = /* @__PURE__ */ new Map();
2009
2048
  var MAX_ROLE_REFS_CACHE = 50;
2010
2049
  function normalizeCdpUrl(raw) {
@@ -2019,7 +2058,8 @@ function rememberRoleRefsForTarget(opts) {
2019
2058
  roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
2020
2059
  refs: opts.refs,
2021
2060
  ...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
2022
- ...opts.mode !== void 0 ? { mode: opts.mode } : {}
2061
+ ...opts.mode !== void 0 ? { mode: opts.mode } : {},
2062
+ storedAt: Date.now()
2023
2063
  });
2024
2064
  while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
2025
2065
  const first = roleRefsByTarget.keys().next();
@@ -2032,6 +2072,7 @@ function storeRoleRefsForTarget(opts) {
2032
2072
  state.roleRefs = opts.refs;
2033
2073
  state.roleRefsFrameSelector = opts.frameSelector;
2034
2074
  state.roleRefsMode = opts.mode;
2075
+ state.roleRefsStoredAt = Date.now();
2035
2076
  if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
2036
2077
  rememberRoleRefsForTarget({
2037
2078
  cdpUrl: opts.cdpUrl,
@@ -2041,17 +2082,6 @@ function storeRoleRefsForTarget(opts) {
2041
2082
  mode: opts.mode
2042
2083
  });
2043
2084
  }
2044
- function restoreRoleRefsForTarget(opts) {
2045
- const targetId = opts.targetId?.trim() ?? "";
2046
- if (targetId === "") return;
2047
- const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
2048
- if (!entry) return;
2049
- const state = ensurePageState(opts.page);
2050
- if (state.roleRefs) return;
2051
- state.roleRefs = entry.refs;
2052
- state.roleRefsFrameSelector = entry.frameSelector;
2053
- state.roleRefsMode = entry.mode;
2054
- }
2055
2085
  function clearRoleRefsForCdpUrl(cdpUrl) {
2056
2086
  const normalized = normalizeCdpUrl(cdpUrl);
2057
2087
  for (const key of roleRefsByTarget.keys()) {
@@ -2090,6 +2120,14 @@ function refLocator(page, ref) {
2090
2120
  if (normalized.trim() === "") throw new Error("ref is required");
2091
2121
  if (/^e\d+$/.test(normalized)) {
2092
2122
  const state = getPageState(page);
2123
+ if (state?.roleRefsStoredAt !== void 0) {
2124
+ const ageMs = Date.now() - state.roleRefsStoredAt;
2125
+ if (ageMs > REFS_STALENESS_THRESHOLD_MS) {
2126
+ console.warn(
2127
+ `[browserclaw] refs are ${String(Math.round(ageMs / 1e3))}s old \u2014 consider re-snapshotting for fresh refs`
2128
+ );
2129
+ }
2130
+ }
2093
2131
  if (state?.roleRefsMode === "aria") {
2094
2132
  return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2095
2133
  }
@@ -2138,14 +2176,16 @@ function appendCdpPath2(cdpUrl, cdpPath) {
2138
2176
  }
2139
2177
  async function withPlaywrightPageCdpSession(page, fn) {
2140
2178
  const CDP_SESSION_TIMEOUT_MS = 1e4;
2179
+ let timer;
2141
2180
  const session = await Promise.race([
2142
2181
  page.context().newCDPSession(page),
2143
2182
  new Promise((_, reject) => {
2144
- setTimeout(() => {
2183
+ timer = setTimeout(() => {
2145
2184
  reject(new Error("newCDPSession timed out after 10s"));
2146
2185
  }, CDP_SESSION_TIMEOUT_MS);
2147
2186
  })
2148
2187
  ]);
2188
+ clearTimeout(timer);
2149
2189
  try {
2150
2190
  return await fn(session);
2151
2191
  } finally {
@@ -2171,8 +2211,10 @@ function isLoopbackCdpUrl(url) {
2171
2211
  }
2172
2212
  }
2173
2213
  var envMutexPromise = Promise.resolve();
2214
+ var envMutexDepth = 0;
2174
2215
  async function withNoProxyForCdpUrl(url, fn) {
2175
2216
  if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return fn();
2217
+ if (envMutexDepth > 0) return fn();
2176
2218
  const prev = envMutexPromise;
2177
2219
  let release = () => {
2178
2220
  };
@@ -2193,9 +2235,11 @@ async function withNoProxyForCdpUrl(url, fn) {
2193
2235
  const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
2194
2236
  process.env.NO_PROXY = applied;
2195
2237
  process.env.no_proxy = applied;
2238
+ envMutexDepth += 1;
2196
2239
  try {
2197
2240
  return await fn();
2198
2241
  } finally {
2242
+ envMutexDepth -= 1;
2199
2243
  if (process.env.NO_PROXY === applied) {
2200
2244
  if (savedNoProxy !== void 0) process.env.NO_PROXY = savedNoProxy;
2201
2245
  else delete process.env.NO_PROXY;
@@ -2226,12 +2270,28 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
2226
2270
  }
2227
2271
  var cachedByCdpUrl = /* @__PURE__ */ new Map();
2228
2272
  var connectingByCdpUrl = /* @__PURE__ */ new Map();
2273
+ var connectionMutex = Promise.resolve();
2274
+ async function withConnectionLock(fn) {
2275
+ const prev = connectionMutex;
2276
+ let release = () => {
2277
+ };
2278
+ connectionMutex = new Promise((r) => {
2279
+ release = r;
2280
+ });
2281
+ await prev;
2282
+ try {
2283
+ return await fn();
2284
+ } finally {
2285
+ release();
2286
+ }
2287
+ }
2229
2288
  var BlockedBrowserTargetError = class extends Error {
2230
2289
  constructor() {
2231
2290
  super("Browser target is unavailable after SSRF policy blocked its navigation.");
2232
2291
  this.name = "BlockedBrowserTargetError";
2233
2292
  }
2234
2293
  };
2294
+ var MAX_BLOCKED_TARGETS = 200;
2235
2295
  var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
2236
2296
  var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
2237
2297
  function blockedTargetKey(cdpUrl, targetId) {
@@ -2246,6 +2306,10 @@ function markTargetBlocked(cdpUrl, targetId) {
2246
2306
  const normalized = targetId?.trim() ?? "";
2247
2307
  if (normalized === "") return;
2248
2308
  blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
2309
+ if (blockedTargetsByCdpUrl.size > MAX_BLOCKED_TARGETS) {
2310
+ const first = blockedTargetsByCdpUrl.values().next();
2311
+ if (first.done !== true) blockedTargetsByCdpUrl.delete(first.value);
2312
+ }
2249
2313
  }
2250
2314
  function clearBlockedTarget(cdpUrl, targetId) {
2251
2315
  const normalized = targetId?.trim() ?? "";
@@ -2259,6 +2323,16 @@ function hasBlockedTargetsForCdpUrl(cdpUrl) {
2259
2323
  }
2260
2324
  return false;
2261
2325
  }
2326
+ function clearBlockedTargetsForCdpUrl(cdpUrl) {
2327
+ if (cdpUrl === void 0) {
2328
+ blockedTargetsByCdpUrl.clear();
2329
+ return;
2330
+ }
2331
+ const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
2332
+ for (const key of blockedTargetsByCdpUrl) {
2333
+ if (key.startsWith(prefix)) blockedTargetsByCdpUrl.delete(key);
2334
+ }
2335
+ }
2262
2336
  function blockedPageRefsForCdpUrl(cdpUrl) {
2263
2337
  const normalized = normalizeCdpUrl(cdpUrl);
2264
2338
  const existing = blockedPageRefsByCdpUrl.get(normalized);
@@ -2273,6 +2347,13 @@ function isBlockedPageRef(cdpUrl, page) {
2273
2347
  function markPageRefBlocked(cdpUrl, page) {
2274
2348
  blockedPageRefsForCdpUrl(cdpUrl).add(page);
2275
2349
  }
2350
+ function clearBlockedPageRefsForCdpUrl(cdpUrl) {
2351
+ if (cdpUrl === void 0) {
2352
+ blockedPageRefsByCdpUrl.clear();
2353
+ return;
2354
+ }
2355
+ blockedPageRefsByCdpUrl.delete(normalizeCdpUrl(cdpUrl));
2356
+ }
2276
2357
  function clearBlockedPageRef(cdpUrl, page) {
2277
2358
  blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
2278
2359
  }
@@ -2286,64 +2367,98 @@ async function connectBrowser(cdpUrl, authToken) {
2286
2367
  if (existing_cached) return existing_cached;
2287
2368
  const existing = connectingByCdpUrl.get(normalized);
2288
2369
  if (existing) return await existing;
2289
- const connectWithRetry = async () => {
2290
- let lastErr;
2291
- for (let attempt = 0; attempt < 3; attempt++) {
2292
- try {
2293
- const timeout = 5e3 + attempt * 2e3;
2294
- const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
2295
- const headers = getHeadersWithAuth(endpoint);
2296
- if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
2297
- headers.Authorization = `Bearer ${authToken}`;
2298
- const browser = await withNoProxyForCdpUrl(
2299
- endpoint,
2300
- () => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers })
2301
- );
2302
- const onDisconnected = () => {
2303
- if (cachedByCdpUrl.get(normalized)?.browser === browser) {
2304
- cachedByCdpUrl.delete(normalized);
2305
- clearRoleRefsForCdpUrl(normalized);
2370
+ return withConnectionLock(async () => {
2371
+ const rechecked = cachedByCdpUrl.get(normalized);
2372
+ if (rechecked) return rechecked;
2373
+ const recheckPending = connectingByCdpUrl.get(normalized);
2374
+ if (recheckPending) return await recheckPending;
2375
+ const connectWithRetry = async () => {
2376
+ let lastErr;
2377
+ for (let attempt = 0; attempt < 3; attempt++) {
2378
+ try {
2379
+ const timeout = 5e3 + attempt * 2e3;
2380
+ const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
2381
+ const headers = getHeadersWithAuth(endpoint);
2382
+ if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
2383
+ headers.Authorization = `Bearer ${authToken}`;
2384
+ const browser = await withNoProxyForCdpUrl(
2385
+ endpoint,
2386
+ () => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers })
2387
+ );
2388
+ const onDisconnected = () => {
2389
+ if (cachedByCdpUrl.get(normalized)?.browser === browser) {
2390
+ cachedByCdpUrl.delete(normalized);
2391
+ clearRoleRefsForCdpUrl(normalized);
2392
+ }
2393
+ };
2394
+ const connected = { browser, cdpUrl: normalized, onDisconnected };
2395
+ cachedByCdpUrl.set(normalized, connected);
2396
+ await observeBrowser(browser);
2397
+ browser.on("disconnected", onDisconnected);
2398
+ return connected;
2399
+ } catch (err) {
2400
+ lastErr = err;
2401
+ if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) {
2402
+ await new Promise((r) => setTimeout(r, 1e3 + attempt * 1e3));
2403
+ continue;
2306
2404
  }
2307
- };
2308
- const connected = { browser, cdpUrl: normalized, onDisconnected };
2309
- cachedByCdpUrl.set(normalized, connected);
2310
- observeBrowser(browser);
2311
- browser.on("disconnected", onDisconnected);
2312
- return connected;
2313
- } catch (err) {
2314
- lastErr = err;
2315
- if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) break;
2316
- await new Promise((r) => setTimeout(r, 250 + attempt * 250));
2405
+ await new Promise((r) => setTimeout(r, 250 + attempt * 250));
2406
+ }
2317
2407
  }
2318
- }
2319
- throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
2320
- };
2321
- const promise = connectWithRetry().finally(() => {
2322
- connectingByCdpUrl.delete(normalized);
2408
+ throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
2409
+ };
2410
+ const promise = connectWithRetry().finally(() => {
2411
+ connectingByCdpUrl.delete(normalized);
2412
+ });
2413
+ connectingByCdpUrl.set(normalized, promise);
2414
+ return await promise;
2323
2415
  });
2324
- connectingByCdpUrl.set(normalized, promise);
2325
- return await promise;
2326
2416
  }
2327
2417
  async function disconnectBrowser() {
2328
- if (connectingByCdpUrl.size) {
2329
- for (const p of connectingByCdpUrl.values()) {
2330
- try {
2331
- await p;
2332
- } catch (err) {
2333
- console.warn(
2334
- `[browserclaw] disconnectBrowser: pending connect failed: ${err instanceof Error ? err.message : String(err)}`
2335
- );
2418
+ return withConnectionLock(async () => {
2419
+ if (connectingByCdpUrl.size) {
2420
+ for (const p of connectingByCdpUrl.values()) {
2421
+ try {
2422
+ await p;
2423
+ } catch (err) {
2424
+ console.warn(
2425
+ `[browserclaw] disconnectBrowser: pending connect failed: ${err instanceof Error ? err.message : String(err)}`
2426
+ );
2427
+ }
2336
2428
  }
2337
2429
  }
2338
- }
2339
- for (const cur of cachedByCdpUrl.values()) {
2340
- clearRoleRefsForCdpUrl(cur.cdpUrl);
2341
- if (cur.onDisconnected && typeof cur.browser.off === "function")
2342
- cur.browser.off("disconnected", cur.onDisconnected);
2343
- await cur.browser.close().catch(() => {
2430
+ for (const cur of cachedByCdpUrl.values()) {
2431
+ clearRoleRefsForCdpUrl(cur.cdpUrl);
2432
+ if (cur.onDisconnected && typeof cur.browser.off === "function")
2433
+ cur.browser.off("disconnected", cur.onDisconnected);
2434
+ await cur.browser.close().catch(() => {
2435
+ });
2436
+ }
2437
+ cachedByCdpUrl.clear();
2438
+ clearBlockedTargetsForCdpUrl();
2439
+ clearBlockedPageRefsForCdpUrl();
2440
+ });
2441
+ }
2442
+ async function closePlaywrightBrowserConnection(opts) {
2443
+ if (opts?.cdpUrl !== void 0 && opts.cdpUrl !== "") {
2444
+ return withConnectionLock(async () => {
2445
+ const cdpUrl = opts.cdpUrl;
2446
+ if (cdpUrl === void 0 || cdpUrl === "") return;
2447
+ const normalized = normalizeCdpUrl(cdpUrl);
2448
+ clearBlockedTargetsForCdpUrl(normalized);
2449
+ clearBlockedPageRefsForCdpUrl(normalized);
2450
+ const cur = cachedByCdpUrl.get(normalized);
2451
+ cachedByCdpUrl.delete(normalized);
2452
+ connectingByCdpUrl.delete(normalized);
2453
+ if (!cur) return;
2454
+ if (cur.onDisconnected && typeof cur.browser.off === "function")
2455
+ cur.browser.off("disconnected", cur.onDisconnected);
2456
+ await cur.browser.close().catch(() => {
2457
+ });
2344
2458
  });
2459
+ } else {
2460
+ await disconnectBrowser();
2345
2461
  }
2346
- cachedByCdpUrl.clear();
2347
2462
  }
2348
2463
  function cdpSocketNeedsAttach(wsUrl) {
2349
2464
  try {
@@ -2452,7 +2567,7 @@ async function forceDisconnectPlaywrightConnection(opts) {
2452
2567
  await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
2453
2568
  });
2454
2569
  }
2455
- cur.browser.close().catch(() => {
2570
+ await cur.browser.close().catch(() => {
2456
2571
  });
2457
2572
  }
2458
2573
  var forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightConnection;
@@ -2463,17 +2578,13 @@ var pageTargetIdCache = /* @__PURE__ */ new WeakMap();
2463
2578
  async function pageTargetId(page) {
2464
2579
  const cached = pageTargetIdCache.get(page);
2465
2580
  if (cached !== void 0) return cached;
2466
- const session = await page.context().newCDPSession(page);
2467
- try {
2581
+ return withPlaywrightPageCdpSession(page, async (session) => {
2468
2582
  const info = await session.send("Target.getTargetInfo");
2469
2583
  const targetInfo = info.targetInfo;
2470
2584
  const id = (targetInfo?.targetId ?? "").trim() || null;
2471
2585
  if (id !== null) pageTargetIdCache.set(page, id);
2472
2586
  return id;
2473
- } finally {
2474
- await session.detach().catch(() => {
2475
- });
2476
- }
2587
+ });
2477
2588
  }
2478
2589
  function matchPageByTargetList(pages, targets, targetId) {
2479
2590
  const target = targets.find((entry) => entry.id === targetId);
@@ -2509,7 +2620,6 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
2509
2620
  }
2510
2621
  })
2511
2622
  );
2512
- const resolvedViaCdp = results.some(({ tid }) => tid !== null);
2513
2623
  const matched = results.find(({ tid }) => tid !== null && tid !== "" && tid === targetId);
2514
2624
  if (matched) return matched.page;
2515
2625
  if (cdpUrl !== void 0 && cdpUrl !== "") {
@@ -2518,7 +2628,6 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
2518
2628
  } catch {
2519
2629
  }
2520
2630
  }
2521
- if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
2522
2631
  return null;
2523
2632
  }
2524
2633
  async function partitionAccessiblePages(opts) {
@@ -2561,7 +2670,6 @@ async function getPageForTargetId(opts) {
2561
2670
  if (opts.targetId === void 0 || opts.targetId === "") return first;
2562
2671
  const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
2563
2672
  if (!found) {
2564
- if (pages.length === 1) return first;
2565
2673
  throw new BrowserTabNotFoundError(
2566
2674
  `Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
2567
2675
  );
@@ -2578,7 +2686,6 @@ async function resolvePageByTargetIdOrThrow(opts) {
2578
2686
  async function getRestoredPageForTarget(opts) {
2579
2687
  const page = await getPageForTargetId(opts);
2580
2688
  ensurePageState(page);
2581
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2582
2689
  return page;
2583
2690
  }
2584
2691
 
@@ -2632,10 +2739,11 @@ var BROWSER_EVALUATOR = new Function(
2632
2739
  " catch (_) { candidate = (0, eval)(fnBody); }",
2633
2740
  ' var result = typeof candidate === "function" ? candidate() : candidate;',
2634
2741
  ' if (result && typeof result.then === "function") {',
2742
+ " var tid;",
2635
2743
  " return Promise.race([",
2636
- " result,",
2744
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2637
2745
  " new Promise(function(_, reject) {",
2638
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2746
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2639
2747
  " })",
2640
2748
  " ]);",
2641
2749
  " }",
@@ -2657,10 +2765,11 @@ var ELEMENT_EVALUATOR = new Function(
2657
2765
  " catch (_) { candidate = (0, eval)(fnBody); }",
2658
2766
  ' var result = typeof candidate === "function" ? candidate(el) : candidate;',
2659
2767
  ' if (result && typeof result.then === "function") {',
2768
+ " var tid;",
2660
2769
  " return Promise.race([",
2661
- " result,",
2770
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2662
2771
  " new Promise(function(_, reject) {",
2663
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2772
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2664
2773
  " })",
2665
2774
  " ]);",
2666
2775
  " }",
@@ -2675,10 +2784,8 @@ async function evaluateViaPlaywright(opts) {
2675
2784
  if (!fnText) throw new Error("function is required");
2676
2785
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2677
2786
  ensurePageState(page);
2678
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2679
2787
  const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2680
- let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
2681
- evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
2788
+ const evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 1e3));
2682
2789
  const signal = opts.signal;
2683
2790
  let abortListener;
2684
2791
  let abortReject;
@@ -2692,10 +2799,16 @@ async function evaluateViaPlaywright(opts) {
2692
2799
  }
2693
2800
  if (signal !== void 0) {
2694
2801
  const disconnect = () => {
2695
- forceDisconnectPlaywrightConnection({
2696
- cdpUrl: opts.cdpUrl,
2697
- targetId: opts.targetId}).catch(() => {
2698
- });
2802
+ const targetId = opts.targetId?.trim() ?? "";
2803
+ if (targetId !== "") {
2804
+ tryTerminateExecutionViaCdp(opts.cdpUrl, targetId).catch(() => {
2805
+ });
2806
+ } else {
2807
+ console.warn("[browserclaw] evaluate abort: no targetId, forcing full disconnect");
2808
+ forceDisconnectPlaywrightConnection({
2809
+ cdpUrl: opts.cdpUrl}).catch(() => {
2810
+ });
2811
+ }
2699
2812
  };
2700
2813
  if (signal.aborted) {
2701
2814
  disconnect();
@@ -2731,6 +2844,8 @@ async function evaluateViaPlaywright(opts) {
2731
2844
  );
2732
2845
  } finally {
2733
2846
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
2847
+ abortReject = void 0;
2848
+ abortListener = void 0;
2734
2849
  }
2735
2850
  }
2736
2851
 
@@ -2923,7 +3038,7 @@ function isBlockedHostnameOrIp(hostname, policy) {
2923
3038
  function isPrivateIpAddress(address, policy) {
2924
3039
  let normalized = address.trim().toLowerCase();
2925
3040
  if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
2926
- if (!normalized) return false;
3041
+ if (!normalized) return true;
2927
3042
  const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
2928
3043
  const strictIp = parseCanonicalIpAddress(normalized);
2929
3044
  if (strictIp) {
@@ -3010,6 +3125,25 @@ function createPinnedLookup(params) {
3010
3125
  cb(null, chosen.address, chosen.family);
3011
3126
  });
3012
3127
  }
3128
+ var DNS_CACHE_TTL_MS = 3e4;
3129
+ var MAX_DNS_CACHE_SIZE = 100;
3130
+ var dnsCache = /* @__PURE__ */ new Map();
3131
+ function getCachedDnsResult(hostname) {
3132
+ const entry = dnsCache.get(hostname);
3133
+ if (!entry) return void 0;
3134
+ if (Date.now() > entry.expiresAt) {
3135
+ dnsCache.delete(hostname);
3136
+ return void 0;
3137
+ }
3138
+ return entry.result;
3139
+ }
3140
+ function cacheDnsResult(hostname, result) {
3141
+ dnsCache.set(hostname, { result, expiresAt: Date.now() + DNS_CACHE_TTL_MS });
3142
+ if (dnsCache.size > MAX_DNS_CACHE_SIZE) {
3143
+ const first = dnsCache.keys().next();
3144
+ if (first.done !== true) dnsCache.delete(first.value);
3145
+ }
3146
+ }
3013
3147
  async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3014
3148
  const normalized = normalizeHostname(hostname);
3015
3149
  if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
@@ -3028,6 +3162,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3028
3162
  );
3029
3163
  }
3030
3164
  }
3165
+ const cached = getCachedDnsResult(normalized);
3166
+ if (cached) return cached;
3031
3167
  const lookupFn = params.lookupFn ?? promises.lookup;
3032
3168
  let results;
3033
3169
  try {
@@ -3057,11 +3193,13 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3057
3193
  `Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
3058
3194
  );
3059
3195
  }
3060
- return {
3196
+ const pinned = {
3061
3197
  hostname: normalized,
3062
3198
  addresses,
3063
3199
  lookup: createPinnedLookup({ hostname: normalized, addresses })
3064
3200
  };
3201
+ cacheDnsResult(normalized, pinned);
3202
+ return pinned;
3065
3203
  }
3066
3204
  async function assertBrowserNavigationAllowed(opts) {
3067
3205
  const rawUrl = opts.url.trim();
@@ -3215,13 +3353,27 @@ function buildSiblingTempPath(targetPath) {
3215
3353
  return path.join(path.dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
3216
3354
  }
3217
3355
  async function writeViaSiblingTempPath(params) {
3218
- const rootDir = await promises$1.realpath(path.resolve(params.rootDir)).catch(() => path.resolve(params.rootDir));
3356
+ let rootDir;
3357
+ try {
3358
+ rootDir = await promises$1.realpath(path.resolve(params.rootDir));
3359
+ } catch {
3360
+ console.warn(`[browserclaw] writeViaSiblingTempPath: rootDir realpath failed, using lexical resolve`);
3361
+ rootDir = path.resolve(params.rootDir);
3362
+ }
3219
3363
  const requestedTargetPath = path.resolve(params.targetPath);
3220
3364
  const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
3221
3365
  const relativeTargetPath = path.relative(rootDir, targetPath);
3222
3366
  if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTargetPath)) {
3223
3367
  throw new Error("Target path is outside the allowed root");
3224
3368
  }
3369
+ try {
3370
+ const stat = await promises$1.lstat(targetPath);
3371
+ if (stat.isSymbolicLink()) {
3372
+ throw new Error(`Unsafe output path: "${params.targetPath}" is a symbolic link.`);
3373
+ }
3374
+ } catch (e) {
3375
+ if (e.code !== "ENOENT") throw e;
3376
+ }
3225
3377
  const tempPath = buildSiblingTempPath(targetPath);
3226
3378
  let renameSucceeded = false;
3227
3379
  try {
@@ -3243,6 +3395,9 @@ async function assertBrowserNavigationResultAllowed(opts) {
3243
3395
  } catch {
3244
3396
  return;
3245
3397
  }
3398
+ if (parsed.protocol === "data:" || parsed.protocol === "blob:") {
3399
+ throw new InvalidBrowserNavigationUrlError(`Navigation result blocked: "${parsed.protocol}" URLs are not allowed.`);
3400
+ }
3246
3401
  if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) {
3247
3402
  await assertBrowserNavigationAllowed(opts);
3248
3403
  }
@@ -3360,9 +3515,10 @@ async function clickViaPlaywright(opts) {
3360
3515
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3361
3516
  const POLL_INTERVAL_MS = 50;
3362
3517
  const POLL_TIMEOUT_MS = 500;
3518
+ const ATTR_TIMEOUT_MS = Math.min(timeout, POLL_TIMEOUT_MS);
3363
3519
  let changed = false;
3364
3520
  for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
3365
- const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3521
+ const current = await locator.getAttribute("aria-checked", { timeout: ATTR_TIMEOUT_MS }).catch(() => void 0);
3366
3522
  if (current === void 0 || current !== ariaCheckedBefore) {
3367
3523
  changed = true;
3368
3524
  break;
@@ -3439,6 +3595,7 @@ async function dragViaPlaywright(opts) {
3439
3595
  async function fillFormViaPlaywright(opts) {
3440
3596
  const page = await getRestoredPageForTarget(opts);
3441
3597
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3598
+ let filledCount = 0;
3442
3599
  for (const field of opts.fields) {
3443
3600
  const ref = field.ref.trim();
3444
3601
  const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
@@ -3457,16 +3614,24 @@ async function fillFormViaPlaywright(opts) {
3457
3614
  try {
3458
3615
  await setCheckedViaEvaluate(locator, checked);
3459
3616
  } catch (err) {
3460
- throw toAIFriendlyError(err, ref);
3617
+ const friendly = toAIFriendlyError(err, ref);
3618
+ throw new Error(
3619
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3620
+ );
3461
3621
  }
3462
3622
  }
3623
+ filledCount += 1;
3463
3624
  continue;
3464
3625
  }
3465
3626
  try {
3466
3627
  await locator.fill(value, { timeout });
3467
3628
  } catch (err) {
3468
- throw toAIFriendlyError(err, ref);
3629
+ const friendly = toAIFriendlyError(err, ref);
3630
+ throw new Error(
3631
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3632
+ );
3469
3633
  }
3634
+ filledCount += 1;
3470
3635
  }
3471
3636
  }
3472
3637
  async function scrollIntoViewViaPlaywright(opts) {
@@ -3532,16 +3697,22 @@ async function armDialogViaPlaywright(opts) {
3532
3697
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3533
3698
  state.armIdDialog = bumpDialogArmId(state);
3534
3699
  const armId = state.armIdDialog;
3700
+ const resetArm = () => {
3701
+ if (state.armIdDialog === armId) state.armIdDialog = 0;
3702
+ };
3703
+ page.once("close", resetArm);
3535
3704
  page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
3536
3705
  if (state.armIdDialog !== armId) return;
3537
3706
  try {
3538
3707
  if (opts.accept) await dialog.accept(opts.promptText);
3539
3708
  else await dialog.dismiss();
3540
3709
  } finally {
3541
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3710
+ resetArm();
3711
+ page.off("close", resetArm);
3542
3712
  }
3543
3713
  }).catch(() => {
3544
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3714
+ resetArm();
3715
+ page.off("close", resetArm);
3545
3716
  });
3546
3717
  }
3547
3718
  async function armFileUploadViaPlaywright(opts) {
@@ -3550,6 +3721,10 @@ async function armFileUploadViaPlaywright(opts) {
3550
3721
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3551
3722
  state.armIdUpload = bumpUploadArmId(state);
3552
3723
  const armId = state.armIdUpload;
3724
+ const resetArm = () => {
3725
+ if (state.armIdUpload === armId) state.armIdUpload = 0;
3726
+ };
3727
+ page.once("close", resetArm);
3553
3728
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3554
3729
  if (state.armIdUpload !== armId) return;
3555
3730
  if (opts.paths === void 0 || opts.paths.length === 0) {
@@ -3581,9 +3756,18 @@ async function armFileUploadViaPlaywright(opts) {
3581
3756
  el.dispatchEvent(new Event("change", { bubbles: true }));
3582
3757
  });
3583
3758
  }
3584
- } catch {
3759
+ } catch (e) {
3760
+ console.warn(
3761
+ `[browserclaw] armFileUpload: dispatch events failed: ${e instanceof Error ? e.message : String(e)}`
3762
+ );
3585
3763
  }
3586
- }).catch(() => {
3764
+ }).catch((e) => {
3765
+ console.warn(
3766
+ `[browserclaw] armFileUpload: filechooser wait failed: ${e instanceof Error ? e.message : String(e)}`
3767
+ );
3768
+ }).finally(() => {
3769
+ resetArm();
3770
+ page.off("close", resetArm);
3587
3771
  });
3588
3772
  }
3589
3773
 
@@ -3603,7 +3787,7 @@ function clearRecordingContext(cdpUrl) {
3603
3787
  }
3604
3788
  async function createRecordingContext(browser, cdpUrl, recordVideo) {
3605
3789
  const context = await browser.newContext({ recordVideo });
3606
- observeContext(context);
3790
+ await observeContext(context);
3607
3791
  recordingContexts.set(cdpUrl, context);
3608
3792
  context.on("close", () => recordingContexts.delete(cdpUrl));
3609
3793
  return context;
@@ -3658,6 +3842,11 @@ async function gotoPageWithNavigationGuard(opts) {
3658
3842
  await route.continue();
3659
3843
  return;
3660
3844
  }
3845
+ const isRedirect = request.redirectedFrom() !== null;
3846
+ if (!isRedirect && request.url() !== opts.url) {
3847
+ await route.continue();
3848
+ return;
3849
+ }
3661
3850
  try {
3662
3851
  await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
3663
3852
  } catch (err) {
@@ -3709,6 +3898,7 @@ async function navigateViaPlaywright(opts) {
3709
3898
  response = await navigate();
3710
3899
  } catch (err) {
3711
3900
  if (!isRetryableNavigateError(err)) throw err;
3901
+ recordingContexts.delete(opts.cdpUrl);
3712
3902
  await forceDisconnectPlaywrightConnection({
3713
3903
  cdpUrl: opts.cdpUrl,
3714
3904
  targetId: opts.targetId}).catch(() => {
@@ -3768,6 +3958,7 @@ async function createPageViaPlaywright(opts) {
3768
3958
  });
3769
3959
  } catch (err) {
3770
3960
  if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
3961
+ console.warn(`[browserclaw] createPage navigation failed: ${err instanceof Error ? err.message : String(err)}`);
3771
3962
  }
3772
3963
  await assertPageNavigationCompletedSafely({
3773
3964
  cdpUrl: opts.cdpUrl,
@@ -3854,39 +4045,46 @@ var MAX_WAIT_TIME_MS = 3e4;
3854
4045
  async function waitForViaPlaywright(opts) {
3855
4046
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3856
4047
  ensurePageState(page);
3857
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4048
+ const totalTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4049
+ const deadline = Date.now() + totalTimeout;
4050
+ const remaining = () => Math.max(500, deadline - Date.now());
3858
4051
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3859
4052
  await page.waitForTimeout(resolveBoundedDelayMs(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3860
4053
  }
3861
4054
  if (opts.text !== void 0 && opts.text !== "") {
3862
- await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
4055
+ await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, {
4056
+ timeout: remaining()
4057
+ });
3863
4058
  }
3864
4059
  if (opts.textGone !== void 0 && opts.textGone !== "") {
3865
- await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, { timeout });
4060
+ await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, {
4061
+ timeout: remaining()
4062
+ });
3866
4063
  }
3867
4064
  if (opts.selector !== void 0 && opts.selector !== "") {
3868
4065
  const selector = opts.selector.trim();
3869
- if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
4066
+ if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout: remaining() });
3870
4067
  }
3871
4068
  if (opts.url !== void 0 && opts.url !== "") {
3872
4069
  const url = opts.url.trim();
3873
- if (url !== "") await page.waitForURL(url, { timeout });
4070
+ if (url !== "") await page.waitForURL(url, { timeout: remaining() });
3874
4071
  }
3875
4072
  if (opts.loadState !== void 0) {
3876
- await page.waitForLoadState(opts.loadState, { timeout });
4073
+ await page.waitForLoadState(opts.loadState, { timeout: remaining() });
3877
4074
  }
3878
4075
  if (opts.fn !== void 0) {
3879
4076
  if (typeof opts.fn === "function") {
3880
- await page.waitForFunction(opts.fn, opts.arg, { timeout });
4077
+ await page.waitForFunction(opts.fn, opts.arg, { timeout: remaining() });
3881
4078
  } else {
3882
4079
  const fn = opts.fn.trim();
3883
- if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
4080
+ if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout: remaining() });
3884
4081
  }
3885
4082
  }
3886
4083
  }
3887
4084
 
3888
4085
  // src/actions/batch.ts
3889
4086
  var MAX_BATCH_DEPTH = 5;
4087
+ var MAX_BATCH_TIMEOUT_MS = 3e5;
3890
4088
  var MAX_BATCH_ACTIONS = 100;
3891
4089
  async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
3892
4090
  if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
@@ -4034,13 +4232,19 @@ async function batchViaPlaywright(opts) {
4034
4232
  throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
4035
4233
  const results = [];
4036
4234
  const evaluateEnabled = opts.evaluateEnabled !== false;
4235
+ const deadline = Date.now() + MAX_BATCH_TIMEOUT_MS;
4037
4236
  for (const action of opts.actions) {
4237
+ if (Date.now() > deadline) {
4238
+ results.push({ ok: false, error: "Batch timeout exceeded" });
4239
+ break;
4240
+ }
4038
4241
  try {
4039
4242
  await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
4040
4243
  results.push({ ok: true });
4041
4244
  } catch (err) {
4042
4245
  const message = err instanceof Error ? err.message : String(err);
4043
4246
  results.push({ ok: false, error: message });
4247
+ if (err instanceof BrowserTabNotFoundError || err instanceof BlockedBrowserTargetError) break;
4044
4248
  if (opts.stopOnError !== false) break;
4045
4249
  }
4046
4250
  }
@@ -4058,8 +4262,10 @@ function createPageDownloadWaiter(page, timeoutMs) {
4058
4262
  handler = void 0;
4059
4263
  }
4060
4264
  };
4265
+ let rejectPromise;
4061
4266
  return {
4062
4267
  promise: new Promise((resolve2, reject) => {
4268
+ rejectPromise = reject;
4063
4269
  handler = (download) => {
4064
4270
  if (done) return;
4065
4271
  done = true;
@@ -4078,6 +4284,7 @@ function createPageDownloadWaiter(page, timeoutMs) {
4078
4284
  if (done) return;
4079
4285
  done = true;
4080
4286
  cleanup();
4287
+ rejectPromise?.(new Error("Download waiter cancelled"));
4081
4288
  }
4082
4289
  };
4083
4290
  }
@@ -4109,12 +4316,11 @@ async function downloadViaPlaywright(opts) {
4109
4316
  await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
4110
4317
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4111
4318
  const state = ensurePageState(page);
4112
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4113
4319
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4114
4320
  const outPath = opts.path.trim();
4115
4321
  if (!outPath) throw new Error("path is required");
4116
- state.armIdDownload = bumpDownloadArmId(state);
4117
- const armId = state.armIdDownload;
4322
+ const armId = bumpDownloadArmId(state);
4323
+ state.armIdDownload = armId;
4118
4324
  const waiter = createPageDownloadWaiter(page, timeout);
4119
4325
  try {
4120
4326
  const locator = refLocator(page, opts.ref);
@@ -4161,12 +4367,6 @@ async function setDeviceViaPlaywright(opts) {
4161
4367
  }
4162
4368
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4163
4369
  ensurePageState(page);
4164
- if (device.viewport !== null) {
4165
- await page.setViewportSize({
4166
- width: device.viewport.width,
4167
- height: device.viewport.height
4168
- });
4169
- }
4170
4370
  await withPageScopedCdpClient({
4171
4371
  cdpUrl: opts.cdpUrl,
4172
4372
  page,
@@ -4194,11 +4394,24 @@ async function setDeviceViaPlaywright(opts) {
4194
4394
  }
4195
4395
  }
4196
4396
  });
4397
+ if (device.viewport !== null) {
4398
+ await page.setViewportSize({
4399
+ width: device.viewport.width,
4400
+ height: device.viewport.height
4401
+ });
4402
+ }
4197
4403
  }
4198
4404
  async function setExtraHTTPHeadersViaPlaywright(opts) {
4199
4405
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4200
4406
  ensurePageState(page);
4201
- await page.context().setExtraHTTPHeaders(opts.headers);
4407
+ await withPageScopedCdpClient({
4408
+ cdpUrl: opts.cdpUrl,
4409
+ page,
4410
+ targetId: opts.targetId,
4411
+ fn: async (send) => {
4412
+ await send("Network.setExtraHTTPHeaders", { headers: opts.headers });
4413
+ }
4414
+ });
4202
4415
  }
4203
4416
  async function setGeolocationViaPlaywright(opts) {
4204
4417
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -4206,7 +4419,8 @@ async function setGeolocationViaPlaywright(opts) {
4206
4419
  const context = page.context();
4207
4420
  if (opts.clear === true) {
4208
4421
  await context.setGeolocation(null);
4209
- await context.clearPermissions().catch(() => {
4422
+ await context.clearPermissions().catch((err) => {
4423
+ console.warn(`[browserclaw] clearPermissions failed: ${err instanceof Error ? err.message : String(err)}`);
4210
4424
  });
4211
4425
  return;
4212
4426
  }
@@ -4508,7 +4722,6 @@ async function waitForRequestViaPlaywright(opts) {
4508
4722
  async function takeScreenshotViaPlaywright(opts) {
4509
4723
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4510
4724
  ensurePageState(page);
4511
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4512
4725
  const type = opts.type ?? "png";
4513
4726
  if (opts.ref !== void 0 && opts.ref !== "") {
4514
4727
  if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
@@ -4523,7 +4736,6 @@ async function takeScreenshotViaPlaywright(opts) {
4523
4736
  async function screenshotWithLabelsViaPlaywright(opts) {
4524
4737
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4525
4738
  ensurePageState(page);
4526
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4527
4739
  const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
4528
4740
  const type = opts.type ?? "png";
4529
4741
  const refs = opts.refs.slice(0, maxLabels);
@@ -4587,7 +4799,8 @@ async function screenshotWithLabelsViaPlaywright(opts) {
4587
4799
  document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
4588
4800
  el.remove();
4589
4801
  });
4590
- }).catch(() => {
4802
+ }).catch((err) => {
4803
+ console.warn(`[browserclaw] label overlay cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
4591
4804
  });
4592
4805
  }
4593
4806
  }
@@ -4615,17 +4828,14 @@ async function traceStopViaPlaywright(opts) {
4615
4828
  if (!ctxState.traceActive) {
4616
4829
  throw new Error("No active trace. Start a trace before stopping it.");
4617
4830
  }
4618
- try {
4619
- await writeViaSiblingTempPath({
4620
- rootDir: path.dirname(opts.path),
4621
- targetPath: opts.path,
4622
- writeTemp: async (tempPath) => {
4623
- await context.tracing.stop({ path: tempPath });
4624
- }
4625
- });
4626
- } finally {
4627
- ctxState.traceActive = false;
4628
- }
4831
+ ctxState.traceActive = false;
4832
+ await writeViaSiblingTempPath({
4833
+ rootDir: path.dirname(opts.path),
4834
+ targetPath: opts.path,
4835
+ writeTemp: async (tempPath) => {
4836
+ await context.tracing.stop({ path: tempPath });
4837
+ }
4838
+ });
4629
4839
  }
4630
4840
 
4631
4841
  // src/snapshot/ref-map.ts
@@ -5186,17 +5396,27 @@ async function storageClearViaPlaywright(opts) {
5186
5396
  // src/browser.ts
5187
5397
  var CrawlPage = class {
5188
5398
  cdpUrl;
5189
- targetId;
5399
+ _targetId;
5190
5400
  ssrfPolicy;
5191
5401
  /** @internal */
5192
5402
  constructor(cdpUrl, targetId, ssrfPolicy) {
5193
5403
  this.cdpUrl = cdpUrl;
5194
- this.targetId = targetId;
5404
+ this._targetId = targetId;
5195
5405
  this.ssrfPolicy = ssrfPolicy;
5196
5406
  }
5197
5407
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
5198
5408
  get id() {
5199
- return this.targetId;
5409
+ return this._targetId;
5410
+ }
5411
+ /**
5412
+ * Refresh the target ID by re-resolving the page from the browser.
5413
+ * Useful after reconnection when the old target ID may be stale.
5414
+ */
5415
+ async refreshTargetId() {
5416
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5417
+ const newId = await pageTargetId(page);
5418
+ if (newId !== null && newId !== "") this._targetId = newId;
5419
+ return this._targetId;
5200
5420
  }
5201
5421
  // ── Snapshot ──────────────────────────────────────────────────
5202
5422
  /**
@@ -5224,7 +5444,7 @@ var CrawlPage = class {
5224
5444
  if (opts?.mode === "role") {
5225
5445
  return snapshotRole({
5226
5446
  cdpUrl: this.cdpUrl,
5227
- targetId: this.targetId,
5447
+ targetId: this._targetId,
5228
5448
  selector: opts.selector,
5229
5449
  frameSelector: opts.frameSelector,
5230
5450
  refsMode: opts.refsMode,
@@ -5243,7 +5463,7 @@ var CrawlPage = class {
5243
5463
  }
5244
5464
  return snapshotAi({
5245
5465
  cdpUrl: this.cdpUrl,
5246
- targetId: this.targetId,
5466
+ targetId: this._targetId,
5247
5467
  maxChars: opts?.maxChars,
5248
5468
  options: {
5249
5469
  interactive: opts?.interactive,
@@ -5262,7 +5482,7 @@ var CrawlPage = class {
5262
5482
  * @returns Array of accessibility tree nodes
5263
5483
  */
5264
5484
  async ariaSnapshot(opts) {
5265
- return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
5485
+ return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this._targetId, limit: opts?.limit });
5266
5486
  }
5267
5487
  // ── Interactions ─────────────────────────────────────────────
5268
5488
  /**
@@ -5282,7 +5502,7 @@ var CrawlPage = class {
5282
5502
  async click(ref, opts) {
5283
5503
  return clickViaPlaywright({
5284
5504
  cdpUrl: this.cdpUrl,
5285
- targetId: this.targetId,
5505
+ targetId: this._targetId,
5286
5506
  ref,
5287
5507
  doubleClick: opts?.doubleClick,
5288
5508
  button: opts?.button,
@@ -5309,7 +5529,7 @@ var CrawlPage = class {
5309
5529
  async clickBySelector(selector, opts) {
5310
5530
  return clickViaPlaywright({
5311
5531
  cdpUrl: this.cdpUrl,
5312
- targetId: this.targetId,
5532
+ targetId: this._targetId,
5313
5533
  selector,
5314
5534
  doubleClick: opts?.doubleClick,
5315
5535
  button: opts?.button,
@@ -5338,7 +5558,7 @@ var CrawlPage = class {
5338
5558
  async mouseClick(x, y, opts) {
5339
5559
  return mouseClickViaPlaywright({
5340
5560
  cdpUrl: this.cdpUrl,
5341
- targetId: this.targetId,
5561
+ targetId: this._targetId,
5342
5562
  x,
5343
5563
  y,
5344
5564
  button: opts?.button,
@@ -5365,7 +5585,7 @@ var CrawlPage = class {
5365
5585
  async pressAndHold(x, y, opts) {
5366
5586
  return pressAndHoldViaCdp({
5367
5587
  cdpUrl: this.cdpUrl,
5368
- targetId: this.targetId,
5588
+ targetId: this._targetId,
5369
5589
  x,
5370
5590
  y,
5371
5591
  delay: opts?.delay,
@@ -5389,7 +5609,7 @@ var CrawlPage = class {
5389
5609
  async clickByText(text, opts) {
5390
5610
  return clickByTextViaPlaywright({
5391
5611
  cdpUrl: this.cdpUrl,
5392
- targetId: this.targetId,
5612
+ targetId: this._targetId,
5393
5613
  text,
5394
5614
  exact: opts?.exact,
5395
5615
  button: opts?.button,
@@ -5416,7 +5636,7 @@ var CrawlPage = class {
5416
5636
  async clickByRole(role, name, opts) {
5417
5637
  return clickByRoleViaPlaywright({
5418
5638
  cdpUrl: this.cdpUrl,
5419
- targetId: this.targetId,
5639
+ targetId: this._targetId,
5420
5640
  role,
5421
5641
  name,
5422
5642
  index: opts?.index,
@@ -5445,7 +5665,7 @@ var CrawlPage = class {
5445
5665
  async type(ref, text, opts) {
5446
5666
  return typeViaPlaywright({
5447
5667
  cdpUrl: this.cdpUrl,
5448
- targetId: this.targetId,
5668
+ targetId: this._targetId,
5449
5669
  ref,
5450
5670
  text,
5451
5671
  submit: opts?.submit,
@@ -5462,7 +5682,7 @@ var CrawlPage = class {
5462
5682
  async hover(ref, opts) {
5463
5683
  return hoverViaPlaywright({
5464
5684
  cdpUrl: this.cdpUrl,
5465
- targetId: this.targetId,
5685
+ targetId: this._targetId,
5466
5686
  ref,
5467
5687
  timeoutMs: opts?.timeoutMs
5468
5688
  });
@@ -5482,7 +5702,7 @@ var CrawlPage = class {
5482
5702
  async select(ref, ...values) {
5483
5703
  return selectOptionViaPlaywright({
5484
5704
  cdpUrl: this.cdpUrl,
5485
- targetId: this.targetId,
5705
+ targetId: this._targetId,
5486
5706
  ref,
5487
5707
  values
5488
5708
  });
@@ -5497,7 +5717,7 @@ var CrawlPage = class {
5497
5717
  async drag(startRef, endRef, opts) {
5498
5718
  return dragViaPlaywright({
5499
5719
  cdpUrl: this.cdpUrl,
5500
- targetId: this.targetId,
5720
+ targetId: this._targetId,
5501
5721
  startRef,
5502
5722
  endRef,
5503
5723
  timeoutMs: opts?.timeoutMs
@@ -5522,7 +5742,7 @@ var CrawlPage = class {
5522
5742
  async fill(fields) {
5523
5743
  return fillFormViaPlaywright({
5524
5744
  cdpUrl: this.cdpUrl,
5525
- targetId: this.targetId,
5745
+ targetId: this._targetId,
5526
5746
  fields
5527
5747
  });
5528
5748
  }
@@ -5535,7 +5755,7 @@ var CrawlPage = class {
5535
5755
  async scrollIntoView(ref, opts) {
5536
5756
  return scrollIntoViewViaPlaywright({
5537
5757
  cdpUrl: this.cdpUrl,
5538
- targetId: this.targetId,
5758
+ targetId: this._targetId,
5539
5759
  ref,
5540
5760
  timeoutMs: opts?.timeoutMs
5541
5761
  });
@@ -5548,7 +5768,7 @@ var CrawlPage = class {
5548
5768
  async highlight(ref) {
5549
5769
  return highlightViaPlaywright({
5550
5770
  cdpUrl: this.cdpUrl,
5551
- targetId: this.targetId,
5771
+ targetId: this._targetId,
5552
5772
  ref
5553
5773
  });
5554
5774
  }
@@ -5561,7 +5781,7 @@ var CrawlPage = class {
5561
5781
  async uploadFile(ref, paths) {
5562
5782
  return setInputFilesViaPlaywright({
5563
5783
  cdpUrl: this.cdpUrl,
5564
- targetId: this.targetId,
5784
+ targetId: this._targetId,
5565
5785
  ref,
5566
5786
  paths
5567
5787
  });
@@ -5584,7 +5804,7 @@ var CrawlPage = class {
5584
5804
  async armDialog(opts) {
5585
5805
  return armDialogViaPlaywright({
5586
5806
  cdpUrl: this.cdpUrl,
5587
- targetId: this.targetId,
5807
+ targetId: this._targetId,
5588
5808
  accept: opts.accept,
5589
5809
  promptText: opts.promptText,
5590
5810
  timeoutMs: opts.timeoutMs
@@ -5628,7 +5848,7 @@ var CrawlPage = class {
5628
5848
  async onDialog(handler) {
5629
5849
  return setDialogHandler({
5630
5850
  cdpUrl: this.cdpUrl,
5631
- targetId: this.targetId,
5851
+ targetId: this._targetId,
5632
5852
  handler: handler ?? void 0
5633
5853
  });
5634
5854
  }
@@ -5650,7 +5870,7 @@ var CrawlPage = class {
5650
5870
  async armFileUpload(paths, opts) {
5651
5871
  return armFileUploadViaPlaywright({
5652
5872
  cdpUrl: this.cdpUrl,
5653
- targetId: this.targetId,
5873
+ targetId: this._targetId,
5654
5874
  paths,
5655
5875
  timeoutMs: opts?.timeoutMs
5656
5876
  });
@@ -5665,7 +5885,7 @@ var CrawlPage = class {
5665
5885
  async batch(actions, opts) {
5666
5886
  return batchViaPlaywright({
5667
5887
  cdpUrl: this.cdpUrl,
5668
- targetId: this.targetId,
5888
+ targetId: this._targetId,
5669
5889
  actions,
5670
5890
  stopOnError: opts?.stopOnError,
5671
5891
  evaluateEnabled: opts?.evaluateEnabled
@@ -5690,7 +5910,7 @@ var CrawlPage = class {
5690
5910
  async press(key, opts) {
5691
5911
  return pressKeyViaPlaywright({
5692
5912
  cdpUrl: this.cdpUrl,
5693
- targetId: this.targetId,
5913
+ targetId: this._targetId,
5694
5914
  key,
5695
5915
  delayMs: opts?.delayMs
5696
5916
  });
@@ -5700,14 +5920,14 @@ var CrawlPage = class {
5700
5920
  * Get the current URL of the page.
5701
5921
  */
5702
5922
  async url() {
5703
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5923
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5704
5924
  return page.url();
5705
5925
  }
5706
5926
  /**
5707
5927
  * Get the page title.
5708
5928
  */
5709
5929
  async title() {
5710
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5930
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5711
5931
  return page.title();
5712
5932
  }
5713
5933
  /**
@@ -5720,7 +5940,7 @@ var CrawlPage = class {
5720
5940
  async goto(url, opts) {
5721
5941
  return navigateViaPlaywright({
5722
5942
  cdpUrl: this.cdpUrl,
5723
- targetId: this.targetId,
5943
+ targetId: this._targetId,
5724
5944
  url,
5725
5945
  timeoutMs: opts?.timeoutMs,
5726
5946
  ssrfPolicy: this.ssrfPolicy
@@ -5732,7 +5952,7 @@ var CrawlPage = class {
5732
5952
  * @param opts - Timeout options
5733
5953
  */
5734
5954
  async reload(opts) {
5735
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5955
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5736
5956
  ensurePageState(page);
5737
5957
  await page.reload({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5738
5958
  }
@@ -5742,7 +5962,7 @@ var CrawlPage = class {
5742
5962
  * @param opts - Timeout options
5743
5963
  */
5744
5964
  async goBack(opts) {
5745
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5965
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5746
5966
  ensurePageState(page);
5747
5967
  await page.goBack({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5748
5968
  }
@@ -5752,7 +5972,7 @@ var CrawlPage = class {
5752
5972
  * @param opts - Timeout options
5753
5973
  */
5754
5974
  async goForward(opts) {
5755
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5975
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5756
5976
  ensurePageState(page);
5757
5977
  await page.goForward({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5758
5978
  }
@@ -5775,7 +5995,7 @@ var CrawlPage = class {
5775
5995
  async waitFor(opts) {
5776
5996
  return waitForViaPlaywright({
5777
5997
  cdpUrl: this.cdpUrl,
5778
- targetId: this.targetId,
5998
+ targetId: this._targetId,
5779
5999
  ...opts
5780
6000
  });
5781
6001
  }
@@ -5800,7 +6020,7 @@ var CrawlPage = class {
5800
6020
  async evaluate(fn, opts) {
5801
6021
  return evaluateViaPlaywright({
5802
6022
  cdpUrl: this.cdpUrl,
5803
- targetId: this.targetId,
6023
+ targetId: this._targetId,
5804
6024
  fn,
5805
6025
  ref: opts?.ref,
5806
6026
  timeoutMs: opts?.timeoutMs,
@@ -5827,7 +6047,7 @@ var CrawlPage = class {
5827
6047
  async evaluateInAllFrames(fn) {
5828
6048
  return evaluateInAllFramesViaPlaywright({
5829
6049
  cdpUrl: this.cdpUrl,
5830
- targetId: this.targetId,
6050
+ targetId: this._targetId,
5831
6051
  fn
5832
6052
  });
5833
6053
  }
@@ -5848,7 +6068,7 @@ var CrawlPage = class {
5848
6068
  async screenshot(opts) {
5849
6069
  const result = await takeScreenshotViaPlaywright({
5850
6070
  cdpUrl: this.cdpUrl,
5851
- targetId: this.targetId,
6071
+ targetId: this._targetId,
5852
6072
  fullPage: opts?.fullPage,
5853
6073
  ref: opts?.ref,
5854
6074
  element: opts?.element,
@@ -5864,7 +6084,7 @@ var CrawlPage = class {
5864
6084
  * @returns PDF document as a Buffer
5865
6085
  */
5866
6086
  async pdf() {
5867
- const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6087
+ const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5868
6088
  return result.buffer;
5869
6089
  }
5870
6090
  /**
@@ -5885,7 +6105,7 @@ var CrawlPage = class {
5885
6105
  async screenshotWithLabels(refs, opts) {
5886
6106
  return screenshotWithLabelsViaPlaywright({
5887
6107
  cdpUrl: this.cdpUrl,
5888
- targetId: this.targetId,
6108
+ targetId: this._targetId,
5889
6109
  refs,
5890
6110
  maxLabels: opts?.maxLabels,
5891
6111
  type: opts?.type
@@ -5902,7 +6122,7 @@ var CrawlPage = class {
5902
6122
  async traceStart(opts) {
5903
6123
  return traceStartViaPlaywright({
5904
6124
  cdpUrl: this.cdpUrl,
5905
- targetId: this.targetId,
6125
+ targetId: this._targetId,
5906
6126
  screenshots: opts?.screenshots,
5907
6127
  snapshots: opts?.snapshots,
5908
6128
  sources: opts?.sources
@@ -5917,7 +6137,7 @@ var CrawlPage = class {
5917
6137
  async traceStop(path2, opts) {
5918
6138
  return traceStopViaPlaywright({
5919
6139
  cdpUrl: this.cdpUrl,
5920
- targetId: this.targetId,
6140
+ targetId: this._targetId,
5921
6141
  path: path2,
5922
6142
  allowedOutputRoots: opts?.allowedOutputRoots
5923
6143
  });
@@ -5938,7 +6158,7 @@ var CrawlPage = class {
5938
6158
  async responseBody(url, opts) {
5939
6159
  return responseBodyViaPlaywright({
5940
6160
  cdpUrl: this.cdpUrl,
5941
- targetId: this.targetId,
6161
+ targetId: this._targetId,
5942
6162
  url,
5943
6163
  timeoutMs: opts?.timeoutMs,
5944
6164
  maxChars: opts?.maxChars
@@ -5966,7 +6186,7 @@ var CrawlPage = class {
5966
6186
  async waitForRequest(url, opts) {
5967
6187
  return waitForRequestViaPlaywright({
5968
6188
  cdpUrl: this.cdpUrl,
5969
- targetId: this.targetId,
6189
+ targetId: this._targetId,
5970
6190
  url,
5971
6191
  method: opts?.method,
5972
6192
  timeoutMs: opts?.timeoutMs,
@@ -5984,7 +6204,7 @@ var CrawlPage = class {
5984
6204
  async consoleLogs(opts) {
5985
6205
  return getConsoleMessagesViaPlaywright({
5986
6206
  cdpUrl: this.cdpUrl,
5987
- targetId: this.targetId,
6207
+ targetId: this._targetId,
5988
6208
  level: opts?.level,
5989
6209
  clear: opts?.clear
5990
6210
  });
@@ -5998,7 +6218,7 @@ var CrawlPage = class {
5998
6218
  async pageErrors(opts) {
5999
6219
  const result = await getPageErrorsViaPlaywright({
6000
6220
  cdpUrl: this.cdpUrl,
6001
- targetId: this.targetId,
6221
+ targetId: this._targetId,
6002
6222
  clear: opts?.clear
6003
6223
  });
6004
6224
  return result.errors;
@@ -6019,7 +6239,7 @@ var CrawlPage = class {
6019
6239
  async networkRequests(opts) {
6020
6240
  const result = await getNetworkRequestsViaPlaywright({
6021
6241
  cdpUrl: this.cdpUrl,
6022
- targetId: this.targetId,
6242
+ targetId: this._targetId,
6023
6243
  filter: opts?.filter,
6024
6244
  clear: opts?.clear
6025
6245
  });
@@ -6035,7 +6255,7 @@ var CrawlPage = class {
6035
6255
  async resize(width, height) {
6036
6256
  return resizeViewportViaPlaywright({
6037
6257
  cdpUrl: this.cdpUrl,
6038
- targetId: this.targetId,
6258
+ targetId: this._targetId,
6039
6259
  width,
6040
6260
  height
6041
6261
  });
@@ -6047,7 +6267,7 @@ var CrawlPage = class {
6047
6267
  * @returns Array of cookie objects
6048
6268
  */
6049
6269
  async cookies() {
6050
- const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6270
+ const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6051
6271
  return result.cookies;
6052
6272
  }
6053
6273
  /**
@@ -6065,11 +6285,11 @@ var CrawlPage = class {
6065
6285
  * ```
6066
6286
  */
6067
6287
  async setCookie(cookie) {
6068
- return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
6288
+ return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId, cookie });
6069
6289
  }
6070
6290
  /** Clear all cookies in the browser context. */
6071
6291
  async clearCookies() {
6072
- return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6292
+ return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6073
6293
  }
6074
6294
  /**
6075
6295
  * Get values from localStorage or sessionStorage.
@@ -6081,7 +6301,7 @@ var CrawlPage = class {
6081
6301
  async storageGet(kind, key) {
6082
6302
  const result = await storageGetViaPlaywright({
6083
6303
  cdpUrl: this.cdpUrl,
6084
- targetId: this.targetId,
6304
+ targetId: this._targetId,
6085
6305
  kind,
6086
6306
  key
6087
6307
  });
@@ -6097,7 +6317,7 @@ var CrawlPage = class {
6097
6317
  async storageSet(kind, key, value) {
6098
6318
  return storageSetViaPlaywright({
6099
6319
  cdpUrl: this.cdpUrl,
6100
- targetId: this.targetId,
6320
+ targetId: this._targetId,
6101
6321
  kind,
6102
6322
  key,
6103
6323
  value
@@ -6111,7 +6331,7 @@ var CrawlPage = class {
6111
6331
  async storageClear(kind) {
6112
6332
  return storageClearViaPlaywright({
6113
6333
  cdpUrl: this.cdpUrl,
6114
- targetId: this.targetId,
6334
+ targetId: this._targetId,
6115
6335
  kind
6116
6336
  });
6117
6337
  }
@@ -6133,7 +6353,7 @@ var CrawlPage = class {
6133
6353
  async download(ref, path2, opts) {
6134
6354
  return downloadViaPlaywright({
6135
6355
  cdpUrl: this.cdpUrl,
6136
- targetId: this.targetId,
6356
+ targetId: this._targetId,
6137
6357
  ref,
6138
6358
  path: path2,
6139
6359
  timeoutMs: opts?.timeoutMs,
@@ -6151,7 +6371,7 @@ var CrawlPage = class {
6151
6371
  async waitForDownload(opts) {
6152
6372
  return waitForDownloadViaPlaywright({
6153
6373
  cdpUrl: this.cdpUrl,
6154
- targetId: this.targetId,
6374
+ targetId: this._targetId,
6155
6375
  path: opts?.path,
6156
6376
  timeoutMs: opts?.timeoutMs,
6157
6377
  allowedOutputRoots: opts?.allowedOutputRoots
@@ -6166,7 +6386,7 @@ var CrawlPage = class {
6166
6386
  async setOffline(offline) {
6167
6387
  return setOfflineViaPlaywright({
6168
6388
  cdpUrl: this.cdpUrl,
6169
- targetId: this.targetId,
6389
+ targetId: this._targetId,
6170
6390
  offline
6171
6391
  });
6172
6392
  }
@@ -6183,7 +6403,7 @@ var CrawlPage = class {
6183
6403
  async setExtraHeaders(headers) {
6184
6404
  return setExtraHTTPHeadersViaPlaywright({
6185
6405
  cdpUrl: this.cdpUrl,
6186
- targetId: this.targetId,
6406
+ targetId: this._targetId,
6187
6407
  headers
6188
6408
  });
6189
6409
  }
@@ -6195,7 +6415,7 @@ var CrawlPage = class {
6195
6415
  async setHttpCredentials(opts) {
6196
6416
  return setHttpCredentialsViaPlaywright({
6197
6417
  cdpUrl: this.cdpUrl,
6198
- targetId: this.targetId,
6418
+ targetId: this._targetId,
6199
6419
  username: opts.username,
6200
6420
  password: opts.password,
6201
6421
  clear: opts.clear
@@ -6215,7 +6435,7 @@ var CrawlPage = class {
6215
6435
  async setGeolocation(opts) {
6216
6436
  return setGeolocationViaPlaywright({
6217
6437
  cdpUrl: this.cdpUrl,
6218
- targetId: this.targetId,
6438
+ targetId: this._targetId,
6219
6439
  latitude: opts.latitude,
6220
6440
  longitude: opts.longitude,
6221
6441
  accuracy: opts.accuracy,
@@ -6236,7 +6456,7 @@ var CrawlPage = class {
6236
6456
  async emulateMedia(opts) {
6237
6457
  return emulateMediaViaPlaywright({
6238
6458
  cdpUrl: this.cdpUrl,
6239
- targetId: this.targetId,
6459
+ targetId: this._targetId,
6240
6460
  colorScheme: opts.colorScheme
6241
6461
  });
6242
6462
  }
@@ -6248,7 +6468,7 @@ var CrawlPage = class {
6248
6468
  async setLocale(locale) {
6249
6469
  return setLocaleViaPlaywright({
6250
6470
  cdpUrl: this.cdpUrl,
6251
- targetId: this.targetId,
6471
+ targetId: this._targetId,
6252
6472
  locale
6253
6473
  });
6254
6474
  }
@@ -6260,7 +6480,7 @@ var CrawlPage = class {
6260
6480
  async setTimezone(timezoneId) {
6261
6481
  return setTimezoneViaPlaywright({
6262
6482
  cdpUrl: this.cdpUrl,
6263
- targetId: this.targetId,
6483
+ targetId: this._targetId,
6264
6484
  timezoneId
6265
6485
  });
6266
6486
  }
@@ -6277,7 +6497,7 @@ var CrawlPage = class {
6277
6497
  async setDevice(name) {
6278
6498
  return setDeviceViaPlaywright({
6279
6499
  cdpUrl: this.cdpUrl,
6280
- targetId: this.targetId,
6500
+ targetId: this._targetId,
6281
6501
  name
6282
6502
  });
6283
6503
  }
@@ -6298,7 +6518,7 @@ var CrawlPage = class {
6298
6518
  * ```
6299
6519
  */
6300
6520
  async detectChallenge() {
6301
- return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6521
+ return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6302
6522
  }
6303
6523
  /**
6304
6524
  * Wait for an anti-bot challenge to resolve on its own.
@@ -6323,7 +6543,7 @@ var CrawlPage = class {
6323
6543
  async waitForChallenge(opts) {
6324
6544
  return waitForChallengeViaPlaywright({
6325
6545
  cdpUrl: this.cdpUrl,
6326
- targetId: this.targetId,
6546
+ targetId: this._targetId,
6327
6547
  timeoutMs: opts?.timeoutMs,
6328
6548
  pollMs: opts?.pollMs
6329
6549
  });
@@ -6360,8 +6580,24 @@ var CrawlPage = class {
6360
6580
  */
6361
6581
  async isAuthenticated(rules) {
6362
6582
  if (!rules.length) return { authenticated: true, checks: [] };
6363
- const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6583
+ const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6364
6584
  const checks = [];
6585
+ const needsBodyText = rules.some((r) => r.text !== void 0 || r.textGone !== void 0);
6586
+ let bodyText = null;
6587
+ if (needsBodyText) {
6588
+ try {
6589
+ const raw = await evaluateViaPlaywright({
6590
+ cdpUrl: this.cdpUrl,
6591
+ targetId: this._targetId,
6592
+ fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6593
+ });
6594
+ bodyText = typeof raw === "string" ? raw : null;
6595
+ } catch (err) {
6596
+ console.warn(
6597
+ `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6598
+ );
6599
+ }
6600
+ }
6365
6601
  for (const rule of rules) {
6366
6602
  if (rule.url !== void 0) {
6367
6603
  const currentUrl = page.url();
@@ -6394,19 +6630,6 @@ var CrawlPage = class {
6394
6630
  }
6395
6631
  }
6396
6632
  if (rule.text !== void 0 || rule.textGone !== void 0) {
6397
- let bodyText = null;
6398
- try {
6399
- const raw = await evaluateViaPlaywright({
6400
- cdpUrl: this.cdpUrl,
6401
- targetId: this.targetId,
6402
- fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6403
- });
6404
- bodyText = typeof raw === "string" ? raw : null;
6405
- } catch (err) {
6406
- console.warn(
6407
- `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6408
- );
6409
- }
6410
6633
  if (rule.text !== void 0) {
6411
6634
  if (bodyText === null) {
6412
6635
  checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
@@ -6436,7 +6659,7 @@ var CrawlPage = class {
6436
6659
  try {
6437
6660
  const result = await evaluateViaPlaywright({
6438
6661
  cdpUrl: this.cdpUrl,
6439
- targetId: this.targetId,
6662
+ targetId: this._targetId,
6440
6663
  fn: rule.fn
6441
6664
  });
6442
6665
  const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
@@ -6485,7 +6708,7 @@ var CrawlPage = class {
6485
6708
  * ```
6486
6709
  */
6487
6710
  async playwrightPage() {
6488
- return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6711
+ return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6489
6712
  }
6490
6713
  /**
6491
6714
  * Create a Playwright `Locator` for a CSS selector on this page.
@@ -6508,7 +6731,7 @@ var CrawlPage = class {
6508
6731
  * ```
6509
6732
  */
6510
6733
  async locator(selector) {
6511
- const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6734
+ const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6512
6735
  return pwPage.locator(selector);
6513
6736
  }
6514
6737
  };
@@ -6552,21 +6775,27 @@ var BrowserClaw = class _BrowserClaw {
6552
6775
  static async launch(opts = {}) {
6553
6776
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6554
6777
  const chrome = await launchChrome(opts);
6555
- const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6556
- const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6557
- const telemetry = {
6558
- launchMs: chrome.launchMs,
6559
- timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6560
- };
6561
- const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6562
- if (opts.url !== void 0 && opts.url !== "") {
6563
- const page = await browser.currentPage();
6564
- const navT0 = Date.now();
6565
- await page.goto(opts.url);
6566
- telemetry.navMs = Date.now() - navT0;
6567
- telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6778
+ try {
6779
+ const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6780
+ const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6781
+ const telemetry = {
6782
+ launchMs: chrome.launchMs,
6783
+ timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6784
+ };
6785
+ const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6786
+ if (opts.url !== void 0 && opts.url !== "") {
6787
+ const page = await browser.currentPage();
6788
+ const navT0 = Date.now();
6789
+ await page.goto(opts.url);
6790
+ telemetry.navMs = Date.now() - navT0;
6791
+ telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6792
+ }
6793
+ return browser;
6794
+ } catch (err) {
6795
+ await stopChrome(chrome).catch(() => {
6796
+ });
6797
+ throw err;
6568
6798
  }
6569
- return browser;
6570
6799
  }
6571
6800
  /**
6572
6801
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -6723,7 +6952,7 @@ var BrowserClaw = class _BrowserClaw {
6723
6952
  if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
6724
6953
  try {
6725
6954
  clearRecordingContext(this.cdpUrl);
6726
- await disconnectBrowser();
6955
+ await closePlaywrightBrowserConnection({ cdpUrl: this.cdpUrl });
6727
6956
  if (this.chrome) {
6728
6957
  await stopChrome(this.chrome);
6729
6958
  this.chrome = null;