browserclaw 0.11.1 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1218,11 +1218,14 @@ function safeWriteJson(filePath, data) {
1218
1218
  function setDeep(obj, keys, value) {
1219
1219
  let node = obj;
1220
1220
  for (const key of keys.slice(0, -1)) {
1221
+ if (key === "__proto__" || key === "constructor" || key === "prototype") return;
1221
1222
  const next = node[key];
1222
1223
  if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
1223
1224
  node = node[key];
1224
1225
  }
1225
- node[keys[keys.length - 1]] = value;
1226
+ const lastKey = keys[keys.length - 1];
1227
+ if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") return;
1228
+ node[lastKey] = value;
1226
1229
  }
1227
1230
  function parseHexRgbToSignedArgbInt(hex) {
1228
1231
  const cleaned = hex.trim().replace(/^#/, "");
@@ -1779,137 +1782,10 @@ var STEALTH_SCRIPT = `(function() {
1779
1782
  });
1780
1783
  })()`;
1781
1784
 
1782
- // src/connection.ts
1783
- var BrowserTabNotFoundError = class extends Error {
1784
- constructor(message = "Tab not found") {
1785
- super(message);
1786
- this.name = "BrowserTabNotFoundError";
1787
- }
1788
- };
1789
- async function fetchJsonForCdp(url, timeoutMs) {
1790
- const ctrl = new AbortController();
1791
- const t = setTimeout(() => {
1792
- ctrl.abort();
1793
- }, timeoutMs);
1794
- try {
1795
- const res = await fetch(url, { signal: ctrl.signal });
1796
- if (!res.ok) return null;
1797
- return await res.json();
1798
- } catch {
1799
- return null;
1800
- } finally {
1801
- clearTimeout(t);
1802
- }
1803
- }
1804
- function appendCdpPath2(cdpUrl, cdpPath) {
1805
- try {
1806
- const url = new URL(cdpUrl);
1807
- url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
1808
- return url.toString();
1809
- } catch {
1810
- return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
1811
- }
1812
- }
1813
- async function withPlaywrightPageCdpSession(page, fn) {
1814
- const CDP_SESSION_TIMEOUT_MS = 1e4;
1815
- const session = await Promise.race([
1816
- page.context().newCDPSession(page),
1817
- new Promise((_, reject) => {
1818
- setTimeout(() => {
1819
- reject(new Error("newCDPSession timed out after 10s"));
1820
- }, CDP_SESSION_TIMEOUT_MS);
1821
- })
1822
- ]);
1823
- try {
1824
- return await fn(session);
1825
- } finally {
1826
- await session.detach().catch(() => {
1827
- });
1828
- }
1829
- }
1830
- async function withPageScopedCdpClient(opts) {
1831
- return await withPlaywrightPageCdpSession(opts.page, async (session) => {
1832
- return await opts.fn((method, params) => session.send(method, params));
1833
- });
1834
- }
1835
- var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
1836
- function noProxyAlreadyCoversLocalhost() {
1837
- const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
1838
- return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
1839
- }
1840
- function isLoopbackCdpUrl(url) {
1841
- try {
1842
- return isLoopbackHost(new URL(url).hostname);
1843
- } catch {
1844
- return false;
1845
- }
1846
- }
1847
- var NoProxyLeaseManager = class {
1848
- leaseCount = 0;
1849
- snapshot = null;
1850
- acquire(url) {
1851
- if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
1852
- if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
1853
- const noProxy = process.env.NO_PROXY;
1854
- const noProxyLower = process.env.no_proxy;
1855
- const current = noProxy ?? noProxyLower ?? "";
1856
- const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
1857
- process.env.NO_PROXY = applied;
1858
- process.env.no_proxy = applied;
1859
- this.snapshot = { noProxy, noProxyLower, applied };
1860
- }
1861
- this.leaseCount += 1;
1862
- let released = false;
1863
- return () => {
1864
- if (released) return;
1865
- released = true;
1866
- this.release();
1867
- };
1868
- }
1869
- release() {
1870
- if (this.leaseCount <= 0) return;
1871
- this.leaseCount -= 1;
1872
- if (this.leaseCount > 0 || !this.snapshot) return;
1873
- const { noProxy, noProxyLower, applied } = this.snapshot;
1874
- const currentNoProxy = process.env.NO_PROXY;
1875
- const currentNoProxyLower = process.env.no_proxy;
1876
- if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
1877
- if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
1878
- else delete process.env.NO_PROXY;
1879
- if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
1880
- else delete process.env.no_proxy;
1881
- }
1882
- this.snapshot = null;
1883
- }
1884
- };
1885
- var noProxyLeaseManager = new NoProxyLeaseManager();
1886
- async function withNoProxyForCdpUrl(url, fn) {
1887
- const release = noProxyLeaseManager.acquire(url);
1888
- try {
1889
- return await fn();
1890
- } finally {
1891
- release?.();
1892
- }
1893
- }
1894
- new http__default.default.Agent();
1895
- new https__default.default.Agent();
1896
- function getHeadersWithAuth(endpoint, baseHeaders = {}) {
1897
- const headers = { ...baseHeaders };
1898
- try {
1899
- const parsed = new URL(endpoint);
1900
- if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
1901
- if (parsed.username || parsed.password) {
1902
- const credentials = Buffer.from(
1903
- `${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
1904
- ).toString("base64");
1905
- headers.Authorization = `Basic ${credentials}`;
1906
- }
1907
- } catch {
1908
- }
1909
- return headers;
1910
- }
1911
- var cachedByCdpUrl = /* @__PURE__ */ new Map();
1912
- var connectingByCdpUrl = /* @__PURE__ */ new Map();
1785
+ // src/page-utils.ts
1786
+ var MAX_CONSOLE_MESSAGES = 500;
1787
+ var MAX_PAGE_ERRORS = 200;
1788
+ var MAX_NETWORK_REQUESTS = 500;
1913
1789
  var pageStates = /* @__PURE__ */ new WeakMap();
1914
1790
  var contextStates = /* @__PURE__ */ new WeakMap();
1915
1791
  var observedContexts = /* @__PURE__ */ new WeakSet();
@@ -1929,56 +1805,6 @@ function bumpDownloadArmId() {
1929
1805
  nextDownloadArmId += 1;
1930
1806
  return nextDownloadArmId;
1931
1807
  }
1932
- var BlockedBrowserTargetError = class extends Error {
1933
- constructor() {
1934
- super("Browser target is unavailable after SSRF policy blocked its navigation.");
1935
- this.name = "BlockedBrowserTargetError";
1936
- }
1937
- };
1938
- var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
1939
- var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
1940
- function blockedTargetKey(cdpUrl, targetId) {
1941
- return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
1942
- }
1943
- function isBlockedTarget(cdpUrl, targetId) {
1944
- const normalized = targetId?.trim() ?? "";
1945
- if (normalized === "") return false;
1946
- return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
1947
- }
1948
- function markTargetBlocked(cdpUrl, targetId) {
1949
- const normalized = targetId?.trim() ?? "";
1950
- if (normalized === "") return;
1951
- blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
1952
- }
1953
- function clearBlockedTarget(cdpUrl, targetId) {
1954
- const normalized = targetId?.trim() ?? "";
1955
- if (normalized === "") return;
1956
- blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
1957
- }
1958
- function hasBlockedTargetsForCdpUrl(cdpUrl) {
1959
- const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
1960
- for (const key of blockedTargetsByCdpUrl) {
1961
- if (key.startsWith(prefix)) return true;
1962
- }
1963
- return false;
1964
- }
1965
- function blockedPageRefsForCdpUrl(cdpUrl) {
1966
- const normalized = normalizeCdpUrl(cdpUrl);
1967
- const existing = blockedPageRefsByCdpUrl.get(normalized);
1968
- if (existing) return existing;
1969
- const created = /* @__PURE__ */ new WeakSet();
1970
- blockedPageRefsByCdpUrl.set(normalized, created);
1971
- return created;
1972
- }
1973
- function isBlockedPageRef(cdpUrl, page) {
1974
- return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
1975
- }
1976
- function markPageRefBlocked(cdpUrl, page) {
1977
- blockedPageRefsForCdpUrl(cdpUrl).add(page);
1978
- }
1979
- function clearBlockedPageRef(cdpUrl, page) {
1980
- blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
1981
- }
1982
1808
  function ensureContextState(context) {
1983
1809
  const existing = contextStates.get(context);
1984
1810
  if (existing) return existing;
@@ -1986,16 +1812,8 @@ function ensureContextState(context) {
1986
1812
  contextStates.set(context, state);
1987
1813
  return state;
1988
1814
  }
1989
- var roleRefsByTarget = /* @__PURE__ */ new Map();
1990
- var MAX_ROLE_REFS_CACHE = 50;
1991
- var MAX_CONSOLE_MESSAGES = 500;
1992
- var MAX_PAGE_ERRORS = 200;
1993
- var MAX_NETWORK_REQUESTS = 500;
1994
- function normalizeCdpUrl(raw) {
1995
- return raw.replace(/\/$/, "");
1996
- }
1997
- function roleRefsKey(cdpUrl, targetId) {
1998
- return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
1815
+ function getPageState(page) {
1816
+ return pageStates.get(page);
1999
1817
  }
2000
1818
  function findNetworkRequestById(state, id) {
2001
1819
  for (let i = state.requests.length - 1; i >= 0; i--) {
@@ -2116,77 +1934,353 @@ function ensurePageState(page) {
2116
1934
  observedPages.delete(page);
2117
1935
  });
2118
1936
  }
2119
- return state;
1937
+ return state;
1938
+ }
1939
+ function setDialogHandlerOnPage(page, handler) {
1940
+ const state = ensurePageState(page);
1941
+ state.dialogHandler = handler;
1942
+ }
1943
+ function applyStealthToPage(page) {
1944
+ page.evaluate(STEALTH_SCRIPT).catch((e) => {
1945
+ if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1946
+ console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
1947
+ });
1948
+ }
1949
+ function observeContext(context) {
1950
+ if (observedContexts.has(context)) return;
1951
+ observedContexts.add(context);
1952
+ ensureContextState(context);
1953
+ context.addInitScript(STEALTH_SCRIPT).catch((e) => {
1954
+ if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
1955
+ console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
1956
+ });
1957
+ for (const page of context.pages()) {
1958
+ ensurePageState(page);
1959
+ applyStealthToPage(page);
1960
+ }
1961
+ context.on("page", (page) => {
1962
+ ensurePageState(page);
1963
+ applyStealthToPage(page);
1964
+ });
1965
+ }
1966
+ function observeBrowser(browser) {
1967
+ for (const context of browser.contexts()) observeContext(context);
1968
+ }
1969
+ function toAIFriendlyError(error, selector) {
1970
+ const message = error instanceof Error ? error.message : String(error);
1971
+ if (message.includes("strict mode violation")) {
1972
+ const countMatch = /resolved to (\d+) elements/.exec(message);
1973
+ const count = countMatch ? countMatch[1] : "multiple";
1974
+ return new Error(
1975
+ `Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
1976
+ );
1977
+ }
1978
+ if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
1979
+ return new Error(
1980
+ `Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
1981
+ );
1982
+ }
1983
+ if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
1984
+ return new Error(
1985
+ `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
1986
+ );
1987
+ }
1988
+ const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
1989
+ if (timeoutMatch) {
1990
+ return new Error(
1991
+ `Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
1992
+ );
1993
+ }
1994
+ const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
1995
+ return new Error(cleaned || message);
1996
+ }
1997
+ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
1998
+ return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
1999
+ }
2000
+
2001
+ // src/ref-resolver.ts
2002
+ var roleRefsByTarget = /* @__PURE__ */ new Map();
2003
+ var MAX_ROLE_REFS_CACHE = 50;
2004
+ function normalizeCdpUrl(raw) {
2005
+ return raw.replace(/\/$/, "");
2006
+ }
2007
+ function roleRefsKey(cdpUrl, targetId) {
2008
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
2009
+ }
2010
+ function rememberRoleRefsForTarget(opts) {
2011
+ const targetId = opts.targetId.trim();
2012
+ if (targetId === "") return;
2013
+ roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
2014
+ refs: opts.refs,
2015
+ ...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
2016
+ ...opts.mode !== void 0 ? { mode: opts.mode } : {}
2017
+ });
2018
+ while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
2019
+ const first = roleRefsByTarget.keys().next();
2020
+ if (first.done === true) break;
2021
+ roleRefsByTarget.delete(first.value);
2022
+ }
2023
+ }
2024
+ function storeRoleRefsForTarget(opts) {
2025
+ const state = ensurePageState(opts.page);
2026
+ state.roleRefs = opts.refs;
2027
+ state.roleRefsFrameSelector = opts.frameSelector;
2028
+ state.roleRefsMode = opts.mode;
2029
+ if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
2030
+ rememberRoleRefsForTarget({
2031
+ cdpUrl: opts.cdpUrl,
2032
+ targetId: opts.targetId,
2033
+ refs: opts.refs,
2034
+ frameSelector: opts.frameSelector,
2035
+ mode: opts.mode
2036
+ });
2037
+ }
2038
+ function restoreRoleRefsForTarget(opts) {
2039
+ const targetId = opts.targetId?.trim() ?? "";
2040
+ if (targetId === "") return;
2041
+ const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
2042
+ if (!entry) return;
2043
+ const state = ensurePageState(opts.page);
2044
+ if (state.roleRefs) return;
2045
+ state.roleRefs = entry.refs;
2046
+ state.roleRefsFrameSelector = entry.frameSelector;
2047
+ state.roleRefsMode = entry.mode;
2048
+ }
2049
+ function clearRoleRefsForCdpUrl(cdpUrl) {
2050
+ const normalized = normalizeCdpUrl(cdpUrl);
2051
+ for (const key of roleRefsByTarget.keys()) {
2052
+ if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
2053
+ }
2054
+ }
2055
+ function parseRoleRef(raw) {
2056
+ const trimmed = raw.trim();
2057
+ if (!trimmed) return null;
2058
+ const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
2059
+ return /^e\d+$/.test(normalized) ? normalized : null;
2060
+ }
2061
+ function requireRef(value) {
2062
+ const raw = typeof value === "string" ? value.trim() : "";
2063
+ const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
2064
+ if (!ref) throw new Error("ref is required");
2065
+ return ref;
2066
+ }
2067
+ function requireRefOrSelector(ref, selector) {
2068
+ const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2069
+ const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2070
+ if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2071
+ return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2072
+ }
2073
+ function resolveInteractionTimeoutMs(timeoutMs) {
2074
+ return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2075
+ }
2076
+ function resolveBoundedDelayMs(value, label, maxMs) {
2077
+ const normalized = Math.floor(value ?? 0);
2078
+ if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2079
+ if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
2080
+ return normalized;
2081
+ }
2082
+ function refLocator(page, ref) {
2083
+ const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
2084
+ if (normalized.trim() === "") throw new Error("ref is required");
2085
+ if (/^e\d+$/.test(normalized)) {
2086
+ const state = getPageState(page);
2087
+ if (state?.roleRefsMode === "aria") {
2088
+ return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2089
+ }
2090
+ const info = state?.roleRefs?.[normalized];
2091
+ if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
2092
+ const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
2093
+ const role = info.role;
2094
+ const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
2095
+ return info.nth !== void 0 ? locator.nth(info.nth) : locator;
2096
+ }
2097
+ return page.locator(`aria-ref=${normalized}`);
2098
+ }
2099
+
2100
+ // src/connection.ts
2101
+ var BrowserTabNotFoundError = class extends Error {
2102
+ constructor(message = "Tab not found") {
2103
+ super(message);
2104
+ this.name = "BrowserTabNotFoundError";
2105
+ }
2106
+ };
2107
+ async function fetchJsonForCdp(url, timeoutMs) {
2108
+ const ctrl = new AbortController();
2109
+ const t = setTimeout(() => {
2110
+ ctrl.abort();
2111
+ }, timeoutMs);
2112
+ try {
2113
+ const res = await fetch(url, { signal: ctrl.signal });
2114
+ if (!res.ok) return null;
2115
+ return await res.json();
2116
+ } catch {
2117
+ return null;
2118
+ } finally {
2119
+ clearTimeout(t);
2120
+ }
2121
+ }
2122
+ function appendCdpPath2(cdpUrl, cdpPath) {
2123
+ try {
2124
+ const url = new URL(cdpUrl);
2125
+ url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
2126
+ return url.toString();
2127
+ } catch {
2128
+ return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
2129
+ }
2130
+ }
2131
+ async function withPlaywrightPageCdpSession(page, fn) {
2132
+ const CDP_SESSION_TIMEOUT_MS = 1e4;
2133
+ const session = await Promise.race([
2134
+ page.context().newCDPSession(page),
2135
+ new Promise((_, reject) => {
2136
+ setTimeout(() => {
2137
+ reject(new Error("newCDPSession timed out after 10s"));
2138
+ }, CDP_SESSION_TIMEOUT_MS);
2139
+ })
2140
+ ]);
2141
+ try {
2142
+ return await fn(session);
2143
+ } finally {
2144
+ await session.detach().catch(() => {
2145
+ });
2146
+ }
2147
+ }
2148
+ async function withPageScopedCdpClient(opts) {
2149
+ return await withPlaywrightPageCdpSession(opts.page, async (session) => {
2150
+ return await opts.fn((method, params) => session.send(method, params));
2151
+ });
2152
+ }
2153
+ var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
2154
+ function noProxyAlreadyCoversLocalhost() {
2155
+ const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
2156
+ return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
2157
+ }
2158
+ function isLoopbackCdpUrl(url) {
2159
+ try {
2160
+ return isLoopbackHost(new URL(url).hostname);
2161
+ } catch {
2162
+ return false;
2163
+ }
2120
2164
  }
2121
- async function setDialogHandler(opts) {
2122
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2123
- const state = ensurePageState(page);
2124
- state.dialogHandler = opts.handler;
2165
+ var NoProxyLeaseManager = class {
2166
+ leaseCount = 0;
2167
+ snapshot = null;
2168
+ acquire(url) {
2169
+ if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
2170
+ if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
2171
+ const noProxy = process.env.NO_PROXY;
2172
+ const noProxyLower = process.env.no_proxy;
2173
+ const current = noProxy ?? noProxyLower ?? "";
2174
+ const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
2175
+ process.env.NO_PROXY = applied;
2176
+ process.env.no_proxy = applied;
2177
+ this.snapshot = { noProxy, noProxyLower, applied };
2178
+ }
2179
+ this.leaseCount += 1;
2180
+ let released = false;
2181
+ return () => {
2182
+ if (released) return;
2183
+ released = true;
2184
+ this.release();
2185
+ };
2186
+ }
2187
+ release() {
2188
+ if (this.leaseCount <= 0) return;
2189
+ this.leaseCount -= 1;
2190
+ if (this.leaseCount > 0 || !this.snapshot) return;
2191
+ const { noProxy, noProxyLower, applied } = this.snapshot;
2192
+ const currentNoProxy = process.env.NO_PROXY;
2193
+ const currentNoProxyLower = process.env.no_proxy;
2194
+ if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
2195
+ if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
2196
+ else delete process.env.NO_PROXY;
2197
+ if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
2198
+ else delete process.env.no_proxy;
2199
+ }
2200
+ this.snapshot = null;
2201
+ }
2202
+ };
2203
+ var noProxyLeaseManager = new NoProxyLeaseManager();
2204
+ async function withNoProxyForCdpUrl(url, fn) {
2205
+ const release = noProxyLeaseManager.acquire(url);
2206
+ try {
2207
+ return await fn();
2208
+ } finally {
2209
+ release?.();
2210
+ }
2125
2211
  }
2126
- function applyStealthToPage(page) {
2127
- page.evaluate(STEALTH_SCRIPT).catch((e) => {
2128
- if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
2129
- console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
2130
- });
2212
+ new http__default.default.Agent();
2213
+ new https__default.default.Agent();
2214
+ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
2215
+ const headers = { ...baseHeaders };
2216
+ try {
2217
+ const parsed = new URL(endpoint);
2218
+ if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
2219
+ if (parsed.username || parsed.password) {
2220
+ const credentials = Buffer.from(
2221
+ `${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
2222
+ ).toString("base64");
2223
+ headers.Authorization = `Basic ${credentials}`;
2224
+ }
2225
+ } catch {
2226
+ }
2227
+ return headers;
2131
2228
  }
2132
- function observeContext(context) {
2133
- if (observedContexts.has(context)) return;
2134
- observedContexts.add(context);
2135
- ensureContextState(context);
2136
- context.addInitScript(STEALTH_SCRIPT).catch((e) => {
2137
- if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
2138
- console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
2139
- });
2140
- for (const page of context.pages()) {
2141
- ensurePageState(page);
2142
- applyStealthToPage(page);
2229
+ var cachedByCdpUrl = /* @__PURE__ */ new Map();
2230
+ var connectingByCdpUrl = /* @__PURE__ */ new Map();
2231
+ var BlockedBrowserTargetError = class extends Error {
2232
+ constructor() {
2233
+ super("Browser target is unavailable after SSRF policy blocked its navigation.");
2234
+ this.name = "BlockedBrowserTargetError";
2143
2235
  }
2144
- context.on("page", (page) => {
2145
- ensurePageState(page);
2146
- applyStealthToPage(page);
2147
- });
2236
+ };
2237
+ var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
2238
+ var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
2239
+ function blockedTargetKey(cdpUrl, targetId) {
2240
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
2148
2241
  }
2149
- function observeBrowser(browser) {
2150
- for (const context of browser.contexts()) observeContext(context);
2242
+ function isBlockedTarget(cdpUrl, targetId) {
2243
+ const normalized = targetId?.trim() ?? "";
2244
+ if (normalized === "") return false;
2245
+ return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
2151
2246
  }
2152
- function rememberRoleRefsForTarget(opts) {
2153
- const targetId = opts.targetId.trim();
2154
- if (targetId === "") return;
2155
- roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
2156
- refs: opts.refs,
2157
- ...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
2158
- ...opts.mode !== void 0 ? { mode: opts.mode } : {}
2159
- });
2160
- while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
2161
- const first = roleRefsByTarget.keys().next();
2162
- if (first.done === true) break;
2163
- roleRefsByTarget.delete(first.value);
2247
+ function markTargetBlocked(cdpUrl, targetId) {
2248
+ const normalized = targetId?.trim() ?? "";
2249
+ if (normalized === "") return;
2250
+ blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
2251
+ }
2252
+ function clearBlockedTarget(cdpUrl, targetId) {
2253
+ const normalized = targetId?.trim() ?? "";
2254
+ if (normalized === "") return;
2255
+ blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
2256
+ }
2257
+ function hasBlockedTargetsForCdpUrl(cdpUrl) {
2258
+ const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
2259
+ for (const key of blockedTargetsByCdpUrl) {
2260
+ if (key.startsWith(prefix)) return true;
2164
2261
  }
2262
+ return false;
2165
2263
  }
2166
- function storeRoleRefsForTarget(opts) {
2167
- const state = ensurePageState(opts.page);
2168
- state.roleRefs = opts.refs;
2169
- state.roleRefsFrameSelector = opts.frameSelector;
2170
- state.roleRefsMode = opts.mode;
2171
- if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
2172
- rememberRoleRefsForTarget({
2173
- cdpUrl: opts.cdpUrl,
2174
- targetId: opts.targetId,
2175
- refs: opts.refs,
2176
- frameSelector: opts.frameSelector,
2177
- mode: opts.mode
2178
- });
2264
+ function blockedPageRefsForCdpUrl(cdpUrl) {
2265
+ const normalized = normalizeCdpUrl(cdpUrl);
2266
+ const existing = blockedPageRefsByCdpUrl.get(normalized);
2267
+ if (existing) return existing;
2268
+ const created = /* @__PURE__ */ new WeakSet();
2269
+ blockedPageRefsByCdpUrl.set(normalized, created);
2270
+ return created;
2179
2271
  }
2180
- function restoreRoleRefsForTarget(opts) {
2181
- const targetId = opts.targetId?.trim() ?? "";
2182
- if (targetId === "") return;
2183
- const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
2184
- if (!entry) return;
2185
- const state = ensurePageState(opts.page);
2186
- if (state.roleRefs) return;
2187
- state.roleRefs = entry.refs;
2188
- state.roleRefsFrameSelector = entry.frameSelector;
2189
- state.roleRefsMode = entry.mode;
2272
+ function isBlockedPageRef(cdpUrl, page) {
2273
+ return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
2274
+ }
2275
+ function markPageRefBlocked(cdpUrl, page) {
2276
+ blockedPageRefsForCdpUrl(cdpUrl).add(page);
2277
+ }
2278
+ function clearBlockedPageRef(cdpUrl, page) {
2279
+ blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
2280
+ }
2281
+ async function setDialogHandler(opts) {
2282
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2283
+ setDialogHandlerOnPage(page, opts.handler);
2190
2284
  }
2191
2285
  async function connectBrowser(cdpUrl, authToken) {
2192
2286
  const normalized = normalizeCdpUrl(cdpUrl);
@@ -2210,9 +2304,7 @@ async function connectBrowser(cdpUrl, authToken) {
2210
2304
  const onDisconnected = () => {
2211
2305
  if (cachedByCdpUrl.get(normalized)?.browser === browser) {
2212
2306
  cachedByCdpUrl.delete(normalized);
2213
- for (const key of roleRefsByTarget.keys()) {
2214
- if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
2215
- }
2307
+ clearRoleRefsForCdpUrl(normalized);
2216
2308
  }
2217
2309
  };
2218
2310
  const connected = { browser, cdpUrl: normalized, onDisconnected };
@@ -2476,87 +2568,12 @@ async function resolvePageByTargetIdOrThrow(opts) {
2476
2568
  if (!page) throw new BrowserTabNotFoundError();
2477
2569
  return page;
2478
2570
  }
2479
- function parseRoleRef(raw) {
2480
- const trimmed = raw.trim();
2481
- if (!trimmed) return null;
2482
- const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
2483
- return /^e\d+$/.test(normalized) ? normalized : null;
2484
- }
2485
- function requireRef(value) {
2486
- const raw = typeof value === "string" ? value.trim() : "";
2487
- const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
2488
- if (!ref) throw new Error("ref is required");
2489
- return ref;
2490
- }
2491
- function requireRefOrSelector(ref, selector) {
2492
- const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2493
- const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2494
- if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2495
- return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2496
- }
2497
- function resolveInteractionTimeoutMs(timeoutMs) {
2498
- return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2499
- }
2500
- function resolveBoundedDelayMs(value, label, maxMs) {
2501
- const normalized = Math.floor(value ?? 0);
2502
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2503
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
2504
- return normalized;
2505
- }
2506
2571
  async function getRestoredPageForTarget(opts) {
2507
2572
  const page = await getPageForTargetId(opts);
2508
2573
  ensurePageState(page);
2509
2574
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2510
2575
  return page;
2511
2576
  }
2512
- function refLocator(page, ref) {
2513
- const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
2514
- if (normalized.trim() === "") throw new Error("ref is required");
2515
- if (/^e\d+$/.test(normalized)) {
2516
- const state = pageStates.get(page);
2517
- if (state?.roleRefsMode === "aria") {
2518
- return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
2519
- }
2520
- const info = state?.roleRefs?.[normalized];
2521
- if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
2522
- const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
2523
- const role = info.role;
2524
- const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
2525
- return info.nth !== void 0 ? locator.nth(info.nth) : locator;
2526
- }
2527
- return page.locator(`aria-ref=${normalized}`);
2528
- }
2529
- function toAIFriendlyError(error, selector) {
2530
- const message = error instanceof Error ? error.message : String(error);
2531
- if (message.includes("strict mode violation")) {
2532
- const countMatch = /resolved to (\d+) elements/.exec(message);
2533
- const count = countMatch ? countMatch[1] : "multiple";
2534
- return new Error(
2535
- `Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
2536
- );
2537
- }
2538
- if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
2539
- return new Error(
2540
- `Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
2541
- );
2542
- }
2543
- if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
2544
- return new Error(
2545
- `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
2546
- );
2547
- }
2548
- const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
2549
- if (timeoutMatch) {
2550
- return new Error(
2551
- `Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
2552
- );
2553
- }
2554
- const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
2555
- return new Error(cleaned || message);
2556
- }
2557
- function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2558
- return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
2559
- }
2560
2577
 
2561
2578
  // src/actions/evaluate.ts
2562
2579
  async function evaluateInAllFramesViaPlaywright(opts) {
@@ -2581,7 +2598,8 @@ async function evaluateInAllFramesViaPlaywright(opts) {
2581
2598
  frameName: frame.name(),
2582
2599
  result
2583
2600
  });
2584
- } catch {
2601
+ } catch (err) {
2602
+ console.warn("[browserclaw] frame evaluate failed:", err instanceof Error ? err.message : String(err));
2585
2603
  }
2586
2604
  }
2587
2605
  return results;
@@ -3296,7 +3314,7 @@ async function pressAndHoldViaCdp(opts) {
3296
3314
  async function clickByTextViaPlaywright(opts) {
3297
3315
  const page = await getRestoredPageForTarget(opts);
3298
3316
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3299
- const locator = page.getByText(opts.text, { exact: opts.exact }).and(page.locator(":visible")).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
3317
+ const locator = page.getByText(opts.text, { exact: opts.exact }).or(page.getByTitle(opts.text, { exact: opts.exact })).and(page.locator(":visible")).first();
3300
3318
  try {
3301
3319
  await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3302
3320
  } catch (err) {
@@ -5064,6 +5082,7 @@ function axValue(v) {
5064
5082
  return "";
5065
5083
  }
5066
5084
  function formatAriaNodes(nodes, limit) {
5085
+ if (nodes.length === 0) return [];
5067
5086
  const byId = /* @__PURE__ */ new Map();
5068
5087
  for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
5069
5088
  const referenced = /* @__PURE__ */ new Set();