browserclaw 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,12 +2670,14 @@ 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
  );
2568
2676
  }
2569
2677
  if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
2678
+ const foundTargetId = await pageTargetId(found).catch(() => null);
2679
+ if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
2680
+ throw new BlockedBrowserTargetError();
2570
2681
  return found;
2571
2682
  }
2572
2683
  async function resolvePageByTargetIdOrThrow(opts) {
@@ -2578,7 +2689,6 @@ async function resolvePageByTargetIdOrThrow(opts) {
2578
2689
  async function getRestoredPageForTarget(opts) {
2579
2690
  const page = await getPageForTargetId(opts);
2580
2691
  ensurePageState(page);
2581
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2582
2692
  return page;
2583
2693
  }
2584
2694
 
@@ -2632,10 +2742,11 @@ var BROWSER_EVALUATOR = new Function(
2632
2742
  " catch (_) { candidate = (0, eval)(fnBody); }",
2633
2743
  ' var result = typeof candidate === "function" ? candidate() : candidate;',
2634
2744
  ' if (result && typeof result.then === "function") {',
2745
+ " var tid;",
2635
2746
  " return Promise.race([",
2636
- " result,",
2747
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2637
2748
  " new Promise(function(_, reject) {",
2638
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2749
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2639
2750
  " })",
2640
2751
  " ]);",
2641
2752
  " }",
@@ -2657,10 +2768,11 @@ var ELEMENT_EVALUATOR = new Function(
2657
2768
  " catch (_) { candidate = (0, eval)(fnBody); }",
2658
2769
  ' var result = typeof candidate === "function" ? candidate(el) : candidate;',
2659
2770
  ' if (result && typeof result.then === "function") {',
2771
+ " var tid;",
2660
2772
  " return Promise.race([",
2661
- " result,",
2773
+ " result.then(function(v) { clearTimeout(tid); return v; }, function(e) { clearTimeout(tid); throw e; }),",
2662
2774
  " new Promise(function(_, reject) {",
2663
- ' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2775
+ ' tid = setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
2664
2776
  " })",
2665
2777
  " ]);",
2666
2778
  " }",
@@ -2675,10 +2787,8 @@ async function evaluateViaPlaywright(opts) {
2675
2787
  if (!fnText) throw new Error("function is required");
2676
2788
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2677
2789
  ensurePageState(page);
2678
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2679
2790
  const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2680
- let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
2681
- evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
2791
+ const evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 1e3));
2682
2792
  const signal = opts.signal;
2683
2793
  let abortListener;
2684
2794
  let abortReject;
@@ -2692,10 +2802,16 @@ async function evaluateViaPlaywright(opts) {
2692
2802
  }
2693
2803
  if (signal !== void 0) {
2694
2804
  const disconnect = () => {
2695
- forceDisconnectPlaywrightConnection({
2696
- cdpUrl: opts.cdpUrl,
2697
- targetId: opts.targetId}).catch(() => {
2698
- });
2805
+ const targetId = opts.targetId?.trim() ?? "";
2806
+ if (targetId !== "") {
2807
+ tryTerminateExecutionViaCdp(opts.cdpUrl, targetId).catch(() => {
2808
+ });
2809
+ } else {
2810
+ console.warn("[browserclaw] evaluate abort: no targetId, forcing full disconnect");
2811
+ forceDisconnectPlaywrightConnection({
2812
+ cdpUrl: opts.cdpUrl}).catch(() => {
2813
+ });
2814
+ }
2699
2815
  };
2700
2816
  if (signal.aborted) {
2701
2817
  disconnect();
@@ -2731,6 +2847,8 @@ async function evaluateViaPlaywright(opts) {
2731
2847
  );
2732
2848
  } finally {
2733
2849
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
2850
+ abortReject = void 0;
2851
+ abortListener = void 0;
2734
2852
  }
2735
2853
  }
2736
2854
 
@@ -2923,7 +3041,7 @@ function isBlockedHostnameOrIp(hostname, policy) {
2923
3041
  function isPrivateIpAddress(address, policy) {
2924
3042
  let normalized = address.trim().toLowerCase();
2925
3043
  if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
2926
- if (!normalized) return false;
3044
+ if (!normalized) return true;
2927
3045
  const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
2928
3046
  const strictIp = parseCanonicalIpAddress(normalized);
2929
3047
  if (strictIp) {
@@ -3010,6 +3128,25 @@ function createPinnedLookup(params) {
3010
3128
  cb(null, chosen.address, chosen.family);
3011
3129
  });
3012
3130
  }
3131
+ var DNS_CACHE_TTL_MS = 3e4;
3132
+ var MAX_DNS_CACHE_SIZE = 100;
3133
+ var dnsCache = /* @__PURE__ */ new Map();
3134
+ function getCachedDnsResult(hostname) {
3135
+ const entry = dnsCache.get(hostname);
3136
+ if (!entry) return void 0;
3137
+ if (Date.now() > entry.expiresAt) {
3138
+ dnsCache.delete(hostname);
3139
+ return void 0;
3140
+ }
3141
+ return entry.result;
3142
+ }
3143
+ function cacheDnsResult(hostname, result) {
3144
+ dnsCache.set(hostname, { result, expiresAt: Date.now() + DNS_CACHE_TTL_MS });
3145
+ if (dnsCache.size > MAX_DNS_CACHE_SIZE) {
3146
+ const first = dnsCache.keys().next();
3147
+ if (first.done !== true) dnsCache.delete(first.value);
3148
+ }
3149
+ }
3013
3150
  async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3014
3151
  const normalized = normalizeHostname(hostname);
3015
3152
  if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
@@ -3028,6 +3165,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3028
3165
  );
3029
3166
  }
3030
3167
  }
3168
+ const cached = getCachedDnsResult(normalized);
3169
+ if (cached) return cached;
3031
3170
  const lookupFn = params.lookupFn ?? promises.lookup;
3032
3171
  let results;
3033
3172
  try {
@@ -3057,11 +3196,13 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
3057
3196
  `Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
3058
3197
  );
3059
3198
  }
3060
- return {
3199
+ const pinned = {
3061
3200
  hostname: normalized,
3062
3201
  addresses,
3063
3202
  lookup: createPinnedLookup({ hostname: normalized, addresses })
3064
3203
  };
3204
+ cacheDnsResult(normalized, pinned);
3205
+ return pinned;
3065
3206
  }
3066
3207
  async function assertBrowserNavigationAllowed(opts) {
3067
3208
  const rawUrl = opts.url.trim();
@@ -3215,13 +3356,27 @@ function buildSiblingTempPath(targetPath) {
3215
3356
  return path.join(path.dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
3216
3357
  }
3217
3358
  async function writeViaSiblingTempPath(params) {
3218
- const rootDir = await promises$1.realpath(path.resolve(params.rootDir)).catch(() => path.resolve(params.rootDir));
3359
+ let rootDir;
3360
+ try {
3361
+ rootDir = await promises$1.realpath(path.resolve(params.rootDir));
3362
+ } catch {
3363
+ console.warn(`[browserclaw] writeViaSiblingTempPath: rootDir realpath failed, using lexical resolve`);
3364
+ rootDir = path.resolve(params.rootDir);
3365
+ }
3219
3366
  const requestedTargetPath = path.resolve(params.targetPath);
3220
3367
  const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
3221
3368
  const relativeTargetPath = path.relative(rootDir, targetPath);
3222
3369
  if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTargetPath)) {
3223
3370
  throw new Error("Target path is outside the allowed root");
3224
3371
  }
3372
+ try {
3373
+ const stat = await promises$1.lstat(targetPath);
3374
+ if (stat.isSymbolicLink()) {
3375
+ throw new Error(`Unsafe output path: "${params.targetPath}" is a symbolic link.`);
3376
+ }
3377
+ } catch (e) {
3378
+ if (e.code !== "ENOENT") throw e;
3379
+ }
3225
3380
  const tempPath = buildSiblingTempPath(targetPath);
3226
3381
  let renameSucceeded = false;
3227
3382
  try {
@@ -3243,6 +3398,9 @@ async function assertBrowserNavigationResultAllowed(opts) {
3243
3398
  } catch {
3244
3399
  return;
3245
3400
  }
3401
+ if (parsed.protocol === "data:" || parsed.protocol === "blob:") {
3402
+ throw new InvalidBrowserNavigationUrlError(`Navigation result blocked: "${parsed.protocol}" URLs are not allowed.`);
3403
+ }
3246
3404
  if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) {
3247
3405
  await assertBrowserNavigationAllowed(opts);
3248
3406
  }
@@ -3360,9 +3518,10 @@ async function clickViaPlaywright(opts) {
3360
3518
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3361
3519
  const POLL_INTERVAL_MS = 50;
3362
3520
  const POLL_TIMEOUT_MS = 500;
3521
+ const ATTR_TIMEOUT_MS = Math.min(timeout, POLL_TIMEOUT_MS);
3363
3522
  let changed = false;
3364
3523
  for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
3365
- const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3524
+ const current = await locator.getAttribute("aria-checked", { timeout: ATTR_TIMEOUT_MS }).catch(() => void 0);
3366
3525
  if (current === void 0 || current !== ariaCheckedBefore) {
3367
3526
  changed = true;
3368
3527
  break;
@@ -3439,6 +3598,7 @@ async function dragViaPlaywright(opts) {
3439
3598
  async function fillFormViaPlaywright(opts) {
3440
3599
  const page = await getRestoredPageForTarget(opts);
3441
3600
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3601
+ let filledCount = 0;
3442
3602
  for (const field of opts.fields) {
3443
3603
  const ref = field.ref.trim();
3444
3604
  const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
@@ -3457,16 +3617,24 @@ async function fillFormViaPlaywright(opts) {
3457
3617
  try {
3458
3618
  await setCheckedViaEvaluate(locator, checked);
3459
3619
  } catch (err) {
3460
- throw toAIFriendlyError(err, ref);
3620
+ const friendly = toAIFriendlyError(err, ref);
3621
+ throw new Error(
3622
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3623
+ );
3461
3624
  }
3462
3625
  }
3626
+ filledCount += 1;
3463
3627
  continue;
3464
3628
  }
3465
3629
  try {
3466
3630
  await locator.fill(value, { timeout });
3467
3631
  } catch (err) {
3468
- throw toAIFriendlyError(err, ref);
3632
+ const friendly = toAIFriendlyError(err, ref);
3633
+ throw new Error(
3634
+ `Failed at field "${ref}" (${String(filledCount)}/${String(opts.fields.length)} filled): ${friendly.message}`
3635
+ );
3469
3636
  }
3637
+ filledCount += 1;
3470
3638
  }
3471
3639
  }
3472
3640
  async function scrollIntoViewViaPlaywright(opts) {
@@ -3532,16 +3700,22 @@ async function armDialogViaPlaywright(opts) {
3532
3700
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3533
3701
  state.armIdDialog = bumpDialogArmId(state);
3534
3702
  const armId = state.armIdDialog;
3703
+ const resetArm = () => {
3704
+ if (state.armIdDialog === armId) state.armIdDialog = 0;
3705
+ };
3706
+ page.once("close", resetArm);
3535
3707
  page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
3536
3708
  if (state.armIdDialog !== armId) return;
3537
3709
  try {
3538
3710
  if (opts.accept) await dialog.accept(opts.promptText);
3539
3711
  else await dialog.dismiss();
3540
3712
  } finally {
3541
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3713
+ resetArm();
3714
+ page.off("close", resetArm);
3542
3715
  }
3543
3716
  }).catch(() => {
3544
- if (state.armIdDialog === armId) state.armIdDialog = 0;
3717
+ resetArm();
3718
+ page.off("close", resetArm);
3545
3719
  });
3546
3720
  }
3547
3721
  async function armFileUploadViaPlaywright(opts) {
@@ -3550,6 +3724,10 @@ async function armFileUploadViaPlaywright(opts) {
3550
3724
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3551
3725
  state.armIdUpload = bumpUploadArmId(state);
3552
3726
  const armId = state.armIdUpload;
3727
+ const resetArm = () => {
3728
+ if (state.armIdUpload === armId) state.armIdUpload = 0;
3729
+ };
3730
+ page.once("close", resetArm);
3553
3731
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3554
3732
  if (state.armIdUpload !== armId) return;
3555
3733
  if (opts.paths === void 0 || opts.paths.length === 0) {
@@ -3581,9 +3759,18 @@ async function armFileUploadViaPlaywright(opts) {
3581
3759
  el.dispatchEvent(new Event("change", { bubbles: true }));
3582
3760
  });
3583
3761
  }
3584
- } catch {
3762
+ } catch (e) {
3763
+ console.warn(
3764
+ `[browserclaw] armFileUpload: dispatch events failed: ${e instanceof Error ? e.message : String(e)}`
3765
+ );
3585
3766
  }
3586
- }).catch(() => {
3767
+ }).catch((e) => {
3768
+ console.warn(
3769
+ `[browserclaw] armFileUpload: filechooser wait failed: ${e instanceof Error ? e.message : String(e)}`
3770
+ );
3771
+ }).finally(() => {
3772
+ resetArm();
3773
+ page.off("close", resetArm);
3587
3774
  });
3588
3775
  }
3589
3776
 
@@ -3603,7 +3790,7 @@ function clearRecordingContext(cdpUrl) {
3603
3790
  }
3604
3791
  async function createRecordingContext(browser, cdpUrl, recordVideo) {
3605
3792
  const context = await browser.newContext({ recordVideo });
3606
- observeContext(context);
3793
+ await observeContext(context);
3607
3794
  recordingContexts.set(cdpUrl, context);
3608
3795
  context.on("close", () => recordingContexts.delete(cdpUrl));
3609
3796
  return context;
@@ -3658,6 +3845,11 @@ async function gotoPageWithNavigationGuard(opts) {
3658
3845
  await route.continue();
3659
3846
  return;
3660
3847
  }
3848
+ const isRedirect = request.redirectedFrom() !== null;
3849
+ if (!isRedirect && request.url() !== opts.url) {
3850
+ await route.continue();
3851
+ return;
3852
+ }
3661
3853
  try {
3662
3854
  await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
3663
3855
  } catch (err) {
@@ -3709,6 +3901,7 @@ async function navigateViaPlaywright(opts) {
3709
3901
  response = await navigate();
3710
3902
  } catch (err) {
3711
3903
  if (!isRetryableNavigateError(err)) throw err;
3904
+ recordingContexts.delete(opts.cdpUrl);
3712
3905
  await forceDisconnectPlaywrightConnection({
3713
3906
  cdpUrl: opts.cdpUrl,
3714
3907
  targetId: opts.targetId}).catch(() => {
@@ -3768,6 +3961,7 @@ async function createPageViaPlaywright(opts) {
3768
3961
  });
3769
3962
  } catch (err) {
3770
3963
  if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
3964
+ console.warn(`[browserclaw] createPage navigation failed: ${err instanceof Error ? err.message : String(err)}`);
3771
3965
  }
3772
3966
  await assertPageNavigationCompletedSafely({
3773
3967
  cdpUrl: opts.cdpUrl,
@@ -3854,39 +4048,46 @@ var MAX_WAIT_TIME_MS = 3e4;
3854
4048
  async function waitForViaPlaywright(opts) {
3855
4049
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3856
4050
  ensurePageState(page);
3857
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4051
+ const totalTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
4052
+ const deadline = Date.now() + totalTimeout;
4053
+ const remaining = () => Math.max(500, deadline - Date.now());
3858
4054
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3859
4055
  await page.waitForTimeout(resolveBoundedDelayMs(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3860
4056
  }
3861
4057
  if (opts.text !== void 0 && opts.text !== "") {
3862
- await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
4058
+ await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, {
4059
+ timeout: remaining()
4060
+ });
3863
4061
  }
3864
4062
  if (opts.textGone !== void 0 && opts.textGone !== "") {
3865
- await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, { timeout });
4063
+ await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, {
4064
+ timeout: remaining()
4065
+ });
3866
4066
  }
3867
4067
  if (opts.selector !== void 0 && opts.selector !== "") {
3868
4068
  const selector = opts.selector.trim();
3869
- if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
4069
+ if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout: remaining() });
3870
4070
  }
3871
4071
  if (opts.url !== void 0 && opts.url !== "") {
3872
4072
  const url = opts.url.trim();
3873
- if (url !== "") await page.waitForURL(url, { timeout });
4073
+ if (url !== "") await page.waitForURL(url, { timeout: remaining() });
3874
4074
  }
3875
4075
  if (opts.loadState !== void 0) {
3876
- await page.waitForLoadState(opts.loadState, { timeout });
4076
+ await page.waitForLoadState(opts.loadState, { timeout: remaining() });
3877
4077
  }
3878
4078
  if (opts.fn !== void 0) {
3879
4079
  if (typeof opts.fn === "function") {
3880
- await page.waitForFunction(opts.fn, opts.arg, { timeout });
4080
+ await page.waitForFunction(opts.fn, opts.arg, { timeout: remaining() });
3881
4081
  } else {
3882
4082
  const fn = opts.fn.trim();
3883
- if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
4083
+ if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout: remaining() });
3884
4084
  }
3885
4085
  }
3886
4086
  }
3887
4087
 
3888
4088
  // src/actions/batch.ts
3889
4089
  var MAX_BATCH_DEPTH = 5;
4090
+ var MAX_BATCH_TIMEOUT_MS = 3e5;
3890
4091
  var MAX_BATCH_ACTIONS = 100;
3891
4092
  async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
3892
4093
  if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
@@ -4034,13 +4235,19 @@ async function batchViaPlaywright(opts) {
4034
4235
  throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
4035
4236
  const results = [];
4036
4237
  const evaluateEnabled = opts.evaluateEnabled !== false;
4238
+ const deadline = Date.now() + MAX_BATCH_TIMEOUT_MS;
4037
4239
  for (const action of opts.actions) {
4240
+ if (Date.now() > deadline) {
4241
+ results.push({ ok: false, error: "Batch timeout exceeded" });
4242
+ break;
4243
+ }
4038
4244
  try {
4039
4245
  await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
4040
4246
  results.push({ ok: true });
4041
4247
  } catch (err) {
4042
4248
  const message = err instanceof Error ? err.message : String(err);
4043
4249
  results.push({ ok: false, error: message });
4250
+ if (err instanceof BrowserTabNotFoundError || err instanceof BlockedBrowserTargetError) break;
4044
4251
  if (opts.stopOnError !== false) break;
4045
4252
  }
4046
4253
  }
@@ -4058,8 +4265,10 @@ function createPageDownloadWaiter(page, timeoutMs) {
4058
4265
  handler = void 0;
4059
4266
  }
4060
4267
  };
4268
+ let rejectPromise;
4061
4269
  return {
4062
4270
  promise: new Promise((resolve2, reject) => {
4271
+ rejectPromise = reject;
4063
4272
  handler = (download) => {
4064
4273
  if (done) return;
4065
4274
  done = true;
@@ -4078,6 +4287,7 @@ function createPageDownloadWaiter(page, timeoutMs) {
4078
4287
  if (done) return;
4079
4288
  done = true;
4080
4289
  cleanup();
4290
+ rejectPromise?.(new Error("Download waiter cancelled"));
4081
4291
  }
4082
4292
  };
4083
4293
  }
@@ -4109,12 +4319,11 @@ async function downloadViaPlaywright(opts) {
4109
4319
  await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
4110
4320
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4111
4321
  const state = ensurePageState(page);
4112
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4113
4322
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4114
4323
  const outPath = opts.path.trim();
4115
4324
  if (!outPath) throw new Error("path is required");
4116
- state.armIdDownload = bumpDownloadArmId(state);
4117
- const armId = state.armIdDownload;
4325
+ const armId = bumpDownloadArmId(state);
4326
+ state.armIdDownload = armId;
4118
4327
  const waiter = createPageDownloadWaiter(page, timeout);
4119
4328
  try {
4120
4329
  const locator = refLocator(page, opts.ref);
@@ -4161,12 +4370,6 @@ async function setDeviceViaPlaywright(opts) {
4161
4370
  }
4162
4371
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4163
4372
  ensurePageState(page);
4164
- if (device.viewport !== null) {
4165
- await page.setViewportSize({
4166
- width: device.viewport.width,
4167
- height: device.viewport.height
4168
- });
4169
- }
4170
4373
  await withPageScopedCdpClient({
4171
4374
  cdpUrl: opts.cdpUrl,
4172
4375
  page,
@@ -4194,11 +4397,24 @@ async function setDeviceViaPlaywright(opts) {
4194
4397
  }
4195
4398
  }
4196
4399
  });
4400
+ if (device.viewport !== null) {
4401
+ await page.setViewportSize({
4402
+ width: device.viewport.width,
4403
+ height: device.viewport.height
4404
+ });
4405
+ }
4197
4406
  }
4198
4407
  async function setExtraHTTPHeadersViaPlaywright(opts) {
4199
4408
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4200
4409
  ensurePageState(page);
4201
- await page.context().setExtraHTTPHeaders(opts.headers);
4410
+ await withPageScopedCdpClient({
4411
+ cdpUrl: opts.cdpUrl,
4412
+ page,
4413
+ targetId: opts.targetId,
4414
+ fn: async (send) => {
4415
+ await send("Network.setExtraHTTPHeaders", { headers: opts.headers });
4416
+ }
4417
+ });
4202
4418
  }
4203
4419
  async function setGeolocationViaPlaywright(opts) {
4204
4420
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -4206,7 +4422,8 @@ async function setGeolocationViaPlaywright(opts) {
4206
4422
  const context = page.context();
4207
4423
  if (opts.clear === true) {
4208
4424
  await context.setGeolocation(null);
4209
- await context.clearPermissions().catch(() => {
4425
+ await context.clearPermissions().catch((err) => {
4426
+ console.warn(`[browserclaw] clearPermissions failed: ${err instanceof Error ? err.message : String(err)}`);
4210
4427
  });
4211
4428
  return;
4212
4429
  }
@@ -4508,7 +4725,6 @@ async function waitForRequestViaPlaywright(opts) {
4508
4725
  async function takeScreenshotViaPlaywright(opts) {
4509
4726
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4510
4727
  ensurePageState(page);
4511
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4512
4728
  const type = opts.type ?? "png";
4513
4729
  if (opts.ref !== void 0 && opts.ref !== "") {
4514
4730
  if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
@@ -4523,7 +4739,6 @@ async function takeScreenshotViaPlaywright(opts) {
4523
4739
  async function screenshotWithLabelsViaPlaywright(opts) {
4524
4740
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4525
4741
  ensurePageState(page);
4526
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
4527
4742
  const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
4528
4743
  const type = opts.type ?? "png";
4529
4744
  const refs = opts.refs.slice(0, maxLabels);
@@ -4587,7 +4802,8 @@ async function screenshotWithLabelsViaPlaywright(opts) {
4587
4802
  document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
4588
4803
  el.remove();
4589
4804
  });
4590
- }).catch(() => {
4805
+ }).catch((err) => {
4806
+ console.warn(`[browserclaw] label overlay cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
4591
4807
  });
