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.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,7 +2659,6 @@ 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
  );
@@ -2567,7 +2675,6 @@ async function resolvePageByTargetIdOrThrow(opts) {
2567
2675
  async function getRestoredPageForTarget(opts) {
2568
2676
  const page = await getPageForTargetId(opts);
2569
2677
  ensurePageState(page);
2570
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2571
2678
  return page;
2572
2679
  }
2573
2680
 
@@ -2621,10 +2728,11 @@ var BROWSER_EVALUATOR = new Function(
2621
2728
  " catch (_) { candidate = (0, eval)(fnBody); }",
2622
2729
  ' var result = typeof candidate === "function" ? candidate() : candidate;',
2623
2730
  ' if (result && typeof result.then === "function") {',
2731
+ " var tid;",
2624
2732
  " return Promise.race([",
2625
- " result,",
2733
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2626
2734
  " new Promise(function(_, reject) {",
2627
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2735
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2628
2736
  " })",
2629
2737
  " ]);",
2630
2738
  " }",
@@ -2646,10 +2754,11 @@ var ELEMENT_EVALUATOR = new Function(
2646
2754
  " catch (_) { candidate = (0, eval)(fnBody); }",
2647
2755
  ' var result = typeof candidate === "function" ? candidate(el) : candidate;',
2648
2756
  ' if (result && typeof result.then === "function") {',
2757
+ " var tid;",
2649
2758
  " return Promise.race([",
2650
- " result,",
2759
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2651
2760
  " new Promise(function(_, reject) {",
2652
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2761
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2653
2762
  " })",
2654
2763
  " ]);",
2655
2764
  " }",
@@ -2664,10 +2773,8 @@ async function evaluateViaPlaywright(opts) {
2664
2773
  if (!fnText) throw new Error("function is required");
2665
2774
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2666
2775
  ensurePageState(page);
2667
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2668
2776
  const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2669
- let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
2670
- evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
2777
+ const evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 1e3));
2671
2778
  const signal = opts.signal;
2672
2779
  let abortListener;
2673
2780
  let abortReject;
@@ -2681,10 +2788,16 @@ async function evaluateViaPlaywright(opts) {
2681
2788
  }
2682
2789
  if (signal !== void 0) {
2683
2790
  const disconnect = () => {
2684
- forceDisconnectPlaywrightConnection({
2685
- cdpUrl: opts.cdpUrl,
2686
- targetId: opts.targetId}).catch(() => {
2687
- });
2791
+ const targetId = opts.targetId?.trim() ?? "";
2792
+ if (targetId !== "") {
2793
+ tryTerminateExecutionViaCdp(opts.cdpUrl, targetId).catch(() => {
2794
+ });
2795
+ } else {
2796
+ console.warn("[browserclaw] evaluate abort: no targetId, forcing full disconnect");
2797
+ forceDisconnectPlaywrightConnection({
2798
+ cdpUrl: opts.cdpUrl}).catch(() => {
2799
+ });
2800
+ }
2688
2801
  };
2689
2802
  if (signal.aborted) {
2690
2803
  disconnect();
@@ -2720,6 +2833,8 @@ async function evaluateViaPlaywright(opts) {
2720
2833
  );
2721
2834
  } finally {
2722
2835
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
2836
+ abortReject = void 0;
2837
+ abortListener = void 0;
2723
2838
  }
2724
2839
  }
2725
2840
 
@@ -2912,7 +3027,7 @@ function isBlockedHostnameOrIp(hostname, policy) {
2912
3027
  function isPrivateIpAddress(address, policy) {
2913
3028
  let normalized = address.trim().toLowerCase();
2914
3029
  if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
2915
- if (!normalized) return false;
3030
+ if (!normalized) return true;
2916
3031
  const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
2917
3032
  const strictIp = parseCanonicalIpAddress(normalized);
2918
3033
  if (strictIp) {
@@ -2999,6 +3114,25 @@ function createPinnedLookup(params) {
2999
3114
  cb(null, chosen.address, chosen.family);
3000
3115
  });
3001
3116
  }
3117
+ var DNS_CACHE_TTL_MS = 3e4;
3118
+ var MAX_DNS_CACHE_SIZE = 100;
3119
+ var dnsCache = /* @__PURE__ */ new Map();
3120
+ function getCachedDnsResult(hostname) {
3121
+ const entry = dnsCache.get(hostname);
3122
+ if (!entry) return void 0;
3123
+ if (Date.now() > entry.expiresAt) {
3124
+ dnsCache.delete(hostname);
3125
+ return void 0;
3126
+ }
3127
+ return entry.result;
3128
+ }
3129
+ function cacheDnsResult(hostname, result) {
3130
+ dnsCache.set(hostname, { result, expiresAt: Date.now() + DNS_CACHE_TTL_MS });
3131
+ if (dnsCache.size > MAX_DNS_CACHE_SIZE) {
3132
+ const first = dnsCache.keys().next();
3133
+ if (first.done !== true) dnsCache.delete(first.value);
3134
+ }
3135
+ }
3002
3136
  async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3003
3137
  const normalized = normalizeHostname(hostname);
3004
3138
  if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
@@ -3017,6 +3151,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3017
3151
  );
3018
3152
  }
3019
3153
  }
3154
+ const cached = getCachedDnsResult(normalized);
3155
+ if (cached) return cached;
3020
3156
  const lookupFn = params.lookupFn ?? lookup$1;
3021
3157
  let results;
3022
3158
  try {
@@ -3046,11 +3182,13 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3046
3182
  `Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
3047
3183
  );
3048
3184
  }
3049
- return {
3185
+ const pinned = {
3050
3186
  hostname: normalized,
3051
3187
  addresses,
3052
3188
  lookup: createPinnedLookup({ hostname: normalized, addresses })
3053
3189
  };
3190
+ cacheDnsResult(normalized, pinned);
3191
+ return pinned;
3054
3192
  }
3055
3193
  async function assertBrowserNavigationAllowed(opts) {
3056
3194
  const rawUrl = opts.url.trim();
@@ -3204,13 +3342,27 @@ function buildSiblingTempPath(targetPath) {
3204
3342
  return join(dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
3205
3343
  }
3206
3344
  async function writeViaSiblingTempPath(params) {
3207
- const rootDir = await realpath(resolve(params.rootDir)).catch(() => resolve(params.rootDir));
3345
+ let rootDir;
3346
+ try {
3347
+ rootDir = await realpath(resolve(params.rootDir));
3348
+ } catch {
3349
+ console.warn(`[browserclaw] writeViaSiblingTempPath: rootDir realpath failed, using lexical resolve`);
3350
+ rootDir = resolve(params.rootDir);
3351
+ }
3208
3352
  const requestedTargetPath = resolve(params.targetPath);
3209
3353
  const targetPath = await realpath(dirname(requestedTargetPath)).then((realDir) => join(realDir, basename(requestedTargetPath))).catch(() => requestedTargetPath);
3210
3354
  const relativeTargetPath = relative(rootDir, targetPath);
3211
3355
  if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${sep}`) || isAbsolute(relativeTargetPath)) {
3212
3356
  throw new Error("Target path is outside the allowed root");
3213
3357
  }
