browserclaw 0.11.3 → 0.11.5

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
@@ -1319,7 +1319,10 @@ function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) {
1319
1319
  url.pathname = url.pathname.replace(/\/cdp$/, "");
1320
1320
  return url.toString().replace(/\/$/, "");
1321
1321
  } catch {
1322
- return cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:").replace(/\/devtools\/browser\/.*$/, "").replace(/\/cdp$/, "").replace(/\/$/, "");
1322
+ let normalized = cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
1323
+ const dtIdx = normalized.indexOf("/devtools/browser/");
1324
+ if (dtIdx >= 0) normalized = normalized.slice(0, dtIdx);
1325
+ return normalized.replace(/\/cdp$/, "").replace(/\/$/, "");
1323
1326
  }
1324
1327
  }
1325
1328
  function appendCdpPath(cdpUrl, cdpPath) {
@@ -1790,20 +1793,17 @@ var pageStates = /* @__PURE__ */ new WeakMap();
1790
1793
  var contextStates = /* @__PURE__ */ new WeakMap();
1791
1794
  var observedContexts = /* @__PURE__ */ new WeakSet();
1792
1795
  var observedPages = /* @__PURE__ */ new WeakSet();
1793
- var nextUploadArmId = 0;
1794
- var nextDialogArmId = 0;
1795
- var nextDownloadArmId = 0;
1796
- function bumpUploadArmId() {
1797
- nextUploadArmId += 1;
1798
- return nextUploadArmId;
1796
+ function bumpUploadArmId(state) {
1797
+ state.nextArmIdUpload += 1;
1798
+ return state.nextArmIdUpload;
1799
1799
  }
1800
- function bumpDialogArmId() {
1801
- nextDialogArmId += 1;
1802
- return nextDialogArmId;
1800
+ function bumpDialogArmId(state) {
1801
+ state.nextArmIdDialog += 1;
1802
+ return state.nextArmIdDialog;
1803
1803
  }
1804
- function bumpDownloadArmId() {
1805
- nextDownloadArmId += 1;
1806
- return nextDownloadArmId;
1804
+ function bumpDownloadArmId(state) {
1805
+ state.nextArmIdDownload += 1;
1806
+ return state.nextArmIdDownload;
1807
1807
  }
1808
1808
  function ensureContextState(context) {
1809
1809
  const existing = contextStates.get(context);
@@ -1833,7 +1833,10 @@ function ensurePageState(page) {
1833
1833
  nextRequestId: 0,
1834
1834
  armIdUpload: 0,
1835
1835
  armIdDialog: 0,
1836
- armIdDownload: 0
1836
+ armIdDownload: 0,
1837
+ nextArmIdUpload: 0,
1838
+ nextArmIdDialog: 0,
1839
+ nextArmIdDownload: 0
1837
1840
  };
1838
1841
  pageStates.set(page, state);
1839
1842
  if (!observedPages.has(page)) {
@@ -1891,6 +1894,7 @@ function ensurePageState(page) {
1891
1894
  page.on("dialog", (dialog) => {
1892
1895
  if (state.armIdDialog > 0) return;
1893
1896
  if (state.dialogHandler) {
1897
+ const handler = state.dialogHandler;
1894
1898
  let handled = false;
1895
1899
  const event = {
1896
1900
  type: dialog.type(),
@@ -1905,7 +1909,7 @@ function ensurePageState(page) {
1905
1909
  return dialog.dismiss();
1906
1910
  }
1907
1911
  };
1908
- Promise.resolve(state.dialogHandler(event)).then(() => {
1912
+ Promise.resolve().then(() => handler(event)).then(() => {
1909
1913
  if (!handled) {
1910
1914
  dialog.dismiss().catch((err) => {
1911
1915
  console.warn(
@@ -2113,7 +2117,9 @@ async function fetchJsonForCdp(url, timeoutMs) {
2113
2117
  const res = await fetch(url, { signal: ctrl.signal });
2114
2118
  if (!res.ok) return null;
2115
2119
  return await res.json();
2116
- } catch {
2120
+ } catch (err) {
2121
+ if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
2122
+ console.warn(`[browserclaw] fetchJsonForCdp ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
2117
2123
  return null;
2118
2124
  } finally {
2119
2125
  clearTimeout(t);
@@ -2162,51 +2168,41 @@ function isLoopbackCdpUrl(url) {
2162
2168
  return false;
2163
2169
  }
2164
2170
  }
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;
2171
+ var envMutexPromise = Promise.resolve();
2172
+ async function withNoProxyForCdpUrl(url, fn) {
2173
+ if (!isLoopbackCdpUrl(url) || !hasProxyEnvConfigured()) return fn();
2174
+ const prev = envMutexPromise;
2175
+ let release = () => {
2176
+ };
2177
+ envMutexPromise = new Promise((r) => {
2178
+ release = r;
2179
+ });
2180
+ await prev;
2181
+ if (noProxyAlreadyCoversLocalhost()) {
2182
+ try {
2183
+ return await fn();
2184
+ } finally {
2185
+ release();
2199
2186
  }
2200
- this.snapshot = null;
2201
2187
  }
2202
- };
2203
- var noProxyLeaseManager = new NoProxyLeaseManager();
2204
- async function withNoProxyForCdpUrl(url, fn) {
2205
- const release = noProxyLeaseManager.acquire(url);
2188
+ const savedNoProxy = process.env.NO_PROXY;
2189
+ const savedNoProxyLower = process.env.no_proxy;
2190
+ const current = savedNoProxy ?? savedNoProxyLower ?? "";
2191
+ const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
2192
+ process.env.NO_PROXY = applied;
2193
+ process.env.no_proxy = applied;
2206
2194
  try {
2207
2195
  return await fn();
2208
2196
  } finally {
2209
- release?.();
2197
+ if (process.env.NO_PROXY === applied) {
2198
+ if (savedNoProxy !== void 0) process.env.NO_PROXY = savedNoProxy;
2199
+ else delete process.env.NO_PROXY;
2200
+ }
2201
+ if (process.env.no_proxy === applied) {
2202
+ if (savedNoProxyLower !== void 0) process.env.no_proxy = savedNoProxyLower;
2203
+ else delete process.env.no_proxy;
2204
+ }
2205
+ release();
2210
2206
  }
2211
2207
  }
2212
2208
  new http__default.default.Agent();
@@ -2331,7 +2327,10 @@ async function disconnectBrowser() {
2331
2327
  for (const p of connectingByCdpUrl.values()) {
2332
2328
  try {
2333
2329
  await p;
2334
- } catch {
2330
+ } catch (err) {
2331
+ console.warn(
2332
+ `[browserclaw] disconnectBrowser: pending connect failed: ${err instanceof Error ? err.message : String(err)}`
2333
+ );
2335
2334
  }
2336
2335
  }
2337
2336
  }
@@ -2436,7 +2435,7 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
2436
2435
  };
2437
2436
  });
2438
2437
  }
2439
- async function forceDisconnectPlaywrightForTarget(opts) {
2438
+ async function forceDisconnectPlaywrightConnection(opts) {
2440
2439
  const normalized = normalizeCdpUrl(opts.cdpUrl);
2441
2440
  const cur = cachedByCdpUrl.get(normalized);
2442
2441
  if (!cur) return;
@@ -2453,15 +2452,21 @@ async function forceDisconnectPlaywrightForTarget(opts) {
2453
2452
  cur.browser.close().catch(() => {
2454
2453
  });
2455
2454
  }
2455
+ var forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightConnection;
2456
2456
  function getAllPages(browser) {
2457
2457
  return browser.contexts().flatMap((c) => c.pages());
2458
2458
  }
2459
+ var pageTargetIdCache = /* @__PURE__ */ new WeakMap();
2459
2460
  async function pageTargetId(page) {
2461
+ const cached = pageTargetIdCache.get(page);
2462
+ if (cached !== void 0) return cached;
2460
2463
  const session = await page.context().newCDPSession(page);
2461
2464
  try {
2462
2465
  const info = await session.send("Target.getTargetInfo");
2463
2466
  const targetInfo = info.targetInfo;
2464
- return (targetInfo?.targetId ?? "").trim() || null;
2467
+ const id = (targetInfo?.targetId ?? "").trim() || null;
2468
+ if (id !== null) pageTargetIdCache.set(page, id);
2469
+ return id;
2465
2470
  } finally {
2466
2471
  await session.detach().catch(() => {
2467
2472
  });
@@ -2491,17 +2496,19 @@ async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
2491
2496
  }
2492
2497
  async function findPageByTargetId(browser, targetId, cdpUrl) {
2493
2498
  const pages = getAllPages(browser);
2494
- let resolvedViaCdp = false;
2495
- for (const page of pages) {
2496
- let tid = null;
2497
- try {
2498
- tid = await pageTargetId(page);
2499
- resolvedViaCdp = true;
2500
- } catch {
2501
- tid = null;
2502
- }
2503
- if (tid !== null && tid !== "" && tid === targetId) return page;
2504
- }
2499
+ const results = await Promise.all(
2500
+ pages.map(async (page) => {
2501
+ try {
2502
+ const tid = await pageTargetId(page);
2503
+ return { page, tid };
2504
+ } catch {
2505
+ return { page, tid: null };
2506
+ }
2507
+ })
2508
+ );
2509
+ const resolvedViaCdp = results.some(({ tid }) => tid !== null);
2510
+ const matched = results.find(({ tid }) => tid !== null && tid !== "" && tid === targetId);
2511
+ if (matched) return matched.page;
2505
2512
  if (cdpUrl !== void 0 && cdpUrl !== "") {
2506
2513
  try {
2507
2514
  return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
@@ -2557,9 +2564,7 @@ async function getPageForTargetId(opts) {
2557
2564
  );
2558
2565
  }
2559
2566
  if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
2560
- const foundTargetId = await pageTargetId(found).catch(() => null);
2561
- if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
2562
- throw new BlockedBrowserTargetError();
2567
+ if (isBlockedTarget(opts.cdpUrl, opts.targetId)) throw new BlockedBrowserTargetError();
2563
2568
  return found;
2564
2569
  }
2565
2570
  async function resolvePageByTargetIdOrThrow(opts) {
@@ -2685,7 +2690,7 @@ async function evaluateViaPlaywright(opts) {
2685
2690
  }
2686
2691
  if (signal !== void 0) {
2687
2692
  const disconnect = () => {
2688
- forceDisconnectPlaywrightForTarget({
2693
+ forceDisconnectPlaywrightConnection({
2689
2694
  cdpUrl: opts.cdpUrl,
2690
2695
  targetId: opts.targetId}).catch(() => {
2691
2696
  });
@@ -2752,7 +2757,6 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
2752
2757
  }
2753
2758
  var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
2754
2759
  var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
2755
- var PROXY_ENV_KEYS2 = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
2756
2760
  var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
2757
2761
  function isAllowedNonNetworkNavigationUrl(parsed) {
2758
2762
  return SAFE_NON_NETWORK_URLS.has(parsed.href);
@@ -2760,13 +2764,6 @@ function isAllowedNonNetworkNavigationUrl(parsed) {
2760
2764
  function isPrivateNetworkAllowedByPolicy(policy) {
2761
2765
  return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
2762
2766
  }
2763
- function hasProxyEnvConfigured2(env = process.env) {
2764
- for (const key of PROXY_ENV_KEYS2) {
2765
- const value = env[key];
2766
- if (typeof value === "string" && value.trim().length > 0) return true;
2767
- }
2768
- return false;
2769
- }
2770
2767
  function normalizeHostname(hostname) {
2771
2768
  let h = hostname.trim().toLowerCase();
2772
2769
  if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
@@ -3077,7 +3074,7 @@ async function assertBrowserNavigationAllowed(opts) {
3077
3074
  if (isAllowedNonNetworkNavigationUrl(parsed)) return;
3078
3075
  throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
3079
3076
  }
3080
- if (hasProxyEnvConfigured2() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
3077
+ if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
3081
3078
  throw new InvalidBrowserNavigationUrlError(
3082
3079
  "Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
3083
3080
  );
@@ -3532,7 +3529,7 @@ async function armDialogViaPlaywright(opts) {
3532
3529
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3533
3530
  const state = ensurePageState(page);
3534
3531
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3535
- state.armIdDialog = bumpDialogArmId();
3532
+ state.armIdDialog = bumpDialogArmId(state);
3536
3533
  const armId = state.armIdDialog;
3537
3534
  page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
3538
3535
  if (state.armIdDialog !== armId) return;
@@ -3550,7 +3547,7 @@ async function armFileUploadViaPlaywright(opts) {
3550
3547
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3551
3548
  const state = ensurePageState(page);
3552
3549
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
3553
- state.armIdUpload = bumpUploadArmId();
3550
+ state.armIdUpload = bumpUploadArmId(state);
3554
3551
  const armId = state.armIdUpload;
3555
3552
  page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
3556
3553
  if (state.armIdUpload !== armId) return;
@@ -3710,7 +3707,7 @@ async function navigateViaPlaywright(opts) {
3710
3707
  response = await navigate();
3711
3708
  } catch (err) {
3712
3709
  if (!isRetryableNavigateError(err)) throw err;
3713
- await forceDisconnectPlaywrightForTarget({
3710
+ await forceDisconnectPlaywrightConnection({
3714
3711
  cdpUrl: opts.cdpUrl,
3715
3712
  targetId: opts.targetId}).catch(() => {
3716
3713
  });
@@ -3852,18 +3849,12 @@ async function resizeViewportViaPlaywright(opts) {
3852
3849
 
3853
3850
  // src/actions/wait.ts
3854
3851
  var MAX_WAIT_TIME_MS = 3e4;
3855
- function resolveBoundedDelayMs2(value, label, maxMs) {
3856
- const normalized = Math.floor(value ?? 0);
3857
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
3858
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
3859
- return normalized;
3860
- }
3861
3852
  async function waitForViaPlaywright(opts) {
3862
3853
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3863
3854
  ensurePageState(page);
3864
3855
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
3865
3856
  if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
3866
- await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3857
+ await page.waitForTimeout(resolveBoundedDelayMs(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
3867
3858
  }
3868
3859
  if (opts.text !== void 0 && opts.text !== "") {
3869
3860
  await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
@@ -4120,7 +4111,7 @@ async function downloadViaPlaywright(opts) {
4120
4111
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4121
4112
  const outPath = opts.path.trim();
4122
4113
  if (!outPath) throw new Error("path is required");
4123
- state.armIdDownload = bumpDownloadArmId();
4114
+ state.armIdDownload = bumpDownloadArmId(state);
4124
4115
  const armId = state.armIdDownload;
4125
4116
  const waiter = createPageDownloadWaiter(page, timeout);
4126
4117
  try {
@@ -4140,7 +4131,7 @@ async function waitForDownloadViaPlaywright(opts) {
4140
4131
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4141
4132
  const state = ensurePageState(page);
4142
4133
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
4143
- state.armIdDownload = bumpDownloadArmId();
4134
+ state.armIdDownload = bumpDownloadArmId(state);
4144
4135
  const armId = state.armIdDownload;
4145
4136
  const waiter = createPageDownloadWaiter(page, timeout);
4146
4137
  try {
@@ -6577,6 +6568,7 @@ var BrowserClaw = class _BrowserClaw {
6577
6568
  }
6578
6569
  };
6579
6570
 
6571
+ exports.BlockedBrowserTargetError = BlockedBrowserTargetError;
6580
6572
  exports.BrowserClaw = BrowserClaw;
6581
6573
  exports.BrowserTabNotFoundError = BrowserTabNotFoundError;
6582
6574
  exports.CrawlPage = CrawlPage;
@@ -6591,6 +6583,7 @@ exports.createPinnedLookup = createPinnedLookup;
6591
6583
  exports.detectChallengeViaPlaywright = detectChallengeViaPlaywright;
6592
6584
  exports.ensureContextState = ensureContextState;
6593
6585
  exports.executeSingleAction = executeSingleAction;
6586
+ exports.forceDisconnectPlaywrightConnection = forceDisconnectPlaywrightConnection;
6594
6587
  exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
6595
6588
  exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
6596
6589
  exports.getRestoredPageForTarget = getRestoredPageForTarget;
@@ -6606,6 +6599,7 @@ exports.resolveBoundedDelayMs = resolveBoundedDelayMs;
6606
6599
  exports.resolveInteractionTimeoutMs = resolveInteractionTimeoutMs;
6607
6600
  exports.resolvePageByTargetIdOrThrow = resolvePageByTargetIdOrThrow;
6608
6601
  exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
6602
+ exports.resolveStrictExistingPathsWithinRoot = resolveStrictExistingPathsWithinRoot;
6609
6603
  exports.resolveStrictExistingUploadPaths = resolveStrictExistingUploadPaths;
6610
6604
  exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
6611
6605
  exports.setDialogHandler = setDialogHandler;