4592
4808
  }
4593
4809
  }
@@ -4615,17 +4831,14 @@ async function traceStopViaPlaywright(opts) {
4615
4831
  if (!ctxState.traceActive) {
4616
4832
  throw new Error("No active trace. Start a trace before stopping it.");
4617
4833
  }
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
- }
4834
+ ctxState.traceActive = false;
4835
+ await writeViaSiblingTempPath({
4836
+ rootDir: path.dirname(opts.path),
4837
+ targetPath: opts.path,
4838
+ writeTemp: async (tempPath) => {
4839
+ await context.tracing.stop({ path: tempPath });
4840
+ }
4841
+ });
4629
4842
  }
4630
4843
 
4631
4844
  // src/snapshot/ref-map.ts
@@ -5186,17 +5399,27 @@ async function storageClearViaPlaywright(opts) {
5186
5399
  // src/browser.ts
5187
5400
  var CrawlPage = class {
5188
5401
  cdpUrl;
5189
- targetId;
5402
+ _targetId;
5190
5403
  ssrfPolicy;
5191
5404
  /** @internal */
5192
5405
  constructor(cdpUrl, targetId, ssrfPolicy) {
5193
5406
  this.cdpUrl = cdpUrl;
5194
- this.targetId = targetId;
5407
+ this._targetId = targetId;
5195
5408
  this.ssrfPolicy = ssrfPolicy;
5196
5409
  }
5197
5410
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
5198
5411
  get id() {
5199
- return this.targetId;
5412
+ return this._targetId;
5413
+ }
5414
+ /**
5415
+ * Refresh the target ID by re-resolving the page from the browser.
5416
+ * Useful after reconnection when the old target ID may be stale.
5417
+ */
5418
+ async refreshTargetId() {
5419
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5420
+ const newId = await pageTargetId(page);
5421
+ if (newId !== null && newId !== "") this._targetId = newId;
5422
+ return this._targetId;
5200
5423
  }
5201
5424
  // ── Snapshot ──────────────────────────────────────────────────
5202
5425
  /**
@@ -5224,7 +5447,7 @@ var CrawlPage = class {
5224
5447
  if (opts?.mode === "role") {
5225
5448
  return snapshotRole({
5226
5449
  cdpUrl: this.cdpUrl,
5227
- targetId: this.targetId,
5450
+ targetId: this._targetId,
5228
5451
  selector: opts.selector,
5229
5452
  frameSelector: opts.frameSelector,
5230
5453
  refsMode: opts.refsMode,
@@ -5243,7 +5466,7 @@ var CrawlPage = class {
5243
5466
  }
5244
5467
  return snapshotAi({
5245
5468
  cdpUrl: this.cdpUrl,
5246
- targetId: this.targetId,
5469
+ targetId: this._targetId,
5247
5470
  maxChars: opts?.maxChars,
5248
5471
  options: {
5249
5472
  interactive: opts?.interactive,
@@ -5262,7 +5485,7 @@ var CrawlPage = class {
5262
5485
  * @returns Array of accessibility tree nodes
5263
5486
  */
5264
5487
  async ariaSnapshot(opts) {
5265
- return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
5488
+ return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this._targetId, limit: opts?.limit });
5266
5489
  }