3358
+ try {
3359
+ const stat = await lstat(targetPath);
3360
+ if (stat.isSymbolicLink()) {
3361
+ throw new Error(`Unsafe output path: "${params.targetPath}" is a symbolic link.`);
3362
+ }
3363
+ } catch (e) {
3364
+ if (e.code !== "ENOENT") throw e;
3365
+ }
3214
3366
  const tempPath = buildSiblingTempPath(targetPath);
3215
3367
  let renameSucceeded = false;
3216
3368
  try {
@@ -3232,6 +3384,9 @@ async function assertBrowserNavigationResultAllowed(opts) {
3232
3384
  } catch {
3233
3385
  return;
3234
3386
  }
3387
+ if (parsed.protocol === "data:" || parsed.protocol === "blob:") {
3388
+ throw new InvalidBrowserNavigationUrlError(`Navigation result blocked: "${parsed.protocol}" URLs are not allowed.`);
3389
+ }
3235
3390
  if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) {
3236
3391
  await assertBrowserNavigationAllowed(opts);
3237
3392
  }
@@ -3349,9 +3504,10 @@ async function clickViaPlaywright(opts) {
3349
3504
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3350
3505
  const POLL_INTERVAL_MS = 50;
3351
3506
  const POLL_TIMEOUT_MS = 500;
3507
+ const ATTR_TIMEOUT_MS = Math.min(timeout, POLL_TIMEOUT_MS);
3352
3508
  let changed = false;
3353
3509
  for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
3354
- const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3510
+ const current = await locator.getAttribute("aria-checked", { timeout: ATTR_TIMEOUT_MS }).catch(() => void 0);
3355
3511
  if (current === void 0 || current !== ariaCheckedBefore) {
3356
3512
  changed = true;
3357
3513
  break;
@@ -3428,6 +3584,7 @@ async function dragViaPlaywright(opts) {
3428
3584
  async function fillFormViaPlaywright(opts) {
3429
3585
  const page = await getRestoredPageForTarget(opts);
3430
3586
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3587
+ let filledCount = 0;
3431
3588
  for (const field of opts.fields) {
3432
3589
  const ref = field.ref.trim();
3433
3590
  const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
@@ -3446,16 +3603,24 @@ async function fillFormViaPlaywright(opts) {
3446
3603
  try {
3447
3604
  await setCheckedViaEvaluate(locator, checked);
3448
3605
  } catch (err) {
3449
- throw toAIFriendlyError(err, ref);
3606
+ const friendly = toAIFriendlyError(err, ref);
3607
+ throw new Error(
3608
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3609
+ );
3450
3610
  }
3451
3611
  }
3612
+ filledCount += 1;
3452
3613
  continue;
3453
3614
  }
3454
3615
  try {
3455
3616
  await locator.fill(value, { timeout });
3456
3617
  } catch (err) {
3457
- throw toAIFriendlyError(err, ref);
3618
+ const friendly = toAIFriendlyError(err, ref);
3619
+ throw new Error(
3620
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3621
+ );
3458
3622
  }
3623
+ filledCount += 1;
3459
3624
  }
3460
3625
  }
3461
3626
  async function scrollIntoViewViaPlaywright(opts) {
@@ -3521,16 +3686,22 @@ async function armDialogViaPlaywright(opts) {
3521
3686
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3522
3687
  state.armIdDialog = bumpDialogArmId(state);
3523
3688
  const armId = state.armIdDialog;
3689
+ const resetArm = () => {
3690
+ if (state.armIdDialog === armId) state.armIdDialog = 0;
3691
+ };
3692
+ page.once("close", resetArm);
3524
3693
  page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
3525
3694
  if (state.armIdDialog !== armId) return;
3526
3695
  try {
3527
3696
  if (opts.accept) await dialog.accept(opts.promptText);
3528
3697
  else await dialog.dismiss();
3529
3698
  } finally {
3530
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3699
+ resetArm();
3700
+ page.off("close", resetArm);
3531
3701
  }
3532
3702
  }).catch(() => {
3533
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3703
+ resetArm();
3704
+ page.off("close", resetArm);
3534
3705
  });
3535
3706
  }
3536
3707
  async function armFileUploadViaPlaywright(opts) {
@@ -3539,6 +3710,10 @@ async function armFileUploadViaPlaywright(opts) {
3539
3710
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3540
3711
  state.armIdUpload = bumpUploadArmId(state);
3541
3712
  const armId = state.armIdUpload;
3713
+ const resetArm = () => {
3714
+ if (state.armIdUpload === armId) state.armIdUpload = 0;
3715
+ };
3716
+ page.once("close", resetArm);
3542
3717
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3543
3718
  if (state.armIdUpload !== armId) return;
3544
3719
  if (opts.paths === void 0 || opts.paths.length === 0) {
@@ -3570,9 +3745,18 @@ async function armFileUploadViaPlaywright(opts) {
3570
3745
  el.dispatchEvent(new Event("change", { bubbles: true }));
3571
3746
  });
3572
3747
  }
3573
- } catch {
3748
+ } catch (e) {
3749
+ console.warn(
3750
+ `[browserclaw] armFileUpload: dispatch events failed: ${e instanceof Error ? e.message : String(e)}`
3751
+ );
3574
3752
  }
3575
- }).catch(() => {
3753
+ }).catch((e) => {
3754
+ console.warn(
3755
+ `[browserclaw] armFileUpload: filechooser wait failed: ${e instanceof Error ? e.message : String(e)}`
3756
+ );
3757
+ }).finally(() => {
3758
+ resetArm();
3759
+ page.off("close", resetArm);
3576
3760
  });
3577
3761
  }
3578
3762
 
@@ -3592,7 +3776,7 @@ function clearRecordingContext(cdpUrl) {
3592
3776
  }
