browserclaw 0.11.2 → 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 +349 -333
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -23
- package/dist/index.d.ts +25 -23
- package/dist/index.js +349 -333
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1782,137 +1782,10 @@ var STEALTH_SCRIPT = `(function() {
|
|
|
1782
1782
|
});
|
|
1783
1783
|
})()`;
|
|
1784
1784
|
|
|
1785
|
-
// src/
|
|
1786
|
-
var
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
this.name = "BrowserTabNotFoundError";
|
|
1790
|
-
}
|
|
1791
|
-
};
|
|
1792
|
-
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1793
|
-
const ctrl = new AbortController();
|
|
1794
|
-
const t = setTimeout(() => {
|
|
1795
|
-
ctrl.abort();
|
|
1796
|
-
}, timeoutMs);
|
|
1797
|
-
try {
|
|
1798
|
-
const res = await fetch(url, { signal: ctrl.signal });
|
|
1799
|
-
if (!res.ok) return null;
|
|
1800
|
-
return await res.json();
|
|
1801
|
-
} catch {
|
|
1802
|
-
return null;
|
|
1803
|
-
} finally {
|
|
1804
|
-
clearTimeout(t);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
function appendCdpPath2(cdpUrl, cdpPath) {
|
|
1808
|
-
try {
|
|
1809
|
-
const url = new URL(cdpUrl);
|
|
1810
|
-
url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
|
|
1811
|
-
return url.toString();
|
|
1812
|
-
} catch {
|
|
1813
|
-
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1817
|
-
const CDP_SESSION_TIMEOUT_MS = 1e4;
|
|
1818
|
-
const session = await Promise.race([
|
|
1819
|
-
page.context().newCDPSession(page),
|
|
1820
|
-
new Promise((_, reject) => {
|
|
1821
|
-
setTimeout(() => {
|
|
1822
|
-
reject(new Error("newCDPSession timed out after 10s"));
|
|
1823
|
-
}, CDP_SESSION_TIMEOUT_MS);
|
|
1824
|
-
})
|
|
1825
|
-
]);
|
|
1826
|
-
try {
|
|
1827
|
-
return await fn(session);
|
|
1828
|
-
} finally {
|
|
1829
|
-
await session.detach().catch(() => {
|
|
1830
|
-
});
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
async function withPageScopedCdpClient(opts) {
|
|
1834
|
-
return await withPlaywrightPageCdpSession(opts.page, async (session) => {
|
|
1835
|
-
return await opts.fn((method, params) => session.send(method, params));
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1839
|
-
function noProxyAlreadyCoversLocalhost() {
|
|
1840
|
-
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1841
|
-
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1842
|
-
}
|
|
1843
|
-
function isLoopbackCdpUrl(url) {
|
|
1844
|
-
try {
|
|
1845
|
-
return isLoopbackHost(new URL(url).hostname);
|
|
1846
|
-
} catch {
|
|
1847
|
-
return false;
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
var NoProxyLeaseManager = class {
|
|
1851
|
-
leaseCount = 0;
|
|
1852
|
-
snapshot = null;
|
|
1853
|
-
acquire(url) {
|
|
1854
|
-
if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
|
|
1855
|
-
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1856
|
-
const noProxy = process.env.NO_PROXY;
|
|
1857
|
-
const noProxyLower = process.env.no_proxy;
|
|
1858
|
-
const current = noProxy ?? noProxyLower ?? "";
|
|
1859
|
-
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1860
|
-
process.env.NO_PROXY = applied;
|
|
1861
|
-
process.env.no_proxy = applied;
|
|
1862
|
-
this.snapshot = { noProxy, noProxyLower, applied };
|
|
1863
|
-
}
|
|
1864
|
-
this.leaseCount += 1;
|
|
1865
|
-
let released = false;
|
|
1866
|
-
return () => {
|
|
1867
|
-
if (released) return;
|
|
1868
|
-
released = true;
|
|
1869
|
-
this.release();
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1872
|
-
release() {
|
|
1873
|
-
if (this.leaseCount <= 0) return;
|
|
1874
|
-
this.leaseCount -= 1;
|
|
1875
|
-
if (this.leaseCount > 0 || !this.snapshot) return;
|
|
1876
|
-
const { noProxy, noProxyLower, applied } = this.snapshot;
|
|
1877
|
-
const currentNoProxy = process.env.NO_PROXY;
|
|
1878
|
-
const currentNoProxyLower = process.env.no_proxy;
|
|
1879
|
-
if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
|
|
1880
|
-
if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
|
|
1881
|
-
else delete process.env.NO_PROXY;
|
|
1882
|
-
if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
|
|
1883
|
-
else delete process.env.no_proxy;
|
|
1884
|
-
}
|
|
1885
|
-
this.snapshot = null;
|
|
1886
|
-
}
|
|
1887
|
-
};
|
|
1888
|
-
var noProxyLeaseManager = new NoProxyLeaseManager();
|
|
1889
|
-
async function withNoProxyForCdpUrl(url, fn) {
|
|
1890
|
-
const release = noProxyLeaseManager.acquire(url);
|
|
1891
|
-
try {
|
|
1892
|
-
return await fn();
|
|
1893
|
-
} finally {
|
|
1894
|
-
release?.();
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
new http__default.default.Agent();
|
|
1898
|
-
new https__default.default.Agent();
|
|
1899
|
-
function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
1900
|
-
const headers = { ...baseHeaders };
|
|
1901
|
-
try {
|
|
1902
|
-
const parsed = new URL(endpoint);
|
|
1903
|
-
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
1904
|
-
if (parsed.username || parsed.password) {
|
|
1905
|
-
const credentials = Buffer.from(
|
|
1906
|
-
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
1907
|
-
).toString("base64");
|
|
1908
|
-
headers.Authorization = `Basic ${credentials}`;
|
|
1909
|
-
}
|
|
1910
|
-
} catch {
|
|
1911
|
-
}
|
|
1912
|
-
return headers;
|
|
1913
|
-
}
|
|
1914
|
-
var cachedByCdpUrl = /* @__PURE__ */ new Map();
|
|
1915
|
-
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;
|
|
1916
1789
|
var pageStates = /* @__PURE__ */ new WeakMap();
|
|
1917
1790
|
var contextStates = /* @__PURE__ */ new WeakMap();
|
|
1918
1791
|
var observedContexts = /* @__PURE__ */ new WeakSet();
|
|
@@ -1932,56 +1805,6 @@ function bumpDownloadArmId() {
|
|
|
1932
1805
|
nextDownloadArmId += 1;
|
|
1933
1806
|
return nextDownloadArmId;
|
|
1934
1807
|
}
|
|
1935
|
-
var BlockedBrowserTargetError = class extends Error {
|
|
1936
|
-
constructor() {
|
|
1937
|
-
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
1938
|
-
this.name = "BlockedBrowserTargetError";
|
|
1939
|
-
}
|
|
1940
|
-
};
|
|
1941
|
-
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
1942
|
-
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
1943
|
-
function blockedTargetKey(cdpUrl, targetId) {
|
|
1944
|
-
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1945
|
-
}
|
|
1946
|
-
function isBlockedTarget(cdpUrl, targetId) {
|
|
1947
|
-
const normalized = targetId?.trim() ?? "";
|
|
1948
|
-
if (normalized === "") return false;
|
|
1949
|
-
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
1950
|
-
}
|
|
1951
|
-
function markTargetBlocked(cdpUrl, targetId) {
|
|
1952
|
-
const normalized = targetId?.trim() ?? "";
|
|
1953
|
-
if (normalized === "") return;
|
|
1954
|
-
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
1955
|
-
}
|
|
1956
|
-
function clearBlockedTarget(cdpUrl, targetId) {
|
|
1957
|
-
const normalized = targetId?.trim() ?? "";
|
|
1958
|
-
if (normalized === "") return;
|
|
1959
|
-
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
1960
|
-
}
|
|
1961
|
-
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
1962
|
-
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
1963
|
-
for (const key of blockedTargetsByCdpUrl) {
|
|
1964
|
-
if (key.startsWith(prefix)) return true;
|
|
1965
|
-
}
|
|
1966
|
-
return false;
|
|
1967
|
-
}
|
|
1968
|
-
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
1969
|
-
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1970
|
-
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
1971
|
-
if (existing) return existing;
|
|
1972
|
-
const created = /* @__PURE__ */ new WeakSet();
|
|
1973
|
-
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
1974
|
-
return created;
|
|
1975
|
-
}
|
|
1976
|
-
function isBlockedPageRef(cdpUrl, page) {
|
|
1977
|
-
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
1978
|
-
}
|
|
1979
|
-
function markPageRefBlocked(cdpUrl, page) {
|
|
1980
|
-
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
1981
|
-
}
|
|
1982
|
-
function clearBlockedPageRef(cdpUrl, page) {
|
|
1983
|
-
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
1984
|
-
}
|
|
1985
1808
|
function ensureContextState(context) {
|
|
1986
1809
|
const existing = contextStates.get(context);
|
|
1987
1810
|
if (existing) return existing;
|
|
@@ -1989,16 +1812,8 @@ function ensureContextState(context) {
|
|
|
1989
1812
|
contextStates.set(context, state);
|
|
1990
1813
|
return state;
|
|
1991
1814
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
var MAX_CONSOLE_MESSAGES = 500;
|
|
1995
|
-
var MAX_PAGE_ERRORS = 200;
|
|
1996
|
-
var MAX_NETWORK_REQUESTS = 500;
|
|
1997
|
-
function normalizeCdpUrl(raw) {
|
|
1998
|
-
return raw.replace(/\/$/, "");
|
|
1999
|
-
}
|
|
2000
|
-
function roleRefsKey(cdpUrl, targetId) {
|
|
2001
|
-
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1815
|
+
function getPageState(page) {
|
|
1816
|
+
return pageStates.get(page);
|
|
2002
1817
|
}
|
|
2003
1818
|
function findNetworkRequestById(state, id) {
|
|
2004
1819
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
@@ -2119,77 +1934,353 @@ function ensurePageState(page) {
|
|
|
2119
1934
|
observedPages.delete(page);
|
|
2120
1935
|
});
|
|
2121
1936
|
}
|
|
2122
|
-
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
|
+
}
|
|
2123
2164
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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
|
+
}
|
|
2128
2211
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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;
|
|
2134
2228
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
2142
|
-
});
|
|
2143
|
-
for (const page of context.pages()) {
|
|
2144
|
-
ensurePageState(page);
|
|
2145
|
-
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";
|
|
2146
2235
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2236
|
+
};
|
|
2237
|
+
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
2238
|
+
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
2239
|
+
function blockedTargetKey(cdpUrl, targetId) {
|
|
2240
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
2151
2241
|
}
|
|
2152
|
-
function
|
|
2153
|
-
|
|
2242
|
+
function isBlockedTarget(cdpUrl, targetId) {
|
|
2243
|
+
const normalized = targetId?.trim() ?? "";
|
|
2244
|
+
if (normalized === "") return false;
|
|
2245
|
+
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
2154
2246
|
}
|
|
2155
|
-
function
|
|
2156
|
-
const
|
|
2157
|
-
if (
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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;
|
|
2167
2261
|
}
|
|
2262
|
+
return false;
|
|
2168
2263
|
}
|
|
2169
|
-
function
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
cdpUrl: opts.cdpUrl,
|
|
2177
|
-
targetId: opts.targetId,
|
|
2178
|
-
refs: opts.refs,
|
|
2179
|
-
frameSelector: opts.frameSelector,
|
|
2180
|
-
mode: opts.mode
|
|
2181
|
-
});
|
|
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;
|
|
2182
2271
|
}
|
|
2183
|
-
function
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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);
|
|
2193
2284
|
}
|
|
2194
2285
|
async function connectBrowser(cdpUrl, authToken) {
|
|
2195
2286
|
const normalized = normalizeCdpUrl(cdpUrl);
|
|
@@ -2213,9 +2304,7 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
2213
2304
|
const onDisconnected = () => {
|
|
2214
2305
|
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
2215
2306
|
cachedByCdpUrl.delete(normalized);
|
|
2216
|
-
|
|
2217
|
-
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
2218
|
-
}
|
|
2307
|
+
clearRoleRefsForCdpUrl(normalized);
|
|
2219
2308
|
}
|
|
2220
2309
|
};
|
|
2221
2310
|
const connected = { browser, cdpUrl: normalized, onDisconnected };
|
|
@@ -2479,87 +2568,12 @@ async function resolvePageByTargetIdOrThrow(opts) {
|
|
|
2479
2568
|
if (!page) throw new BrowserTabNotFoundError();
|
|
2480
2569
|
return page;
|
|
2481
2570
|
}
|
|
2482
|
-
function parseRoleRef(raw) {
|
|
2483
|
-
const trimmed = raw.trim();
|
|
2484
|
-
if (!trimmed) return null;
|
|
2485
|
-
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
|
|
2486
|
-
return /^e\d+$/.test(normalized) ? normalized : null;
|
|
2487
|
-
}
|
|
2488
|
-
function requireRef(value) {
|
|
2489
|
-
const raw = typeof value === "string" ? value.trim() : "";
|
|
2490
|
-
const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
|
|
2491
|
-
if (!ref) throw new Error("ref is required");
|
|
2492
|
-
return ref;
|
|
2493
|
-
}
|
|
2494
|
-
function requireRefOrSelector(ref, selector) {
|
|
2495
|
-
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2496
|
-
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2497
|
-
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2498
|
-
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2499
|
-
}
|
|
2500
|
-
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2501
|
-
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2502
|
-
}
|
|
2503
|
-
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2504
|
-
const normalized = Math.floor(value ?? 0);
|
|
2505
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2506
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2507
|
-
return normalized;
|
|
2508
|
-
}
|
|
2509
2571
|
async function getRestoredPageForTarget(opts) {
|
|
2510
2572
|
const page = await getPageForTargetId(opts);
|
|
2511
2573
|
ensurePageState(page);
|
|
2512
2574
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2513
2575
|
return page;
|
|
2514
2576
|
}
|
|
2515
|
-
function refLocator(page, ref) {
|
|
2516
|
-
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2517
|
-
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2518
|
-
if (/^e\d+$/.test(normalized)) {
|
|
2519
|
-
const state = pageStates.get(page);
|
|
2520
|
-
if (state?.roleRefsMode === "aria") {
|
|
2521
|
-
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2522
|
-
}
|
|
2523
|
-
const info = state?.roleRefs?.[normalized];
|
|
2524
|
-
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2525
|
-
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2526
|
-
const role = info.role;
|
|
2527
|
-
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2528
|
-
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2529
|
-
}
|
|
2530
|
-
return page.locator(`aria-ref=${normalized}`);
|
|
2531
|
-
}
|
|
2532
|
-
function toAIFriendlyError(error, selector) {
|
|
2533
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2534
|
-
if (message.includes("strict mode violation")) {
|
|
2535
|
-
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
2536
|
-
const count = countMatch ? countMatch[1] : "multiple";
|
|
2537
|
-
return new Error(
|
|
2538
|
-
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
2539
|
-
);
|
|
2540
|
-
}
|
|
2541
|
-
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
2542
|
-
return new Error(
|
|
2543
|
-
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
2544
|
-
);
|
|
2545
|
-
}
|
|
2546
|
-
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
2547
|
-
return new Error(
|
|
2548
|
-
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2549
|
-
);
|
|
2550
|
-
}
|
|
2551
|
-
const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
|
|
2552
|
-
if (timeoutMatch) {
|
|
2553
|
-
return new Error(
|
|
2554
|
-
`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.`
|
|
2555
|
-
);
|
|
2556
|
-
}
|
|
2557
|
-
const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
|
|
2558
|
-
return new Error(cleaned || message);
|
|
2559
|
-
}
|
|
2560
|
-
function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
2561
|
-
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
2562
|
-
}
|
|
2563
2577
|
|
|
2564
2578
|
// src/actions/evaluate.ts
|
|
2565
2579
|
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
@@ -2584,7 +2598,8 @@ async function evaluateInAllFramesViaPlaywright(opts) {
|
|
|
2584
2598
|
frameName: frame.name(),
|
|
2585
2599
|
result
|
|
2586
2600
|
});
|
|
2587
|
-
} catch {
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
console.warn("[browserclaw] frame evaluate failed:", err instanceof Error ? err.message : String(err));
|
|
2588
2603
|
}
|
|
2589
2604
|
}
|
|
2590
2605
|
return results;
|
|
@@ -3299,7 +3314,7 @@ async function pressAndHoldViaCdp(opts) {
|
|
|
3299
3314
|
async function clickByTextViaPlaywright(opts) {
|
|
3300
3315
|
const page = await getRestoredPageForTarget(opts);
|
|
3301
3316
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3302
|
-
const locator = page.getByText(opts.text, { exact: opts.exact }).
|
|
3317
|
+
const locator = page.getByText(opts.text, { exact: opts.exact }).or(page.getByTitle(opts.text, { exact: opts.exact })).and(page.locator(":visible")).first();
|
|
3303
3318
|
try {
|
|
3304
3319
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3305
3320
|
} catch (err) {
|
|
@@ -5067,6 +5082,7 @@ function axValue(v) {
|
|
|
5067
5082
|
return "";
|
|
5068
5083
|
}
|
|
5069
5084
|
function formatAriaNodes(nodes, limit) {
|
|
5085
|
+
if (nodes.length === 0) return [];
|
|
5070
5086
|
const byId = /* @__PURE__ */ new Map();
|
|
5071
5087
|
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
5072
5088
|
const referenced = /* @__PURE__ */ new Set();
|