5267
5490
  // ── Interactions ─────────────────────────────────────────────
5268
5491
  /**
@@ -5282,7 +5505,7 @@ var CrawlPage = class {
5282
5505
  async click(ref, opts) {
5283
5506
  return clickViaPlaywright({
5284
5507
  cdpUrl: this.cdpUrl,
5285
- targetId: this.targetId,
5508
+ targetId: this._targetId,
5286
5509
  ref,
5287
5510
  doubleClick: opts?.doubleClick,
5288
5511
  button: opts?.button,
@@ -5309,7 +5532,7 @@ var CrawlPage = class {
5309
5532
  async clickBySelector(selector, opts) {
5310
5533
  return clickViaPlaywright({
5311
5534
  cdpUrl: this.cdpUrl,
5312
- targetId: this.targetId,
5535
+ targetId: this._targetId,
5313
5536
  selector,
5314
5537
  doubleClick: opts?.doubleClick,
5315
5538
  button: opts?.button,
@@ -5338,7 +5561,7 @@ var CrawlPage = class {
5338
5561
  async mouseClick(x, y, opts) {
5339
5562
  return mouseClickViaPlaywright({
5340
5563
  cdpUrl: this.cdpUrl,
5341
- targetId: this.targetId,
5564
+ targetId: this._targetId,
5342
5565
  x,
5343
5566
  y,
5344
5567
  button: opts?.button,
@@ -5365,7 +5588,7 @@ var CrawlPage = class {
5365
5588
  async pressAndHold(x, y, opts) {
5366
5589
  return pressAndHoldViaCdp({
5367
5590
  cdpUrl: this.cdpUrl,
5368
- targetId: this.targetId,
5591
+ targetId: this._targetId,
5369
5592
  x,
5370
5593
  y,
5371
5594
  delay: opts?.delay,
@@ -5389,7 +5612,7 @@ var CrawlPage = class {
5389
5612
  async clickByText(text, opts) {
5390
5613
  return clickByTextViaPlaywright({
5391
5614
  cdpUrl: this.cdpUrl,
5392
- targetId: this.targetId,
5615
+ targetId: this._targetId,
5393
5616
  text,
5394
5617
  exact: opts?.exact,
5395
5618
  button: opts?.button,
@@ -5416,7 +5639,7 @@ var CrawlPage = class {
5416
5639
  async clickByRole(role, name, opts) {
5417
5640
  return clickByRoleViaPlaywright({
5418
5641
  cdpUrl: this.cdpUrl,
5419
- targetId: this.targetId,
5642
+ targetId: this._targetId,
5420
5643
  role,
5421
5644
  name,
5422
5645
  index: opts?.index,
@@ -5445,7 +5668,7 @@ var CrawlPage = class {
5445
5668
  async type(ref, text, opts) {
5446
5669
  return typeViaPlaywright({
5447
5670
  cdpUrl: this.cdpUrl,
5448
- targetId: this.targetId,
5671
+ targetId: this._targetId,
5449
5672
  ref,
5450
5673
  text,
5451
5674
  submit: opts?.submit,
@@ -5462,7 +5685,7 @@ var CrawlPage = class {
5462
5685
  async hover(ref, opts) {
5463
5686
  return hoverViaPlaywright({
5464
5687
  cdpUrl: this.cdpUrl,
5465
- targetId: this.targetId,
5688
+ targetId: this._targetId,
5466
5689
  ref,
5467
5690
  timeoutMs: opts?.timeoutMs
5468
5691
  });
@@ -5482,7 +5705,7 @@ var CrawlPage = class {
5482
5705
  async select(ref, ...values) {
5483
5706
  return selectOptionViaPlaywright({
5484
5707
  cdpUrl: this.cdpUrl,
5485
- targetId: this.targetId,
5708
+ targetId: this._targetId,
5486
5709
  ref,
5487
5710
  values
5488
5711
  });
@@ -5497,7 +5720,7 @@ var CrawlPage = class {
5497
5720
  async drag(startRef, endRef, opts) {
5498
5721
  return dragViaPlaywright({
5499
5722
  cdpUrl: this.cdpUrl,
5500
- targetId: this.targetId,
5723
+ targetId: this._targetId,
5501
5724
  startRef,
5502
5725
  endRef,
5503
5726
  timeoutMs: opts?.timeoutMs
@@ -5522,7 +5745,7 @@ var CrawlPage = class {
5522
5745
  async fill(fields) {
5523
5746
  return fillFormViaPlaywright({
5524
5747
  cdpUrl: this.cdpUrl,
5525
- targetId: this.targetId,
5748
+ targetId: this._targetId,
5526
5749
  fields
5527
5750
  });
5528
5751
  }
@@ -5535,7 +5758,7 @@ var CrawlPage = class {
5535
5758
  async scrollIntoView(ref, opts) {
5536
5759
  return scrollIntoViewViaPlaywright({
5537
5760
  cdpUrl: this.cdpUrl,
5538
- targetId: this.targetId,
5761
+ targetId: this._targetId,
5539
5762
  ref,
5540
5763
  timeoutMs: opts?.timeoutMs
5541
5764
  });
@@ -5548,7 +5771,7 @@ var CrawlPage = class {
5548
5771
  async highlight(ref) {
5549
5772
  return highlightViaPlaywright({
5550
5773
  cdpUrl: this.cdpUrl,
5551
- targetId: this.targetId,
5774
+ targetId: this._targetId,
5552
5775
  ref
5553
5776
  });
5554
5777
  }
@@ -5561,7 +5784,7 @@ var CrawlPage = class {
5561
5784
  async uploadFile(ref, paths) {
5562
5785
  return setInputFilesViaPlaywright({
5563
5786
  cdpUrl: this.cdpUrl,
5564
- targetId: this.targetId,
5787
+ targetId: this._targetId,
5565
5788
  ref,
5566
5789
  paths
5567
5790
  });
@@ -5584,7 +5807,7 @@ var CrawlPage = class {
5584
5807
  async armDialog(opts) {
5585
5808
  return armDialogViaPlaywright({
5586
5809
  cdpUrl: this.cdpUrl,
5587
- targetId: this.targetId,
5810
+ targetId: this._targetId,
5588
5811
  accept: opts.accept,
5589
5812
  promptText: opts.promptText,
5590
5813
  timeoutMs: opts.timeoutMs
@@ -5628,7 +5851,7 @@ var CrawlPage = class {
5628
5851
  async onDialog(handler) {
5629
5852
  return setDialogHandler({
5630
5853
  cdpUrl: this.cdpUrl,
5631
- targetId: this.targetId,
5854
+ targetId: this._targetId,
5632
5855
  handler: handler ?? void 0
5633
5856
  });
5634
5857
  }
@@ -5650,7 +5873,7 @@ var CrawlPage = class {
5650
5873
  async armFileUpload(paths, opts) {
5651
5874
  return armFileUploadViaPlaywright({
5652
5875
  cdpUrl: this.cdpUrl,
5653
- targetId: this.targetId,
5876
+ targetId: this._targetId,
5654
5877
  paths,
5655
5878
  timeoutMs: opts?.timeoutMs
5656
5879
  });
@@ -5665,7 +5888,7 @@ var CrawlPage = class {
5665
5888
  async batch(actions, opts) {
5666
5889
  return batchViaPlaywright({
5667
5890
  cdpUrl: this.cdpUrl,
5668
- targetId: this.targetId,
5891
+ targetId: this._targetId,
5669
5892
  actions,
5670
5893
  stopOnError: opts?.stopOnError,
5671
5894
  evaluateEnabled: opts?.evaluateEnabled
@@ -5690,7 +5913,7 @@ var CrawlPage = class {
5690
5913
  async press(key, opts) {
5691
5914
  return pressKeyViaPlaywright({
5692
5915
  cdpUrl: this.cdpUrl,
5693
- targetId: this.targetId,
5916
+ targetId: this._targetId,
5694
5917
  key,
5695
5918
  delayMs: opts?.delayMs
5696
5919
  });
@@ -5700,14 +5923,14 @@ var CrawlPage = class {
5700
5923
  * Get the current URL of the page.
5701
5924
  */