3593
3777
  async function createRecordingContext(browser, cdpUrl, recordVideo) {
3594
3778
  const context = await browser.newContext({ recordVideo });
3595
- observeContext(context);
3779
+ await observeContext(context);
3596
3780
  recordingContexts.set(cdpUrl, context);
3597
3781
  context.on("close", () => recordingContexts.delete(cdpUrl));
3598
3782
  return context;
@@ -3647,6 +3831,11 @@ async function gotoPageWithNavigationGuard(opts) {
3647
3831
  await route.continue();
3648
3832
  return;
3649
3833
  }
3834
+ const isRedirect = request.redirectedFrom() !== null;
3835
+ if (!isRedirect && request.url() !== opts.url) {
3836
+ await route.continue();
3837
+ return;
3838
+ }
3650
3839
  try {
3651
3840
  await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
3652
3841
  } catch (err) {
@@ -3698,6 +3887,7 @@ async function navigateViaPlaywright(opts) {
3698
3887
  response = await navigate();
3699
3888
  } catch (err) {
3700
3889
  if (!isRetryableNavigateError(err)) throw err;
3890
+ recordingContexts.delete(opts.cdpUrl);
3701
3891
  await forceDisconnectPlaywrightConnection({
3702
3892
  cdpUrl: opts.cdpUrl,
3703
3893
  targetId: opts.targetId}).catch(() => {
@@ -3757,6 +3947,7 @@ async function createPageViaPlaywright(opts) {
3757
3947
  });
3758
3948
  } catch (err) {
3759
3949
  if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
3950
+ console.warn(`[browserclaw] createPage navigation failed: ${err instanceof Error ? err.message : String(err)}`);
3760
3951
  }
3761
3952
  await assertPageNavigationCompletedSafely({
3762
3953
  cdpUrl: opts.cdpUrl,
@@ -3843,39 +4034,46 @@ var MAX_WAIT_TIME_MS = 3e4;
3843
4034
  async function waitForViaPlaywright(opts) {
3844
4035
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3845
4036
  ensurePageState(page);
3846
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4037
+ const totalTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4038
+ const deadline = Date.now() + totalTimeout;
4039
+ const remaining = () => Math.max(500, deadline - Date.now());
3847
4040
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3848
4041
  await page.waitForTimeout(resolveBoundedDelayMs(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3849
4042
  }
3850
4043
  if (opts.text !== void 0 && opts.text !== "") {
3851
- await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
4044
+ await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, {
4045
+ timeout: remaining()
4046
+ });
3852
4047
  }
3853
4048
  if (opts.textGone !== void 0 && opts.textGone !== "") {
3854
- await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, { timeout });
4049
+ await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, {
4050
+ timeout: remaining()
4051
+ });
3855
4052
  }
3856
4053
  if (opts.selector !== void 0 && opts.selector !== "") {
3857
4054
  const selector = opts.selector.trim();
3858
- if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
4055
+ if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout: remaining() });
3859
4056
  }
3860
4057
  if (opts.url !== void 0 && opts.url !== "") {
3861
4058
  const url = opts.url.trim();
3862
- if (url !== "") await page.waitForURL(url, { timeout });
4059
+ if (url !== "") await page.waitForURL(url, { timeout: remaining() });
3863
4060
  }
3864
4061
  if (opts.loadState !== void 0) {
3865
- await page.waitForLoadState(opts.loadState, { timeout });
4062
+ await page.waitForLoadState(opts.loadState, { timeout: remaining() });
3866
4063
  }
3867
4064
  if (opts.fn !== void 0) {
3868
4065
  if (typeof opts.fn === "function") {
3869
- await page.waitForFunction(opts.fn, opts.arg, { timeout });
4066
+ await page.waitForFunction(opts.fn, opts.arg, { timeout: remaining() });
3870
4067
  } else {
3871
4068
  const fn = opts.fn.trim();
3872
- if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
4069
+ if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout: remaining() });
3873
4070
  }
3874
4071
  }
3875
4072
  }
3876
4073
 
3877
4074
  // src/actions/batch.ts
3878
4075
  var MAX_BATCH_DEPTH = 5;
4076
+ var MAX_BATCH_TIMEOUT_MS = 3e5;
3879
4077
  var MAX_BATCH_ACTIONS = 100;
3880
4078
  async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
3881
4079
  if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
@@ -4023,13 +4221,19 @@ async function batchViaPlaywright(opts) {
4023
4221
  throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
4024
4222
  const results = [];
4025
4223
  const evaluateEnabled = opts.evaluateEnabled !== false;
4224
+ const deadline = Date.now() + MAX_BATCH_TIMEOUT_MS;
4026
4225
  for (const action of opts.actions) {
4226
+ if (Date.now() > deadline) {
4227
+ results.push({ ok: false, error: "Batch timeout exceeded" });
4228
+ break;
4229
+ }
4027
4230
  try {
4028
4231
  await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
4029
4232
  results.push({ ok: true });
4030
4233
  } catch (err) {
4031
4234
  const message = err instanceof Error ? err.message : String(err);
4032
4235
  results.push({ ok: false, error: message });
4236
+ if (err instanceof BrowserTabNotFoundError || err instanceof BlockedBrowserTargetError) break;
4033
4237
  if (opts.stopOnError !== false) break;
4034
4238
  }
4035
4239
  }
@@ -4047,8 +4251,10 @@ function createPageDownloadWaiter(page, timeoutMs) {
4047
4251
  handler = void 0;
4048
4252
  }
4049
4253
  };
