browserclaw 0.5.6 → 0.5.7
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/README.md +2 -1
- package/dist/index.cjs +137 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +135 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import fs from 'fs';
|
|
|
4
4
|
import net from 'net';
|
|
5
5
|
import { spawn, execFileSync } from 'child_process';
|
|
6
6
|
import { devices, chromium } from 'playwright-core';
|
|
7
|
+
import http from 'http';
|
|
8
|
+
import https from 'https';
|
|
7
9
|
import { lookup as lookup$1 } from 'dns/promises';
|
|
8
10
|
import { lookup } from 'dns';
|
|
9
11
|
import { realpath, rename, rm, lstat } from 'fs/promises';
|
|
@@ -1164,6 +1166,9 @@ function isWebSocketUrl(url) {
|
|
|
1164
1166
|
function isLoopbackHost(hostname) {
|
|
1165
1167
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1166
1168
|
}
|
|
1169
|
+
function hasProxyEnvConfigured() {
|
|
1170
|
+
return !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy);
|
|
1171
|
+
}
|
|
1167
1172
|
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1168
1173
|
const ws = new URL(wsUrl);
|
|
1169
1174
|
const cdp = new URL(cdpUrl);
|
|
@@ -1425,8 +1430,81 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1425
1430
|
} catch {
|
|
1426
1431
|
}
|
|
1427
1432
|
}
|
|
1428
|
-
var
|
|
1429
|
-
|
|
1433
|
+
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1434
|
+
function noProxyAlreadyCoversLocalhost() {
|
|
1435
|
+
const current = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
1436
|
+
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1437
|
+
}
|
|
1438
|
+
function isLoopbackCdpUrl(url) {
|
|
1439
|
+
try {
|
|
1440
|
+
return isLoopbackHost(new URL(url).hostname);
|
|
1441
|
+
} catch {
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
var NoProxyLeaseManager = class {
|
|
1446
|
+
leaseCount = 0;
|
|
1447
|
+
snapshot = null;
|
|
1448
|
+
acquire(url) {
|
|
1449
|
+
if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return null;
|
|
1450
|
+
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1451
|
+
const noProxy = process.env.NO_PROXY;
|
|
1452
|
+
const noProxyLower = process.env.no_proxy;
|
|
1453
|
+
const current = noProxy || noProxyLower || "";
|
|
1454
|
+
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1455
|
+
process.env.NO_PROXY = applied;
|
|
1456
|
+
process.env.no_proxy = applied;
|
|
1457
|
+
this.snapshot = { noProxy, noProxyLower, applied };
|
|
1458
|
+
}
|
|
1459
|
+
this.leaseCount += 1;
|
|
1460
|
+
let released = false;
|
|
1461
|
+
return () => {
|
|
1462
|
+
if (released) return;
|
|
1463
|
+
released = true;
|
|
1464
|
+
this.release();
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
release() {
|
|
1468
|
+
if (this.leaseCount <= 0) return;
|
|
1469
|
+
this.leaseCount -= 1;
|
|
1470
|
+
if (this.leaseCount > 0 || !this.snapshot) return;
|
|
1471
|
+
const { noProxy, noProxyLower, applied } = this.snapshot;
|
|
1472
|
+
const currentNoProxy = process.env.NO_PROXY;
|
|
1473
|
+
const currentNoProxyLower = process.env.no_proxy;
|
|
1474
|
+
if (currentNoProxy === applied && (currentNoProxyLower === applied || currentNoProxyLower === void 0)) {
|
|
1475
|
+
if (noProxy !== void 0) process.env.NO_PROXY = noProxy;
|
|
1476
|
+
else delete process.env.NO_PROXY;
|
|
1477
|
+
if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower;
|
|
1478
|
+
else delete process.env.no_proxy;
|
|
1479
|
+
}
|
|
1480
|
+
this.snapshot = null;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
var noProxyLeaseManager = new NoProxyLeaseManager();
|
|
1484
|
+
async function withNoProxyForCdpUrl(url, fn) {
|
|
1485
|
+
const release = noProxyLeaseManager.acquire(url);
|
|
1486
|
+
try {
|
|
1487
|
+
return await fn();
|
|
1488
|
+
} finally {
|
|
1489
|
+
release?.();
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
new http.Agent();
|
|
1493
|
+
new https.Agent();
|
|
1494
|
+
function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
1495
|
+
const headers = { ...baseHeaders };
|
|
1496
|
+
try {
|
|
1497
|
+
const parsed = new URL(endpoint);
|
|
1498
|
+
if (parsed.username && parsed.password) {
|
|
1499
|
+
const credentials = Buffer.from(`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`).toString("base64");
|
|
1500
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
1501
|
+
}
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
return headers;
|
|
1505
|
+
}
|
|
1506
|
+
var cachedByCdpUrl = /* @__PURE__ */ new Map();
|
|
1507
|
+
var connectingByCdpUrl = /* @__PURE__ */ new Map();
|
|
1430
1508
|
var pageStates = /* @__PURE__ */ new WeakMap();
|
|
1431
1509
|
var contextStates = /* @__PURE__ */ new WeakMap();
|
|
1432
1510
|
var observedContexts = /* @__PURE__ */ new WeakSet();
|
|
@@ -1464,6 +1542,13 @@ function normalizeCdpUrl(raw) {
|
|
|
1464
1542
|
function roleRefsKey(cdpUrl, targetId) {
|
|
1465
1543
|
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1466
1544
|
}
|
|
1545
|
+
function findNetworkRequestById(state, id) {
|
|
1546
|
+
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
1547
|
+
const candidate = state.requests[i];
|
|
1548
|
+
if (candidate && candidate.id === id) return candidate;
|
|
1549
|
+
}
|
|
1550
|
+
return void 0;
|
|
1551
|
+
}
|
|
1467
1552
|
function ensurePageState(page) {
|
|
1468
1553
|
const existing = pageStates.get(page);
|
|
1469
1554
|
if (existing) return existing;
|
|
@@ -1515,25 +1600,19 @@ function ensurePageState(page) {
|
|
|
1515
1600
|
const req = resp.request();
|
|
1516
1601
|
const id = state.requestIds.get(req);
|
|
1517
1602
|
if (!id) return;
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
rec.ok = resp.ok();
|
|
1523
|
-
break;
|
|
1524
|
-
}
|
|
1603
|
+
const rec = findNetworkRequestById(state, id);
|
|
1604
|
+
if (rec) {
|
|
1605
|
+
rec.status = resp.status();
|
|
1606
|
+
rec.ok = resp.ok();
|
|
1525
1607
|
}
|
|
1526
1608
|
});
|
|
1527
1609
|
page.on("requestfailed", (req) => {
|
|
1528
1610
|
const id = state.requestIds.get(req);
|
|
1529
1611
|
if (!id) return;
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
rec.ok = false;
|
|
1535
|
-
break;
|
|
1536
|
-
}
|
|
1612
|
+
const rec = findNetworkRequestById(state, id);
|
|
1613
|
+
if (rec) {
|
|
1614
|
+
rec.failureText = req.failure()?.errorText;
|
|
1615
|
+
rec.ok = false;
|
|
1537
1616
|
}
|
|
1538
1617
|
});
|
|
1539
1618
|
page.on("close", () => {
|
|
@@ -1568,12 +1647,8 @@ function observeContext(context) {
|
|
|
1568
1647
|
function observeBrowser(browser) {
|
|
1569
1648
|
for (const context of browser.contexts()) observeContext(context);
|
|
1570
1649
|
}
|
|
1571
|
-
function
|
|
1572
|
-
const
|
|
1573
|
-
state.roleRefs = opts.refs;
|
|
1574
|
-
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1575
|
-
state.roleRefsMode = opts.mode;
|
|
1576
|
-
const targetId = opts.targetId?.trim();
|
|
1650
|
+
function rememberRoleRefsForTarget(opts) {
|
|
1651
|
+
const targetId = opts.targetId.trim();
|
|
1577
1652
|
if (!targetId) return;
|
|
1578
1653
|
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
1579
1654
|
refs: opts.refs,
|
|
@@ -1586,6 +1661,20 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1586
1661
|
roleRefsByTarget.delete(first.value);
|
|
1587
1662
|
}
|
|
1588
1663
|
}
|
|
1664
|
+
function storeRoleRefsForTarget(opts) {
|
|
1665
|
+
const state = ensurePageState(opts.page);
|
|
1666
|
+
state.roleRefs = opts.refs;
|
|
1667
|
+
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1668
|
+
state.roleRefsMode = opts.mode;
|
|
1669
|
+
if (!opts.targetId?.trim()) return;
|
|
1670
|
+
rememberRoleRefsForTarget({
|
|
1671
|
+
cdpUrl: opts.cdpUrl,
|
|
1672
|
+
targetId: opts.targetId,
|
|
1673
|
+
refs: opts.refs,
|
|
1674
|
+
frameSelector: opts.frameSelector,
|
|
1675
|
+
mode: opts.mode
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1589
1678
|
function restoreRoleRefsForTarget(opts) {
|
|
1590
1679
|
const targetId = opts.targetId?.trim() || "";
|
|
1591
1680
|
if (!targetId) return;
|
|
@@ -1599,8 +1688,9 @@ function restoreRoleRefsForTarget(opts) {
|
|
|
1599
1688
|
}
|
|
1600
1689
|
async function connectBrowser(cdpUrl, authToken) {
|
|
1601
1690
|
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1602
|
-
|
|
1603
|
-
|
|
1691
|
+
const existing_cached = cachedByCdpUrl.get(normalized);
|
|
1692
|
+
if (existing_cached) return existing_cached;
|
|
1693
|
+
const existing = connectingByCdpUrl.get(normalized);
|
|
1604
1694
|
if (existing) return await existing;
|
|
1605
1695
|
const connectWithRetry = async () => {
|
|
1606
1696
|
let lastErr;
|
|
@@ -1608,17 +1698,17 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1608
1698
|
try {
|
|
1609
1699
|
const timeout = 5e3 + attempt * 2e3;
|
|
1610
1700
|
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
1611
|
-
const headers =
|
|
1612
|
-
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
1613
|
-
const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
|
|
1701
|
+
const headers = getHeadersWithAuth(endpoint);
|
|
1702
|
+
if (authToken && !headers["Authorization"]) headers["Authorization"] = `Bearer ${authToken}`;
|
|
1703
|
+
const browser = await withNoProxyForCdpUrl(endpoint, () => chromium.connectOverCDP(endpoint, { timeout, headers }));
|
|
1614
1704
|
const onDisconnected = () => {
|
|
1615
|
-
if (
|
|
1705
|
+
if (cachedByCdpUrl.get(normalized)?.browser === browser) cachedByCdpUrl.delete(normalized);
|
|
1616
1706
|
for (const key of roleRefsByTarget.keys()) {
|
|
1617
1707
|
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
1618
1708
|
}
|
|
1619
1709
|
};
|
|
1620
|
-
const connected = { browser, cdpUrl: normalized,
|
|
1621
|
-
|
|
1710
|
+
const connected = { browser, cdpUrl: normalized, onDisconnected };
|
|
1711
|
+
cachedByCdpUrl.set(normalized, connected);
|
|
1622
1712
|
observeBrowser(browser);
|
|
1623
1713
|
browser.on("disconnected", onDisconnected);
|
|
1624
1714
|
return connected;
|
|
@@ -1631,24 +1721,25 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1631
1721
|
throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
|
|
1632
1722
|
};
|
|
1633
1723
|
const promise = connectWithRetry().finally(() => {
|
|
1634
|
-
|
|
1724
|
+
connectingByCdpUrl.delete(normalized);
|
|
1635
1725
|
});
|
|
1636
|
-
|
|
1726
|
+
connectingByCdpUrl.set(normalized, promise);
|
|
1637
1727
|
return await promise;
|
|
1638
1728
|
}
|
|
1639
1729
|
async function disconnectBrowser() {
|
|
1640
|
-
if (
|
|
1641
|
-
for (const p of
|
|
1730
|
+
if (connectingByCdpUrl.size) {
|
|
1731
|
+
for (const p of connectingByCdpUrl.values()) {
|
|
1642
1732
|
try {
|
|
1643
1733
|
await p;
|
|
1644
1734
|
} catch {
|
|
1645
1735
|
}
|
|
1646
1736
|
}
|
|
1647
1737
|
}
|
|
1648
|
-
const cur
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
}
|
|
1738
|
+
for (const cur of cachedByCdpUrl.values()) {
|
|
1739
|
+
await cur.browser.close().catch(() => {
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
cachedByCdpUrl.clear();
|
|
1652
1743
|
}
|
|
1653
1744
|
function cdpSocketNeedsAttach(wsUrl) {
|
|
1654
1745
|
try {
|
|
@@ -1728,10 +1819,10 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1728
1819
|
}
|
|
1729
1820
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
1730
1821
|
const normalized = normalizeCdpUrl(opts.cdpUrl);
|
|
1731
|
-
const cur =
|
|
1732
|
-
if (!cur
|
|
1733
|
-
|
|
1734
|
-
|
|
1822
|
+
const cur = cachedByCdpUrl.get(normalized);
|
|
1823
|
+
if (!cur) return;
|
|
1824
|
+
cachedByCdpUrl.delete(normalized);
|
|
1825
|
+
connectingByCdpUrl.delete(normalized);
|
|
1735
1826
|
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
1736
1827
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
1737
1828
|
}
|
|
@@ -1774,7 +1865,6 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1774
1865
|
try {
|
|
1775
1866
|
const listUrl = `${normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)}/json/list`;
|
|
1776
1867
|
const headers = {};
|
|
1777
|
-
if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
|
|
1778
1868
|
const ctrl = new AbortController();
|
|
1779
1869
|
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
1780
1870
|
try {
|
|
@@ -2311,7 +2401,7 @@ function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
|
2311
2401
|
function isPrivateNetworkAllowedByPolicy(policy) {
|
|
2312
2402
|
return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
|
|
2313
2403
|
}
|
|
2314
|
-
function
|
|
2404
|
+
function hasProxyEnvConfigured2(env = process.env) {
|
|
2315
2405
|
for (const key of PROXY_ENV_KEYS) {
|
|
2316
2406
|
const value = env[key];
|
|
2317
2407
|
if (typeof value === "string" && value.trim().length > 0) return true;
|
|
@@ -2625,7 +2715,7 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
2625
2715
|
if (isAllowedNonNetworkNavigationUrl(parsed)) return;
|
|
2626
2716
|
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
2627
2717
|
}
|
|
2628
|
-
if (
|
|
2718
|
+
if (hasProxyEnvConfigured2() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
|
|
2629
2719
|
throw new InvalidBrowserNavigationUrlError(
|
|
2630
2720
|
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
|
|
2631
2721
|
);
|