5702
5925
  async url() {
5703
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5926
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5704
5927
  return page.url();
5705
5928
  }
5706
5929
  /**
5707
5930
  * Get the page title.
5708
5931
  */
5709
5932
  async title() {
5710
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5933
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5711
5934
  return page.title();
5712
5935
  }
5713
5936
  /**
@@ -5720,7 +5943,7 @@ var CrawlPage = class {
5720
5943
  async goto(url, opts) {
5721
5944
  return navigateViaPlaywright({
5722
5945
  cdpUrl: this.cdpUrl,
5723
- targetId: this.targetId,
5946
+ targetId: this._targetId,
5724
5947
  url,
5725
5948
  timeoutMs: opts?.timeoutMs,
5726
5949
  ssrfPolicy: this.ssrfPolicy
@@ -5732,7 +5955,7 @@ var CrawlPage = class {
5732
5955
  * @param opts - Timeout options
5733
5956
  */
5734
5957
  async reload(opts) {
5735
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5958
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5736
5959
  ensurePageState(page);
5737
5960
  await page.reload({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5738
5961
  }
@@ -5742,7 +5965,7 @@ var CrawlPage = class {
5742
5965
  * @param opts - Timeout options
5743
5966
  */
5744
5967
  async goBack(opts) {
5745
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5968
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5746
5969
  ensurePageState(page);
5747
5970
  await page.goBack({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5748
5971
  }
@@ -5752,7 +5975,7 @@ var CrawlPage = class {
5752
5975
  * @param opts - Timeout options
5753
5976
  */
5754
5977
  async goForward(opts) {
5755
- const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
5978
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5756
5979
  ensurePageState(page);
5757
5980
  await page.goForward({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 2e4) });
5758
5981
  }
@@ -5775,7 +5998,7 @@ var CrawlPage = class {
5775
5998
  async waitFor(opts) {
5776
5999
  return waitForViaPlaywright({
5777
6000
  cdpUrl: this.cdpUrl,
5778
- targetId: this.targetId,
6001
+ targetId: this._targetId,
5779
6002
  ...opts
5780
6003
  });
5781
6004
  }
@@ -5800,7 +6023,7 @@ var CrawlPage = class {
5800
6023
  async evaluate(fn, opts) {
5801
6024
  return evaluateViaPlaywright({
5802
6025
  cdpUrl: this.cdpUrl,
5803
- targetId: this.targetId,
6026
+ targetId: this._targetId,
5804
6027
  fn,
5805
6028
  ref: opts?.ref,
5806
6029
  timeoutMs: opts?.timeoutMs,
@@ -5827,7 +6050,7 @@ var CrawlPage = class {
5827
6050
  async evaluateInAllFrames(fn) {
5828
6051
  return evaluateInAllFramesViaPlaywright({
5829
6052
  cdpUrl: this.cdpUrl,
5830
- targetId: this.targetId,
6053
+ targetId: this._targetId,
5831
6054
  fn
5832
6055
  });
5833
6056
  }
@@ -5848,7 +6071,7 @@ var CrawlPage = class {
5848
6071
  async screenshot(opts) {
5849
6072
  const result = await takeScreenshotViaPlaywright({
5850
6073
  cdpUrl: this.cdpUrl,
5851
- targetId: this.targetId,
6074
+ targetId: this._targetId,
5852
6075
  fullPage: opts?.fullPage,
5853
6076
  ref: opts?.ref,
5854
6077
  element: opts?.element,
@@ -5864,7 +6087,7 @@ var CrawlPage = class {
5864
6087
  * @returns PDF document as a Buffer
5865
6088
  */
5866
6089
  async pdf() {
5867
- const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6090
+ const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
5868
6091
  return result.buffer;
5869
6092
  }
5870
6093
  /**
@@ -5885,7 +6108,7 @@ var CrawlPage = class {
5885
6108
  async screenshotWithLabels(refs, opts) {
5886
6109
  return screenshotWithLabelsViaPlaywright({
5887
6110
  cdpUrl: this.cdpUrl,
5888
- targetId: this.targetId,
6111
+ targetId: this._targetId,
5889
6112
  refs,
5890
6113
  maxLabels: opts?.maxLabels,
5891
6114
  type: opts?.type
@@ -5902,7 +6125,7 @@ var CrawlPage = class {
5902
6125
  async traceStart(opts) {
5903
6126
  return traceStartViaPlaywright({
5904
6127
  cdpUrl: this.cdpUrl,
5905
- targetId: this.targetId,
6128
+ targetId: this._targetId,
5906
6129
  screenshots: opts?.screenshots,
5907
6130
  snapshots: opts?.snapshots,
5908
6131
  sources: opts?.sources
@@ -5917,7 +6140,7 @@ var CrawlPage = class {
5917
6140
  async traceStop(path2, opts) {
5918
6141
  return traceStopViaPlaywright({
5919
6142
  cdpUrl: this.cdpUrl,
5920
- targetId: this.targetId,
6143
+ targetId: this._targetId,
5921
6144
  path: path2,
5922
6145
  allowedOutputRoots: opts?.allowedOutputRoots
5923
6146
  });
@@ -5938,7 +6161,7 @@ var CrawlPage = class {
5938
6161
  async responseBody(url, opts) {
5939
6162
  return responseBodyViaPlaywright({
5940
6163
  cdpUrl: this.cdpUrl,
5941
- targetId: this.targetId,
6164
+ targetId: this._targetId,
5942
6165
  url,
5943
6166
  timeoutMs: opts?.timeoutMs,
5944
6167
  maxChars: opts?.maxChars
@@ -5966,7 +6189,7 @@ var CrawlPage = class {
5966
6189
  async waitForRequest(url, opts) {
5967
6190
  return waitForRequestViaPlaywright({
5968
6191
  cdpUrl: this.cdpUrl,
5969
- targetId: this.targetId,
6192
+ targetId: this._targetId,
5970
6193
  url,
5971
6194
  method: opts?.method,
5972
6195
  timeoutMs: opts?.timeoutMs,
@@ -5984,7 +6207,7 @@ var CrawlPage = class {
5984
6207
  async consoleLogs(opts) {
5985
6208
  return getConsoleMessagesViaPlaywright({
5986
6209
  cdpUrl: this.cdpUrl,
5987
- targetId: this.targetId,
6210
+ targetId: this._targetId,
5988
6211
  level: opts?.level,
5989
6212
  clear: opts?.clear
5990
6213
  });
@@ -5998,7 +6221,7 @@ var CrawlPage = class {
5998
6221
  async pageErrors(opts) {
5999
6222
  const result = await getPageErrorsViaPlaywright({
6000
6223
  cdpUrl: this.cdpUrl,
6001
- targetId: this.targetId,
6224
+ targetId: this._targetId,
6002
6225
  clear: opts?.clear
6003
6226
  });
6004
6227
  return result.errors;
@@ -6019,7 +6242,7 @@ var CrawlPage = class {
6019
6242
  async networkRequests(opts) {
6020
6243
  const result = await getNetworkRequestsViaPlaywright({
6021
6244
  cdpUrl: this.cdpUrl,
6022
- targetId: this.targetId,
6245
+ targetId: this._targetId,
6023
6246
  filter: opts?.filter,
6024
6247
  clear: opts?.clear
6025
6248
  });
@@ -6035,7 +6258,7 @@ var CrawlPage = class {
6035
6258
  async resize(width, height) {
6036
6259
  return resizeViewportViaPlaywright({
6037
6260
  cdpUrl: this.cdpUrl,
6038
- targetId: this.targetId,
6261
+ targetId: this._targetId,
6039
6262
  width,
6040
6263
  height
6041
6264
  });
@@ -6047,7 +6270,7 @@ var CrawlPage = class {
6047
6270
  * @returns Array of cookie objects
6048
6271
  */
6049
6272
  async cookies() {
6050
- const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6273
+ const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6051
6274
  return result.cookies;
6052
6275
  }
6053
6276
  /**
@@ -6065,11 +6288,11 @@ var CrawlPage = class {
6065
6288
  * ```
6066
6289
  */
6067
6290
  async setCookie(cookie) {
6068
- return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
6291
+ return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId, cookie });
6069
6292
  }
6070
6293
  /** Clear all cookies in the browser context. */
6071
6294
  async clearCookies() {
6072
- return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6295
+ return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6073
6296
  }
6074
6297
  /**
6075
6298
  * Get values from localStorage or sessionStorage.
@@ -6081,7 +6304,7 @@ var CrawlPage = class {
6081
6304
  async storageGet(kind, key) {
6082
6305
  const result = await storageGetViaPlaywright({
6083
6306
  cdpUrl: this.cdpUrl,
6084
- targetId: this.targetId,
6307
+ targetId: this._targetId,
6085
6308
  kind,
6086
6309
  key
6087
6310
  });
@@ -6097,7 +6320,7 @@ var CrawlPage = class {
6097
6320
  async storageSet(kind, key, value) {
6098
6321
  return storageSetViaPlaywright({
6099
6322
  cdpUrl: this.cdpUrl,
6100
- targetId: this.targetId,
6323
+ targetId: this._targetId,
6101
6324
  kind,
6102
6325
  key,
6103
6326
  value
@@ -6111,7 +6334,7 @@ var CrawlPage = class {
6111
6334
  async storageClear(kind) {
6112
6335
  return storageClearViaPlaywright({
6113
6336
  cdpUrl: this.cdpUrl,
6114
- targetId: this.targetId,
6337
+ targetId: this._targetId,
6115
6338
  kind
6116
6339
  });
6117
6340
  }
@@ -6133,7 +6356,7 @@ var CrawlPage = class {
6133
6356
  async download(ref, path2, opts) {
6134
6357
  return downloadViaPlaywright({
6135
6358
  cdpUrl: this.cdpUrl,
6136
- targetId: this.targetId,
6359
+ targetId: this._targetId,
6137
6360
  ref,
6138
6361
  path: path2,
6139
6362
  timeoutMs: opts?.timeoutMs,
@@ -6151,7 +6374,7 @@ var CrawlPage = class {
6151
6374
  async waitForDownload(opts) {
6152
6375
  return waitForDownloadViaPlaywright({
6153
6376
  cdpUrl: this.cdpUrl,
6154
- targetId: this.targetId,
6377
+ targetId: this._targetId,
6155
6378
  path: opts?.path,
6156
6379
  timeoutMs: opts?.timeoutMs,
6157
6380
  allowedOutputRoots: opts?.allowedOutputRoots
@@ -6166,7 +6389,7 @@ var CrawlPage = class {
6166
6389
  async setOffline(offline) {
6167
6390
  return setOfflineViaPlaywright({
6168
6391
  cdpUrl: this.cdpUrl,
6169
- targetId: this.targetId,
6392
+ targetId: this._targetId,
6170
6393
  offline
6171
6394
  });
6172
6395
  }
@@ -6183,7 +6406,7 @@ var CrawlPage = class {
6183
6406
  async setExtraHeaders(headers) {
6184
6407
  return setExtraHTTPHeadersViaPlaywright({
6185
6408
  cdpUrl: this.cdpUrl,
6186
- targetId: this.targetId,
6409
+ targetId: this._targetId,
6187
6410
  headers
6188
6411
  });
6189
6412
  }
@@ -6195,7 +6418,7 @@ var CrawlPage = class {
6195
6418
  async setHttpCredentials(opts) {
6196
6419
  return setHttpCredentialsViaPlaywright({
6197
6420
  cdpUrl: this.cdpUrl,
6198
- targetId: this.targetId,
6421
+ targetId: this._targetId,
6199
6422
  username: opts.username,
6200
6423
  password: opts.password,
6201
6424
  clear: opts.clear
@@ -6215,7 +6438,7 @@ var CrawlPage = class {
6215
6438
  async setGeolocation(opts) {
6216
6439
  return setGeolocationViaPlaywright({
6217
6440
  cdpUrl: this.cdpUrl,
6218
- targetId: this.targetId,
6441
+ targetId: this._targetId,
6219
6442
  latitude: opts.latitude,
6220
6443
  longitude: opts.longitude,
6221
6444
  accuracy: opts.accuracy,
@@ -6236,7 +6459,7 @@ var CrawlPage = class {
6236
6459
  async emulateMedia(opts) {
6237
6460
  return emulateMediaViaPlaywright({
6238
6461
  cdpUrl: this.cdpUrl,
6239
- targetId: this.targetId,
6462
+ targetId: this._targetId,
6240
6463
  colorScheme: opts.colorScheme
6241
6464
  });
6242
6465
  }
@@ -6248,7 +6471,7 @@ var CrawlPage = class {
6248
6471
  async setLocale(locale) {
6249
6472
  return setLocaleViaPlaywright({
6250
6473
  cdpUrl: this.cdpUrl,
6251
- targetId: this.targetId,
6474
+ targetId: this._targetId,
6252
6475
  locale
6253
6476
  });
6254
6477
  }
@@ -6260,7 +6483,7 @@ var CrawlPage = class {
6260
6483
  async setTimezone(timezoneId) {
6261
6484
  return setTimezoneViaPlaywright({
6262
6485
  cdpUrl: this.cdpUrl,
6263
- targetId: this.targetId,
6486
+ targetId: this._targetId,
6264
6487
  timezoneId
6265
6488
  });
6266
6489
  }
@@ -6277,7 +6500,7 @@ var CrawlPage = class {
6277
6500
  async setDevice(name) {
6278
6501
  return setDeviceViaPlaywright({
6279
6502
  cdpUrl: this.cdpUrl,
6280
- targetId: this.targetId,
6503
+ targetId: this._targetId,
6281
6504
  name
6282
6505
  });
6283
6506
  }
@@ -6298,7 +6521,7 @@ var CrawlPage = class {
6298
6521
  * ```
6299
6522
  */
6300
6523
  async detectChallenge() {
6301
- return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6524
+ return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6302
6525
  }
6303
6526
  /**
6304
6527
  * Wait for an anti-bot challenge to resolve on its own.
@@ -6323,7 +6546,7 @@ var CrawlPage = class {
6323
6546
  async waitForChallenge(opts) {
6324
6547
  return waitForChallengeViaPlaywright({
6325
6548
  cdpUrl: this.cdpUrl,
6326
- targetId: this.targetId,
6549
+ targetId: this._targetId,
6327
6550
  timeoutMs: opts?.timeoutMs,
6328
6551
  pollMs: opts?.pollMs
6329
6552
  });
@@ -6360,8 +6583,24 @@ var CrawlPage = class {
6360
6583
  */
6361
6584
  async isAuthenticated(rules) {
6362
6585
  if (!rules.length) return { authenticated: true, checks: [] };
6363
- const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6586
+ const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6364
6587
  const checks = [];
6588
+ const needsBodyText = rules.some((r) => r.text !== void 0 || r.textGone !== void 0);
6589
+ let bodyText = null;
6590
+ if (needsBodyText) {
6591
+ try {
6592
+ const raw = await evaluateViaPlaywright({
6593
+ cdpUrl: this.cdpUrl,
6594
+ targetId: this._targetId,
6595
+ fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6596
+ });
6597
+ bodyText = typeof raw === "string" ? raw : null;
6598
+ } catch (err) {
6599
+ console.warn(
6600
+ `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6601
+ );
6602
+ }
6603
+ }
6365
6604
  for (const rule of rules) {
6366
6605
  if (rule.url !== void 0) {
6367
6606
  const currentUrl = page.url();
@@ -6394,19 +6633,6 @@ var CrawlPage = class {
6394
6633
  }
6395
6634
  }
6396
6635
  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
6636
  if (rule.text !== void 0) {
6411
6637
  if (bodyText === null) {
6412
6638
  checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
@@ -6436,7 +6662,7 @@ var CrawlPage = class {
6436
6662
  try {
6437
6663
  const result = await evaluateViaPlaywright({
6438
6664
  cdpUrl: this.cdpUrl,
6439
- targetId: this.targetId,
6665
+ targetId: this._targetId,
6440
6666
  fn: rule.fn
6441
6667
  });
6442
6668
  const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
@@ -6485,7 +6711,7 @@ var CrawlPage = class {
6485
6711
  * ```
6486
6712
  */
6487
6713
  async playwrightPage() {
6488
- return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6714
+ return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6489
6715
  }
6490
6716
  /**
6491
6717
  * Create a Playwright `Locator` for a CSS selector on this page.
@@ -6508,7 +6734,7 @@ var CrawlPage = class {
6508
6734
  * ```
6509
6735
  */
6510
6736
  async locator(selector) {
6511
- const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6737
+ const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this._targetId });
6512
6738
  return pwPage.locator(selector);
6513
6739
  }
6514
6740
  };
@@ -6552,21 +6778,27 @@ var BrowserClaw = class _BrowserClaw {
6552
6778
  static async launch(opts = {}) {
6553
6779
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6554
6780
  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();
6781
+ try {
6782
+ const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6783
+ const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6784
+ const telemetry = {
6785
+ launchMs: chrome.launchMs,
6786
+ timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6787
+ };
6788
+ const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6789
+ if (opts.url !== void 0 && opts.url !== "") {
6790
+ const page = await browser.currentPage();
6791
+ const navT0 = Date.now();
6792
+ await page.goto(opts.url);
6793
+ telemetry.navMs = Date.now() - navT0;
6794
+ telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6795
+ }
6796
+ return browser;
6797
+ } catch (err) {
6798
+ await stopChrome(chrome).catch(() => {
6799
+ });
6800
+ throw err;
6568
6801
  }
6569
- return browser;
6570
6802
  }
6571
6803
  /**
6572
6804
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -6723,7 +6955,7 @@ var BrowserClaw = class _BrowserClaw {
6723
6955
  if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
6724
6956
  try {
6725
6957
  clearRecordingContext(this.cdpUrl);
6726
- await disconnectBrowser();
6958
+ await closePlaywrightBrowserConnection({ cdpUrl: this.cdpUrl });
6727
6959
  if (this.chrome) {
6728
6960
  await stopChrome(this.chrome);
6729
6961
  this.chrome = null;