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/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 cached = null;
1429
- var connectingByUrl = /* @__PURE__ */ new Map();
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
- for (let i = state.requests.length - 1; i >= 0; i--) {
1519
- const rec = state.requests[i];
1520
- if (rec && rec.id === id) {
1521
- rec.status = resp.status();
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
- for (let i = state.requests.length - 1; i >= 0; i--) {
1531
- const rec = state.requests[i];
1532
- if (rec && rec.id === id) {
1533
- rec.failureText = req.failure()?.errorText;
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 storeRoleRefsForTarget(opts) {
1572
- const state = ensurePageState(opts.page);
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
- if (cached?.cdpUrl === normalized) return cached;
1603
- const existing = connectingByUrl.get(normalized);
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 (cached?.browser === browser) cached = null;
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, authToken, onDisconnected };
1621
- cached = connected;
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
- connectingByUrl.delete(normalized);
1724
+ connectingByCdpUrl.delete(normalized);
1635
1725
  });
1636
- connectingByUrl.set(normalized, promise);
1726
+ connectingByCdpUrl.set(normalized, promise);
1637
1727
  return await promise;
1638
1728
  }
1639
1729
  async function disconnectBrowser() {
1640
- if (connectingByUrl.size) {
1641
- for (const p of connectingByUrl.values()) {
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 = cached;
1649
- cached = null;
1650
- if (cur) await cur.browser.close().catch(() => {
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 = cached;
1732
- if (!cur || cur.cdpUrl !== normalized) return;
1733
- cached = null;
1734
- connectingByUrl.delete(normalized);
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 hasProxyEnvConfigured(env = process.env) {
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 (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
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
  );