4254
+ let rejectPromise;
4050
4255
  return {
4051
4256
  promise: new Promise((resolve2, reject) => {
4257
+ rejectPromise = reject;
4052
4258
  handler = (download) => {
4053
4259
  if (done) return;
4054
4260
  done = true;
@@ -4067,6 +4273,7 @@ function createPageDownloadWaiter(page, timeoutMs) {
4067
4273
  if (done) return;
4068
4274
  done = true;
4069
4275
  cleanup();
4276
+ rejectPromise?.(new Error("Download waiter cancelled"));
4070
4277
  }
4071
4278
  };
4072
4279
  }
@@ -4098,12 +4305,11 @@ async function downloadViaPlaywright(opts) {
4098
4305
  await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
4099
4306
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4100
4307
  const state = ensurePageState(page);
4101
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4102
4308
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4103
4309
  const outPath = opts.path.trim();
4104
4310
  if (!outPath) throw new Error("path is required");
4105
- state.armIdDownload = bumpDownloadArmId(state);
4106
- const armId = state.armIdDownload;
4311
+ const armId = bumpDownloadArmId(state);
4312
+ state.armIdDownload = armId;
4107
4313
  const waiter = createPageDownloadWaiter(page, timeout);
4108
4314
  try {
4109
4315
  const locator = refLocator(page, opts.ref);
@@ -4150,12 +4356,6 @@ async function setDeviceViaPlaywright(opts) {
4150
4356
  }
4151
4357
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4152
4358
  ensurePageState(page);
4153
- if (device.viewport !== null) {
4154
- await page.setViewportSize({
4155
- width: device.viewport.width,
4156
- height: device.viewport.height
4157
- });
4158
- }
4159
4359
  await withPageScopedCdpClient({
4160
4360
  cdpUrl: opts.cdpUrl,
4161
4361
  page,
@@ -4183,11 +4383,24 @@ async function setDeviceViaPlaywright(opts) {
4183
4383
  }
4184
4384
  }
4185
4385
  });
4386
+ if (device.viewport !== null) {
4387
+ await page.setViewportSize({
4388
+ width: device.viewport.width,
4389
+ height: device.viewport.height
4390
+ });
4391
+ }
4186
4392
  }
4187
4393
  async function setExtraHTTPHeadersViaPlaywright(opts) {
4188
4394
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4189
4395
  ensurePageState(page);
4190
- await page.context().setExtraHTTPHeaders(opts.headers);
4396
+ await withPageScopedCdpClient({
4397
+ cdpUrl: opts.cdpUrl,
4398
+ page,
4399
+ targetId: opts.targetId,
4400
+ fn: async (send) => {
4401
+ await send("Network.setExtraHTTPHeaders", { headers: opts.headers });
4402
+ }
4403
+ });
4191
4404
  }
4192
4405
  async function setGeolocationViaPlaywright(opts) {
4193
4406
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -4195,7 +4408,8 @@ async function setGeolocationViaPlaywright(opts) {
4195
4408
  const context = page.context();
4196
4409
  if (opts.clear === true) {
4197
4410
  await context.setGeolocation(null);
4198
- await context.clearPermissions().catch(() => {
4411
+ await context.clearPermissions().catch((err) => {
4412
+ console.warn(`[browserclaw] clearPermissions failed: ${err instanceof Error ? err.message : String(err)}`);
4199
4413
  });
4200
4414
  return;
4201
4415
  }
@@ -4497,7 +4711,6 @@ async function waitForRequestViaPlaywright(opts) {
4497
4711
  async function takeScreenshotViaPlaywright(opts) {
4498
4712
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4499
4713
  ensurePageState(page);
4500
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4501
4714
  const type = opts.type ?? "png";
4502
4715
  if (opts.ref !== void 0 && opts.ref !== "") {
4503
4716
  if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
@@ -4512,7 +4725,6 @@ async function takeScreenshotViaPlaywright(opts) {
4512
4725
  async function screenshotWithLabelsViaPlaywright(opts) {
4513
4726
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4514
4727
  ensurePageState(page);
4515
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4516
4728
  const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
4517
4729
  const type = opts.type ?? "png";
4518
4730
  const refs = opts.refs.slice(0, maxLabels);
@@ -4576,7 +4788,8 @@ async function screenshotWithLabelsViaPlaywright(opts) {
4576
4788
  document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
4577
4789
  el.remove();
4578
4790
  });
4579
- }).catch(() => {
4791
+ }).catch((err) => {
4792
+ console.warn(`[browserclaw] label overlay cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
4580
4793
  });
4581
4794
  }
4582
4795
  }
@@ -4604,17 +4817,14 @@ async function traceStopViaPlaywright(opts) {
4604
4817
  if (!ctxState.traceActive) {
4605
4818
  throw new Error("No active trace. Start a trace before stopping it.");
4606
4819
  }
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
- }
4820
+ ctxState.traceActive = false;
4821
+ await writeViaSiblingTempPath({
4822
+ rootDir: dirname(opts.path),
4823
+ targetPath: opts.path,
4824
+ writeTemp: async (tempPath) => {
4825
+ await context.tracing.stop({ path: tempPath });
4826
+ }
4827
+ });
4618
4828
  }
4619
4829
 
4620
4830
  // src/snapshot/ref-map.ts
@@ -5175,17 +5385,27 @@ async function storageClearViaPlaywright(opts) {
5175
5385
  // src/browser.ts
5176
5386
  var CrawlPage = class {
5177
5387
  cdpUrl;
5178
- targetId;
5388
+ _targetId;
5179
5389
  ssrfPolicy;
5180
5390
  /** @internal */
5181
5391
  constructor(cdpUrl, targetId, ssrfPolicy) {
5182
5392
  this.cdpUrl = cdpUrl;
5183
- this.targetId = targetId;
5393
+ this._targetId = targetId;
5184
5394
  this.ssrfPolicy = ssrfPolicy;
5185
5395
  }
5186
5396
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
5187
5397
  get id() {
5188
- return this.targetId;
5398
+ return this._targetId;
5399
+ }
5400
+ /**
5401
+ * Refresh the target ID by re-resolving the page from the browser.
5402
+ * Useful after reconnection when the old target ID may be stale.
5403
+ */
5404
+ async refreshTargetId() {
5405
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5406
+ const newId = await pageTargetId(page);
5407
+ if (newId !== null && newId !== "") this._targetId = newId;
5408
+ return this._targetId;
5189
5409
  }
5190
5410
  // ── Snapshot ──────────────────────────────────────────────────
5191
5411
  /**
@@ -5213,7 +5433,7 @@ var CrawlPage = class {
5213
5433
  if (opts?.mode === "role") {
5214
5434
  return snapshotRole({
5215
5435
  cdpUrl: this.cdpUrl,
5216
- targetId: this.targetId,
5436
+ targetId: this._targetId,
5217
5437
  selector: opts.selector,
5218
5438
  frameSelector: opts.frameSelector,
5219
5439
  refsMode: opts.refsMode,
@@ -5232,7 +5452,7 @@ var CrawlPage = class {
5232
5452
  }
5233
5453
  return snapshotAi({
5234
5454
  cdpUrl: this.cdpUrl,
5235
- targetId: this.targetId,
5455
+ targetId: this._targetId,
5236
5456
  maxChars: opts?.maxChars,
5237
5457
  options: {
5238
5458
  interactive: opts?.interactive,
@@ -5251,7 +5471,7 @@ var CrawlPage = class {
5251
5471
  * @returns Array of accessibility tree nodes
5252
5472
  */
5253
5473
  async ariaSnapshot(opts) {
5254
- return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
5474
+ return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this._targetId, limit: opts?.limit });
5255
5475
  }
5256
5476
  // ── Interactions ─────────────────────────────────────────────
5257
5477
  /**
@@ -5271,7 +5491,7 @@ var CrawlPage = class {
5271
5491
  async click(ref, opts) {
5272
5492
  return clickViaPlaywright({
5273
5493
  cdpUrl: this.cdpUrl,
5274
- targetId: this.targetId,
5494
+ targetId: this._targetId,
5275
5495
  ref,
5276
5496
  doubleClick: opts?.doubleClick,
5277
5497
  button: opts?.button,
@@ -5298,7 +5518,7 @@ var CrawlPage = class {
5298
5518
  async clickBySelector(selector, opts) {
5299
5519
  return clickViaPlaywright({
5300
5520
  cdpUrl: this.cdpUrl,
5301
- targetId: this.targetId,
5521
+ targetId: this._targetId,
5302
5522
  selector,
5303
5523
  doubleClick: opts?.doubleClick,
5304
5524
  button: opts?.button,
@@ -5327,7 +5547,7 @@ var CrawlPage = class {
5327
5547
  async mouseClick(x, y, opts) {
5328
5548
  return mouseClickViaPlaywright({
5329
5549
  cdpUrl: this.cdpUrl,
5330
- targetId: this.targetId,
5550
+ targetId: this._targetId,
5331
5551
  x,
5332
5552
  y,
5333
5553
  button: opts?.button,
@@ -5354,7 +5574,7 @@ var CrawlPage = class {
5354
5574
  async pressAndHold(x, y, opts) {
5355
5575
  return pressAndHoldViaCdp({
5356
5576
  cdpUrl: this.cdpUrl,
5357
- targetId: this.targetId,
5577
+ targetId: this._targetId,
5358
5578
  x,
5359
5579
  y,
5360
5580
  delay: opts?.delay,
@@ -5378,7 +5598,7 @@ var CrawlPage = class {
5378
5598
  async clickByText(text, opts) {
5379
5599
  return clickByTextViaPlaywright({
5380
5600
  cdpUrl: this.cdpUrl,
5381
- targetId: this.targetId,
5601
+ targetId: this._targetId,
5382
5602
  text,
5383
5603
  exact: opts?.exact,
5384
5604
  button: opts?.button,
@@ -5405,7 +5625,7 @@ var CrawlPage = class {
5405
5625
  async clickByRole(role, name, opts) {
5406
5626
  return clickByRoleViaPlaywright({
5407
5627
  cdpUrl: this.cdpUrl,
5408
- targetId: this.targetId,
5628
+ targetId: this._targetId,
5409
5629
  role,
5410
5630
  name,
5411
5631
  index: opts?.index,
@@ -5434,7 +5654,7 @@ var CrawlPage = class {
5434
5654
  async type(ref, text, opts) {
5435
5655
  return typeViaPlaywright({
5436
5656
  cdpUrl: this.cdpUrl,
5437
- targetId: this.targetId,
5657
+ targetId: this._targetId,
5438
5658
  ref,
5439
5659
  text,
5440
5660
  submit: opts?.submit,
@@ -5451,7 +5671,7 @@ var CrawlPage = class {
5451
5671
  async hover(ref, opts) {
5452
5672
  return hoverViaPlaywright({
5453
5673
  cdpUrl: this.cdpUrl,
5454
- targetId: this.targetId,
5674
+ targetId: this._targetId,
5455
5675
  ref,
5456
5676
  timeoutMs: opts?.timeoutMs
5457
5677
  });
@@ -5471,7 +5691,7 @@ var CrawlPage = class {
5471
5691
  async select(ref, ...values) {
5472
5692
  return selectOptionViaPlaywright({
5473
5693
  cdpUrl: this.cdpUrl,
5474
- targetId: this.targetId,
5694
+ targetId: this._targetId,
5475
5695
  ref,
5476
5696
  values
5477
5697
  });
@@ -5486,7 +5706,7 @@ var CrawlPage = class {
5486
5706
  async drag(startRef, endRef, opts) {
5487
5707
  return dragViaPlaywright({
5488
5708
  cdpUrl: this.cdpUrl,
5489
- targetId: this.targetId,
5709
+ targetId: this._targetId,
5490
5710
  startRef,
5491
5711
  endRef,
5492
5712
  timeoutMs: opts?.timeoutMs
@@ -5511,7 +5731,7 @@ var CrawlPage = class {
5511
5731
  async fill(fields) {
5512
5732
  return fillFormViaPlaywright({
5513
5733
  cdpUrl: this.cdpUrl,
5514
- targetId: this.targetId,
5734
+ targetId: this._targetId,
5515
5735
  fields
5516
5736
  });
5517
5737
  }
@@ -5524,7 +5744,7 @@ var CrawlPage = class {
5524
5744
  async scrollIntoView(ref, opts) {
5525
5745
  return scrollIntoViewViaPlaywright({
5526
5746
  cdpUrl: this.cdpUrl,
5527
- targetId: this.targetId,
5747
+ targetId: this._targetId,
5528
5748
  ref,
5529
5749
  timeoutMs: opts?.timeoutMs
5530
5750
  });
@@ -5537,7 +5757,7 @@ var CrawlPage = class {
5537
5757
  async highlight(ref) {
5538
5758
  return highlightViaPlaywright({
5539
5759
  cdpUrl: this.cdpUrl,
5540
- targetId: this.targetId,
5760
+ targetId: this._targetId,
5541
5761
  ref
5542
5762
  });
5543
5763
  }
@@ -5550,7 +5770,7 @@ var CrawlPage = class {
5550
5770
  async uploadFile(ref, paths) {
5551
5771
  return setInputFilesViaPlaywright({
5552
5772
  cdpUrl: this.cdpUrl,
5553
- targetId: this.targetId,
5773
+ targetId: this._targetId,
5554
5774
  ref,
5555
5775
  paths
5556
5776
  });
@@ -5573,7 +5793,7 @@ var CrawlPage = class {
5573
5793
  async armDialog(opts) {
5574
5794
  return armDialogViaPlaywright({
5575
5795
  cdpUrl: this.cdpUrl,
5576
- targetId: this.targetId,
5796
+ targetId: this._targetId,
5577
5797
  accept: opts.accept,
5578
5798
  promptText: opts.promptText,
5579
5799
  timeoutMs: opts.timeoutMs
@@ -5617,7 +5837,7 @@ var CrawlPage = class {
5617
5837
  async onDialog(handler) {
5618
5838
  return setDialogHandler({
5619
5839
  cdpUrl: this.cdpUrl,
5620
- targetId: this.targetId,
5840
+ targetId: this._targetId,
5621
5841
  handler: handler ?? void 0
5622
5842
  });
5623
5843
  }
@@ -5639,7 +5859,7 @@ var CrawlPage = class {
5639
5859
  async armFileUpload(paths, opts) {
5640
5860
  return armFileUploadViaPlaywright({
5641
5861
  cdpUrl: this.cdpUrl,
5642
- targetId: this.targetId,
5862
+ targetId: this._targetId,
5643
5863
  paths,
5644
5864
  timeoutMs: opts?.timeoutMs
5645
5865
  });
@@ -5654,7 +5874,7 @@ var CrawlPage = class {
5654
5874
  async batch(actions, opts) {
5655
5875
  return batchViaPlaywright({
5656
5876
  cdpUrl: this.cdpUrl,
5657
- targetId: this.targetId,
5877
+ targetId: this._targetId,
5658
5878
  actions,
5659
5879
  stopOnError: opts?.stopOnError,
5660
5880
  evaluateEnabled: opts?.evaluateEnabled
@@ -5679,7 +5899,7 @@ var CrawlPage = class {
5679
5899
  async press(key, opts) {
5680
5900
  return pressKeyViaPlaywright({
5681
5901
  cdpUrl: this.cdpUrl,
5682
- targetId: this.targetId,
5902
+ targetId: this._targetId,
5683
5903
  key,
5684
5904
  delayMs: opts?.delayMs
5685
5905
  });
@@ -5689,14 +5909,14 @@ var CrawlPage = class {
5689
5909
  * Get the current URL of the page.
5690
5910
  */
5691
5911
  async url() {
5692
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5912
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5693
5913
  return page.url();
5694
5914
  }
5695
5915
  /**
5696
5916
  * Get the page title.
5697
5917
  */
5698
5918
  async title() {
5699
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5919
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5700
5920
  return page.title();
5701
5921
  }
5702
5922
  /**
@@ -5709,7 +5929,7 @@ var CrawlPage = class {
5709
5929
  async goto(url, opts) {
5710
5930
  return navigateViaPlaywright({
5711
5931
  cdpUrl: this.cdpUrl,
5712
- targetId: this.targetId,
5932
+ targetId: this._targetId,
5713
5933
  url,
5714
5934
  timeoutMs: opts?.timeoutMs,
5715
5935
  ssrfPolicy: this.ssrfPolicy
@@ -5721,7 +5941,7 @@ var CrawlPage = class {
5721
5941
  * @param opts - Timeout options
5722
5942
  */
5723
5943
  async reload(opts) {
5724
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5944
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5725
5945
  ensurePageState(page);
5726
5946
  await page.reload({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5727
5947
  }
@@ -5731,7 +5951,7 @@ var CrawlPage = class {
5731
5951
  * @param opts - Timeout options
5732
5952
  */
5733
5953
  async goBack(opts) {
5734
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5954
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5735
5955
  ensurePageState(page);
5736
5956
  await page.goBack({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5737
5957
  }
@@ -5741,7 +5961,7 @@ var CrawlPage = class {
5741
5961
  * @param opts - Timeout options
5742
5962
  */
5743
5963
  async goForward(opts) {
5744
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5964
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5745
5965
  ensurePageState(page);
5746
5966
  await page.goForward({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5747
5967
  }
@@ -5764,7 +5984,7 @@ var CrawlPage = class {
5764
5984
  async waitFor(opts) {
5765
5985
  return waitForViaPlaywright({
5766
5986
  cdpUrl: this.cdpUrl,
5767
- targetId: this.targetId,
5987
+ targetId: this._targetId,
5768
5988
  ...opts
5769
5989
  });
5770
5990
  }
@@ -5789,7 +6009,7 @@ var CrawlPage = class {
5789
6009
  async evaluate(fn, opts) {
5790
6010
  return evaluateViaPlaywright({
5791
6011
  cdpUrl: this.cdpUrl,
5792
- targetId: this.targetId,
6012
+ targetId: this._targetId,
5793
6013
  fn,
5794
6014
  ref: opts?.ref,
5795
6015
  timeoutMs: opts?.timeoutMs,
@@ -5816,7 +6036,7 @@ var CrawlPage = class {
5816
6036
  async evaluateInAllFrames(fn) {
5817
6037
  return evaluateInAllFramesViaPlaywright({
5818
6038
  cdpUrl: this.cdpUrl,
5819
- targetId: this.targetId,
6039
+ targetId: this._targetId,
5820
6040
  fn
5821
6041
  });
5822
6042
  }
@@ -5837,7 +6057,7 @@ var CrawlPage = class {
5837
6057
  async screenshot(opts) {
5838
6058
  const result = await takeScreenshotViaPlaywright({
5839
6059
  cdpUrl: this.cdpUrl,
5840
- targetId: this.targetId,
6060
+ targetId: this._targetId,
5841
6061
  fullPage: opts?.fullPage,
5842
6062
  ref: opts?.ref,
5843
6063
  element: opts?.element,
@@ -5853,7 +6073,7 @@ var CrawlPage = class {
5853
6073
  * @returns PDF document as a Buffer
5854
6074
  */
5855
6075
  async pdf() {
5856
- const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6076
+ const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5857
6077
  return result.buffer;
5858
6078
  }
5859
6079
  /**
@@ -5874,7 +6094,7 @@ var CrawlPage = class {
5874
6094
  async screenshotWithLabels(refs, opts) {
5875
6095
  return screenshotWithLabelsViaPlaywright({
5876
6096
  cdpUrl: this.cdpUrl,
5877
- targetId: this.targetId,
6097
+ targetId: this._targetId,
5878
6098
  refs,
5879
6099
  maxLabels: opts?.maxLabels,
5880
6100
  type: opts?.type
@@ -5891,7 +6111,7 @@ var CrawlPage = class {
5891
6111
  async traceStart(opts) {
5892
6112
  return traceStartViaPlaywright({
5893
6113
  cdpUrl: this.cdpUrl,
5894
- targetId: this.targetId,
6114
+ targetId: this._targetId,
5895
6115
  screenshots: opts?.screenshots,
5896
6116
  snapshots: opts?.snapshots,
5897
6117
  sources: opts?.sources
@@ -5906,7 +6126,7 @@ var CrawlPage = class {
5906
6126
  async traceStop(path2, opts) {
5907
6127
  return traceStopViaPlaywright({
5908
6128
  cdpUrl: this.cdpUrl,
5909
- targetId: this.targetId,
6129
+ targetId: this._targetId,
5910
6130
  path: path2,
5911
6131
  allowedOutputRoots: opts?.allowedOutputRoots
5912
6132
  });
@@ -5927,7 +6147,7 @@ var CrawlPage = class {
5927
6147
  async responseBody(url, opts) {
5928
6148
  return responseBodyViaPlaywright({
5929
6149
  cdpUrl: this.cdpUrl,
5930
- targetId: this.targetId,
6150
+ targetId: this._targetId,
5931
6151
  url,
5932
6152
  timeoutMs: opts?.timeoutMs,
5933
6153
  maxChars: opts?.maxChars
@@ -5955,7 +6175,7 @@ var CrawlPage = class {
5955
6175
  async waitForRequest(url, opts) {
5956
6176
  return waitForRequestViaPlaywright({
5957
6177
  cdpUrl: this.cdpUrl,
5958
- targetId: this.targetId,
6178
+ targetId: this._targetId,
5959
6179
  url,
5960
6180
  method: opts?.method,
5961
6181
  timeoutMs: opts?.timeoutMs,
@@ -5973,7 +6193,7 @@ var CrawlPage = class {
5973
6193
  async consoleLogs(opts) {
5974
6194
  return getConsoleMessagesViaPlaywright({
5975
6195
  cdpUrl: this.cdpUrl,
5976
- targetId: this.targetId,
6196
+ targetId: this._targetId,
5977
6197
  level: opts?.level,
5978
6198
  clear: opts?.clear
5979
6199
  });
@@ -5987,7 +6207,7 @@ var CrawlPage = class {
5987
6207
  async pageErrors(opts) {
5988
6208
  const result = await getPageErrorsViaPlaywright({
5989
6209
  cdpUrl: this.cdpUrl,
5990
- targetId: this.targetId,
6210
+ targetId: this._targetId,
5991
6211
  clear: opts?.clear
5992
6212
  });
5993
6213
  return result.errors;
@@ -6008,7 +6228,7 @@ var CrawlPage = class {
6008
6228
  async networkRequests(opts) {
6009
6229
  const result = await getNetworkRequestsViaPlaywright({
6010
6230
  cdpUrl: this.cdpUrl,
6011
- targetId: this.targetId,
6231
+ targetId: this._targetId,
6012
6232
  filter: opts?.filter,
6013
6233
  clear: opts?.clear
6014
6234
  });
@@ -6024,7 +6244,7 @@ var CrawlPage = class {
6024
6244
  async resize(width, height) {
6025
6245
  return resizeViewportViaPlaywright({
6026
6246
  cdpUrl: this.cdpUrl,
6027
- targetId: this.targetId,
6247
+ targetId: this._targetId,
6028
6248
  width,
6029
6249
  height
6030
6250
  });
@@ -6036,7 +6256,7 @@ var CrawlPage = class {
6036
6256
  * @returns Array of cookie objects
6037
6257
  */
6038
6258
  async cookies() {
6039
- const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6259
+ const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6040
6260
  return result.cookies;
6041
6261
  }
6042
6262
  /**
@@ -6054,11 +6274,11 @@ var CrawlPage = class {
6054
6274
  * ```
6055
6275
  */
6056
6276
  async setCookie(cookie) {
6057
- return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
6277
+ return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId, cookie });
6058
6278
  }
6059
6279
  /** Clear all cookies in the browser context. */
6060
6280
  async clearCookies() {
6061
- return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6281
+ return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6062
6282
  }
6063
6283
  /**
6064
6284
  * Get values from localStorage or sessionStorage.
@@ -6070,7 +6290,7 @@ var CrawlPage = class {
6070
6290
  async storageGet(kind, key) {
6071
6291
  const result = await storageGetViaPlaywright({
6072
6292
  cdpUrl: this.cdpUrl,
6073
- targetId: this.targetId,
6293
+ targetId: this._targetId,
6074
6294
  kind,
6075
6295
  key
6076
6296
  });
@@ -6086,7 +6306,7 @@ var CrawlPage = class {
6086
6306
  async storageSet(kind, key, value) {
6087
6307
  return storageSetViaPlaywright({
6088
6308
  cdpUrl: this.cdpUrl,
6089
- targetId: this.targetId,
6309
+ targetId: this._targetId,
6090
6310
  kind,
6091
6311
  key,
6092
6312
  value
@@ -6100,7 +6320,7 @@ var CrawlPage = class {
6100
6320
  async storageClear(kind) {
6101
6321
  return storageClearViaPlaywright({
6102
6322
  cdpUrl: this.cdpUrl,
6103
- targetId: this.targetId,
6323
+ targetId: this._targetId,
6104
6324
  kind
6105
6325
  });
6106
6326
  }
@@ -6122,7 +6342,7 @@ var CrawlPage = class {
6122
6342
  async download(ref, path2, opts) {
6123
6343
  return downloadViaPlaywright({
6124
6344
  cdpUrl: this.cdpUrl,
6125
- targetId: this.targetId,
6345
+ targetId: this._targetId,
6126
6346
  ref,
6127
6347
  path: path2,
6128
6348
  timeoutMs: opts?.timeoutMs,
@@ -6140,7 +6360,7 @@ var CrawlPage = class {
6140
6360
  async waitForDownload(opts) {
6141
6361
  return waitForDownloadViaPlaywright({
6142
6362
  cdpUrl: this.cdpUrl,
6143
- targetId: this.targetId,
6363
+ targetId: this._targetId,
6144
6364
  path: opts?.path,
6145
6365
  timeoutMs: opts?.timeoutMs,
6146
6366
  allowedOutputRoots: opts?.allowedOutputRoots
@@ -6155,7 +6375,7 @@ var CrawlPage = class {
6155
6375
  async setOffline(offline) {
6156
6376
  return setOfflineViaPlaywright({
6157
6377
  cdpUrl: this.cdpUrl,
6158
- targetId: this.targetId,
6378
+ targetId: this._targetId,
6159
6379
  offline
6160
6380
  });
6161
6381
  }
@@ -6172,7 +6392,7 @@ var CrawlPage = class {
6172
6392
  async setExtraHeaders(headers) {
6173
6393
  return setExtraHTTPHeadersViaPlaywright({
6174
6394
  cdpUrl: this.cdpUrl,
6175
- targetId: this.targetId,
6395
+ targetId: this._targetId,
6176
6396
  headers
6177
6397
  });
6178
6398
  }
@@ -6184,7 +6404,7 @@ var CrawlPage = class {
6184
6404
  async setHttpCredentials(opts) {
6185
6405
  return setHttpCredentialsViaPlaywright({
6186
6406
  cdpUrl: this.cdpUrl,
6187
- targetId: this.targetId,
6407
+ targetId: this._targetId,
6188
6408
  username: opts.username,
6189
6409
  password: opts.password,
6190
6410
  clear: opts.clear
@@ -6204,7 +6424,7 @@ var CrawlPage = class {
6204
6424
  async setGeolocation(opts) {
6205
6425
  return setGeolocationViaPlaywright({
6206
6426
  cdpUrl: this.cdpUrl,
6207
- targetId: this.targetId,
6427
+ targetId: this._targetId,
6208
6428
  latitude: opts.latitude,
6209
6429
  longitude: opts.longitude,
6210
6430
  accuracy: opts.accuracy,
@@ -6225,7 +6445,7 @@ var CrawlPage = class {
6225
6445
  async emulateMedia(opts) {
6226
6446
  return emulateMediaViaPlaywright({
6227
6447
  cdpUrl: this.cdpUrl,
6228
- targetId: this.targetId,
6448
+ targetId: this._targetId,
6229
6449
  colorScheme: opts.colorScheme
6230
6450
  });
6231
6451
  }
@@ -6237,7 +6457,7 @@ var CrawlPage = class {
6237
6457
  async setLocale(locale) {
6238
6458
  return setLocaleViaPlaywright({
6239
6459
  cdpUrl: this.cdpUrl,
6240
- targetId: this.targetId,
6460
+ targetId: this._targetId,
6241
6461
  locale
6242
6462
  });
6243
6463
  }
@@ -6249,7 +6469,7 @@ var CrawlPage = class {
6249
6469
  async setTimezone(timezoneId) {
6250
6470
  return setTimezoneViaPlaywright({
6251
6471
  cdpUrl: this.cdpUrl,
6252
- targetId: this.targetId,
6472
+ targetId: this._targetId,
6253
6473
  timezoneId
6254
6474
  });
6255
6475
  }
@@ -6266,7 +6486,7 @@ var CrawlPage = class {
6266
6486
  async setDevice(name) {
6267
6487
  return setDeviceViaPlaywright({
6268
6488
  cdpUrl: this.cdpUrl,
6269
- targetId: this.targetId,
6489
+ targetId: this._targetId,
6270
6490
  name
6271
6491
  });
6272
6492
  }
@@ -6287,7 +6507,7 @@ var CrawlPage = class {
6287
6507
  * ```
6288
6508
  */
6289
6509
  async detectChallenge() {
6290
- return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6510
+ return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6291
6511
  }
6292
6512
  /**
6293
6513
  * Wait for an anti-bot challenge to resolve on its own.
@@ -6312,7 +6532,7 @@ var CrawlPage = class {
6312
6532
  async waitForChallenge(opts) {
6313
6533
  return waitForChallengeViaPlaywright({
6314
6534
  cdpUrl: this.cdpUrl,
6315
- targetId: this.targetId,
6535
+ targetId: this._targetId,
6316
6536
  timeoutMs: opts?.timeoutMs,
6317
6537
  pollMs: opts?.pollMs
6318
6538
  });
@@ -6349,8 +6569,24 @@ var CrawlPage = class {
6349
6569
  */
6350
6570
  async isAuthenticated(rules) {
6351
6571
  if (!rules.length) return { authenticated: true, checks: [] };
6352
- const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6572
+ const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6353
6573
  const checks = [];
6574
+ const needsBodyText = rules.some((r) => r.text !== void 0 || r.textGone !== void 0);
6575
+ let bodyText = null;
6576
+ if (needsBodyText) {
6577
+ try {
6578
+ const raw = await evaluateViaPlaywright({
6579
+ cdpUrl: this.cdpUrl,
6580
+ targetId: this._targetId,
6581
+ fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6582
+ });
6583
+ bodyText = typeof raw === "string" ? raw : null;
6584
+ } catch (err) {
6585
+ console.warn(
6586
+ `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6587
+ );
6588
+ }
6589
+ }
6354
6590
  for (const rule of rules) {
6355
6591
  if (rule.url !== void 0) {
6356
6592
  const currentUrl = page.url();
@@ -6383,19 +6619,6 @@ var CrawlPage = class {
6383
6619
  }
6384
6620
  }
6385
6621
  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
6622
  if (rule.text !== void 0) {
6400
6623
  if (bodyText === null) {
6401
6624
  checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
@@ -6425,7 +6648,7 @@ var CrawlPage = class {
6425
6648
  try {
6426
6649
  const result = await evaluateViaPlaywright({
6427
6650
  cdpUrl: this.cdpUrl,
6428
- targetId: this.targetId,
6651
+ targetId: this._targetId,
6429
6652
  fn: rule.fn
6430
6653
  });
6431
6654
  const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
@@ -6474,7 +6697,7 @@ var CrawlPage = class {
6474
6697
  * ```
6475
6698
  */
6476
6699
  async playwrightPage() {
6477
- return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6700
+ return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6478
6701
  }
6479
6702
  /**
6480
6703
  * Create a Playwright `Locator` for a CSS selector on this page.
@@ -6497,7 +6720,7 @@ var CrawlPage = class {
6497
6720
  * ```
6498
6721
  */
6499
6722
  async locator(selector) {
6500
- const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6723
+ const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6501
6724
  return pwPage.locator(selector);
6502
6725
  }
6503
6726
  };
@@ -6541,21 +6764,27 @@ var BrowserClaw = class _BrowserClaw {
6541
6764
  static async launch(opts = {}) {
6542
6765
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6543
6766
  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();
6767
+ try {
6768
+ const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6769
+ const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6770
+ const telemetry = {
6771
+ launchMs: chrome.launchMs,
6772
+ timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6773
+ };
6774
+ const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6775
+ if (opts.url !== void 0 && opts.url !== "") {
6776
+ const page = await browser.currentPage();
6777
+ const navT0 = Date.now();
6778
+ await page.goto(opts.url);
6779
+ telemetry.navMs = Date.now() - navT0;
6780
+ telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6781
+ }
6782
+ return browser;
6783
+ } catch (err) {
6784
+ await stopChrome(chrome).catch(() => {
6785
+ });
6786
+ throw err;
6557
6787
  }
6558
- return browser;
6559
6788
  }
6560
6789
  /**
6561
6790
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -6712,7 +6941,7 @@ var BrowserClaw = class _BrowserClaw {
6712
6941
  if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
6713
6942
  try {
6714
6943
  clearRecordingContext(this.cdpUrl);
6715
- await disconnectBrowser();
6944
+ await closePlaywrightBrowserConnection({ cdpUrl: this.cdpUrl });
6716
6945
  if (this.chrome) {
6717
6946
  await stopChrome(this.chrome);
6718
6947
  this.chrome = null;