browserclaw 0.2.8 → 0.3.0

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
@@ -1393,6 +1393,32 @@ var InvalidBrowserNavigationUrlError = class extends Error {
1393
1393
  this.name = "InvalidBrowserNavigationUrlError";
1394
1394
  }
1395
1395
  };
1396
+ function withBrowserNavigationPolicy(ssrfPolicy) {
1397
+ return { ssrfPolicy };
1398
+ }
1399
+ async function assertBrowserNavigationAllowed(opts) {
1400
+ const policy = opts.ssrfPolicy;
1401
+ if (policy?.allowPrivateNetwork) return;
1402
+ const allowedHostnames = [
1403
+ ...policy?.allowedHostnames ?? [],
1404
+ ...policy?.hostnameAllowlist ?? []
1405
+ ];
1406
+ if (allowedHostnames.length) {
1407
+ let parsed;
1408
+ try {
1409
+ parsed = new URL(opts.url);
1410
+ } catch {
1411
+ throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${opts.url}"`);
1412
+ }
1413
+ const hostname = parsed.hostname.toLowerCase();
1414
+ if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1415
+ }
1416
+ if (await isInternalUrlResolved(opts.url, opts.lookupFn)) {
1417
+ throw new InvalidBrowserNavigationUrlError(
1418
+ `Navigation to internal/loopback address blocked: "${opts.url}". Use ssrfPolicy: { allowPrivateNetwork: true } if this is intentional.`
1419
+ );
1420
+ }
1421
+ }
1396
1422
  function assertSafeOutputPath(path2, allowedRoots) {
1397
1423
  if (!path2 || typeof path2 !== "string") {
1398
1424
  throw new Error("Output path is required.");
@@ -1515,7 +1541,7 @@ function isInternalUrl(url) {
1515
1541
  }
1516
1542
  return false;
1517
1543
  }
1518
- async function isInternalUrlResolved(url) {
1544
+ async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1519
1545
  if (isInternalUrl(url)) return true;
1520
1546
  let parsed;
1521
1547
  try {
@@ -1524,7 +1550,7 @@ async function isInternalUrlResolved(url) {
1524
1550
  return true;
1525
1551
  }
1526
1552
  try {
1527
- const { address } = await promises.lookup(parsed.hostname);
1553
+ const { address } = await lookupFn(parsed.hostname);
1528
1554
  if (isInternalIP(address)) return true;
1529
1555
  } catch {
1530
1556
  return true;
@@ -1536,9 +1562,8 @@ async function isInternalUrlResolved(url) {
1536
1562
  async function navigateViaPlaywright(opts) {
1537
1563
  const url = String(opts.url ?? "").trim();
1538
1564
  if (!url) throw new Error("url is required");
1539
- if (!opts.allowInternal && await isInternalUrlResolved(url)) {
1540
- throw new InvalidBrowserNavigationUrlError(`Navigation to internal/loopback address blocked: "${url}". Set allowInternal: true if this is intentional.`);
1541
- }
1565
+ const policy = opts.allowInternal ? { ...opts.ssrfPolicy, allowPrivateNetwork: true } : opts.ssrfPolicy;
1566
+ await assertBrowserNavigationAllowed({ url, ssrfPolicy: policy });
1542
1567
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1543
1568
  ensurePageState(page);
1544
1569
  await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
@@ -1561,8 +1586,9 @@ async function listPagesViaPlaywright(opts) {
1561
1586
  }
1562
1587
  async function createPageViaPlaywright(opts) {
1563
1588
  const targetUrl = (opts.url ?? "").trim() || "about:blank";
1564
- if (targetUrl !== "about:blank" && !opts.allowInternal && await isInternalUrlResolved(targetUrl)) {
1565
- throw new InvalidBrowserNavigationUrlError(`Navigation to internal/loopback address blocked: "${targetUrl}". Set allowInternal: true if this is intentional.`);
1589
+ if (targetUrl !== "about:blank") {
1590
+ const policy = opts.allowInternal ? { ...opts.ssrfPolicy, allowPrivateNetwork: true } : opts.ssrfPolicy;
1591
+ await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
1566
1592
  }
1567
1593
  const { browser } = await connectBrowser(opts.cdpUrl);
1568
1594
  const context = browser.contexts()[0] ?? await browser.newContext();
@@ -1682,6 +1708,7 @@ async function evaluateViaPlaywright(opts) {
1682
1708
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1683
1709
  ensurePageState(page);
1684
1710
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1711
+ const timeout = opts.timeoutMs != null ? opts.timeoutMs : void 0;
1685
1712
  if (opts.ref) {
1686
1713
  const locator = refLocator(page, opts.ref);
1687
1714
  return await locator.evaluate(
@@ -1694,10 +1721,11 @@ async function evaluateViaPlaywright(opts) {
1694
1721
  throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
1695
1722
  }
1696
1723
  },
1697
- fnText
1724
+ fnText,
1725
+ { timeout }
1698
1726
  );
1699
1727
  }
1700
- return await page.evaluate(
1728
+ const evalPromise = page.evaluate(
1701
1729
  // eslint-disable-next-line no-eval
1702
1730
  (fnBody) => {
1703
1731
  try {
@@ -1709,6 +1737,13 @@ async function evaluateViaPlaywright(opts) {
1709
1737
  },
1710
1738
  fnText
1711
1739
  );
1740
+ if (!opts.signal) return evalPromise;
1741
+ return Promise.race([
1742
+ evalPromise,
1743
+ new Promise((_, reject) => {
1744
+ opts.signal.addEventListener("abort", () => reject(new Error("Evaluate aborted")), { once: true });
1745
+ })
1746
+ ]);
1712
1747
  }
1713
1748
 
1714
1749
  // src/actions/download.ts
@@ -2070,12 +2105,12 @@ async function storageClearViaPlaywright(opts) {
2070
2105
  var CrawlPage = class {
2071
2106
  cdpUrl;
2072
2107
  targetId;
2073
- allowInternal;
2108
+ ssrfPolicy;
2074
2109
  /** @internal */
2075
- constructor(cdpUrl, targetId, allowInternal = false) {
2110
+ constructor(cdpUrl, targetId, ssrfPolicy) {
2076
2111
  this.cdpUrl = cdpUrl;
2077
2112
  this.targetId = targetId;
2078
- this.allowInternal = allowInternal;
2113
+ this.ssrfPolicy = ssrfPolicy;
2079
2114
  }
2080
2115
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
2081
2116
  get id() {
@@ -2408,7 +2443,7 @@ var CrawlPage = class {
2408
2443
  targetId: this.targetId,
2409
2444
  url,
2410
2445
  timeoutMs: opts?.timeoutMs,
2411
- allowInternal: this.allowInternal
2446
+ ssrfPolicy: this.ssrfPolicy
2412
2447
  });
2413
2448
  }
2414
2449
  /**
@@ -2487,7 +2522,9 @@ var CrawlPage = class {
2487
2522
  cdpUrl: this.cdpUrl,
2488
2523
  targetId: this.targetId,
2489
2524
  fn,
2490
- ref: opts?.ref
2525
+ ref: opts?.ref,
2526
+ timeoutMs: opts?.timeoutMs,
2527
+ signal: opts?.signal
2491
2528
  });
2492
2529
  }
2493
2530
  /**
@@ -2938,12 +2975,12 @@ var CrawlPage = class {
2938
2975
  };
2939
2976
  var BrowserClaw = class _BrowserClaw {
2940
2977
  cdpUrl;
2941
- allowInternal;
2978
+ ssrfPolicy;
2942
2979
  chrome;
2943
- constructor(cdpUrl, chrome, allowInternal = false) {
2980
+ constructor(cdpUrl, chrome, ssrfPolicy) {
2944
2981
  this.cdpUrl = cdpUrl;
2945
2982
  this.chrome = chrome;
2946
- this.allowInternal = allowInternal;
2983
+ this.ssrfPolicy = ssrfPolicy;
2947
2984
  }
2948
2985
  /**
2949
2986
  * Launch a new Chrome instance and connect to it.
@@ -2971,7 +3008,8 @@ var BrowserClaw = class _BrowserClaw {
2971
3008
  static async launch(opts = {}) {
2972
3009
  const chrome = await launchChrome(opts);
2973
3010
  const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
2974
- return new _BrowserClaw(cdpUrl, chrome, opts.allowInternal);
3011
+ const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, allowPrivateNetwork: true } : opts.ssrfPolicy;
3012
+ return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
2975
3013
  }
2976
3014
  /**
2977
3015
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -2992,7 +3030,8 @@ var BrowserClaw = class _BrowserClaw {
2992
3030
  throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
2993
3031
  }
2994
3032
  await connectBrowser(cdpUrl, opts?.authToken);
2995
- return new _BrowserClaw(cdpUrl, null, opts?.allowInternal);
3033
+ const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, allowPrivateNetwork: true } : opts?.ssrfPolicy;
3034
+ return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
2996
3035
  }
2997
3036
  /**
2998
3037
  * Open a URL in a new tab and return the page handle.
@@ -3007,8 +3046,8 @@ var BrowserClaw = class _BrowserClaw {
3007
3046
  * ```
3008
3047
  */
3009
3048
  async open(url) {
3010
- const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, allowInternal: this.allowInternal });
3011
- return new CrawlPage(this.cdpUrl, tab.targetId, this.allowInternal);
3049
+ const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, ssrfPolicy: this.ssrfPolicy });
3050
+ return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
3012
3051
  }
3013
3052
  /**
3014
3053
  * Get a CrawlPage handle for the currently active tab.
@@ -3021,7 +3060,7 @@ var BrowserClaw = class _BrowserClaw {
3021
3060
  if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
3022
3061
  const tid = await pageTargetId(pages[0]).catch(() => null);
3023
3062
  if (!tid) throw new Error("Failed to get targetId for the current page.");
3024
- return new CrawlPage(this.cdpUrl, tid, this.allowInternal);
3063
+ return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
3025
3064
  }
3026
3065
  /**
3027
3066
  * List all open tabs.
@@ -3056,7 +3095,7 @@ var BrowserClaw = class _BrowserClaw {
3056
3095
  * @returns CrawlPage for the specified tab
3057
3096
  */
3058
3097
  page(targetId) {
3059
- return new CrawlPage(this.cdpUrl, targetId, this.allowInternal);
3098
+ return new CrawlPage(this.cdpUrl, targetId, this.ssrfPolicy);
3060
3099
  }
3061
3100
  /** The CDP endpoint URL for this browser connection. */
3062
3101
  get url() {
@@ -3081,5 +3120,7 @@ var BrowserClaw = class _BrowserClaw {
3081
3120
  exports.BrowserClaw = BrowserClaw;
3082
3121
  exports.CrawlPage = CrawlPage;
3083
3122
  exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
3123
+ exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
3124
+ exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
3084
3125
  //# sourceMappingURL=index.cjs.map
3085
3126
  //# sourceMappingURL=index.cjs.map