browserclaw 0.3.7 → 0.3.9

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 CHANGED
@@ -1,4 +1,4 @@
1
- <h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</h1>
1
+ <h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</h2>
2
2
 
3
3
  <p align="center">
4
4
  <a href="https://www.npmjs.com/package/browserclaw"><img src="https://img.shields.io/npm/v/browserclaw.svg" alt="npm version" /></a>
@@ -217,13 +217,13 @@ await page.press('Meta+Shift+p');
217
217
 
218
218
  // Fill multiple form fields at once
219
219
  await page.fill([
220
- { ref: 'e2', type: 'text', value: 'Jane Doe' },
221
- { ref: 'e4', type: 'text', value: 'jane@example.com' },
220
+ { ref: 'e2', value: 'Jane Doe' },
221
+ { ref: 'e4', value: 'jane@example.com' },
222
222
  { ref: 'e6', type: 'checkbox', value: true },
223
223
  ]);
224
224
  ```
225
225
 
226
- `fill()` field types: `'text'` calls Playwright `fill()` with the string value. `'checkbox'` and `'radio'` call `setChecked()` — truthy values are `true`, `1`, `'1'`, `'true'`. Empty ref or type throws.
226
+ `fill()` field types: `'text'` (default) calls Playwright `fill()` with the string value. `'checkbox'` and `'radio'` call `setChecked()` — truthy values are `true`, `1`, `'1'`, `'true'`. Type can be omitted and defaults to `'text'`. Empty ref throws.
227
227
 
228
228
  #### Highlight
229
229
 
package/dist/index.cjs CHANGED
@@ -1232,7 +1232,8 @@ async function assertBrowserNavigationAllowed(opts) {
1232
1232
  const hostname = parsed.hostname.toLowerCase();
1233
1233
  if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1234
1234
  }
1235
- if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
1235
+ const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
1236
+ if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
1236
1237
  throw new InvalidBrowserNavigationUrlError(
1237
1238
  `Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1238
1239
  );
@@ -1347,7 +1348,7 @@ function isUnsupportedIPv4Literal(ip) {
1347
1348
  if (!parts.every(isStrictDecimalOctet)) return true;
1348
1349
  return false;
1349
1350
  }
1350
- function isInternalIP(ip) {
1351
+ function isInternalIP(ip, opts) {
1351
1352
  if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
1352
1353
  if (/^127\./.test(ip)) return true;
1353
1354
  if (/^10\./.test(ip)) return true;
@@ -1356,6 +1357,7 @@ function isInternalIP(ip) {
1356
1357
  if (/^169\.254\./.test(ip)) return true;
1357
1358
  if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1358
1359
  if (ip === "0.0.0.0") return true;
1360
+ if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
1359
1361
  const lower = ip.toLowerCase();
1360
1362
  if (lower === "::1") return true;
1361
1363
  if (lower.startsWith("fe80:")) return true;
@@ -1364,11 +1366,11 @@ function isInternalIP(ip) {
1364
1366
  const embedded = extractEmbeddedIPv4(lower);
1365
1367
  if (embedded !== null) {
1366
1368
  if (embedded === "") return true;
1367
- return isInternalIP(embedded);
1369
+ return isInternalIP(embedded, opts);
1368
1370
  }
1369
1371
  return false;
1370
1372
  }
1371
- function isInternalUrl(url) {
1373
+ function isInternalUrl(url, opts) {
1372
1374
  let parsed;
1373
1375
  try {
1374
1376
  parsed = new URL(url);
@@ -1377,7 +1379,7 @@ function isInternalUrl(url) {
1377
1379
  }
1378
1380
  const hostname = parsed.hostname.toLowerCase();
1379
1381
  if (hostname === "localhost") return true;
1380
- if (isInternalIP(hostname)) return true;
1382
+ if (isInternalIP(hostname, opts)) return true;
1381
1383
  if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1382
1384
  return true;
1383
1385
  }
@@ -1399,8 +1401,8 @@ async function assertSafeUploadPaths(paths) {
1399
1401
  }
1400
1402
  }
1401
1403
  }
1402
- async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1403
- if (isInternalUrl(url)) return true;
1404
+ async function isInternalUrlResolved(url, lookupFn = promises.lookup, opts) {
1405
+ if (isInternalUrl(url, opts)) return true;
1404
1406
  let parsed;
1405
1407
  try {
1406
1408
  parsed = new URL(url);
@@ -1409,12 +1411,25 @@ async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1409
1411
  }
1410
1412
  try {
1411
1413
  const { address } = await lookupFn(parsed.hostname);
1412
- if (isInternalIP(address)) return true;
1414
+ if (isInternalIP(address, opts)) return true;
1413
1415
  } catch {
1414
1416
  return true;
1415
1417
  }
1416
1418
  return false;
1417
1419
  }
1420
+ async function assertBrowserNavigationResultAllowed(opts) {
1421
+ const rawUrl = String(opts.url ?? "").trim();
1422
+ if (!rawUrl) return;
1423
+ let parsed;
1424
+ try {
1425
+ parsed = new URL(rawUrl);
1426
+ } catch {
1427
+ return;
1428
+ }
1429
+ if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || SAFE_NON_NETWORK_URLS.has(parsed.href)) {
1430
+ await assertBrowserNavigationAllowed(opts);
1431
+ }
1432
+ }
1418
1433
 
1419
1434
  // src/actions/interaction.ts
1420
1435
  async function clickViaPlaywright(opts) {
@@ -1497,11 +1512,10 @@ async function fillFormViaPlaywright(opts) {
1497
1512
  for (let i = 0; i < opts.fields.length; i++) {
1498
1513
  const field = opts.fields[i];
1499
1514
  const ref = field.ref.trim();
1500
- const type = field.type.trim();
1515
+ const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
1501
1516
  const rawValue = field.value;
1502
1517
  const value = rawValue == null ? "" : String(rawValue);
1503
1518
  if (!ref) throw new Error(`fill(): field at index ${i} has empty ref`);
1504
- if (!type) throw new Error(`fill(): field "${ref}" has empty type`);
1505
1519
  const locator = refLocator(page, ref);
1506
1520
  if (type === "checkbox" || type === "radio") {
1507
1521
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
@@ -1621,7 +1635,9 @@ async function navigateViaPlaywright(opts) {
1621
1635
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1622
1636
  ensurePageState(page);
1623
1637
  await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
1624
- return { url: page.url() };
1638
+ const finalUrl = page.url();
1639
+ await assertBrowserNavigationResultAllowed({ url: finalUrl, ssrfPolicy: policy });
1640
+ return { url: finalUrl };
1625
1641
  }
1626
1642
  async function listPagesViaPlaywright(opts) {
1627
1643
  const { browser } = await connectBrowser(opts.cdpUrl);
@@ -1640,8 +1656,8 @@ async function listPagesViaPlaywright(opts) {
1640
1656
  }
1641
1657
  async function createPageViaPlaywright(opts) {
1642
1658
  const targetUrl = (opts.url ?? "").trim() || "about:blank";
1659
+ const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1643
1660
  if (targetUrl !== "about:blank") {
1644
- const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1645
1661
  await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
1646
1662
  }
1647
1663
  const { browser } = await connectBrowser(opts.cdpUrl);
@@ -1650,6 +1666,7 @@ async function createPageViaPlaywright(opts) {
1650
1666
  ensurePageState(page);
1651
1667
  if (targetUrl !== "about:blank") {
1652
1668
  await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
1669
+ await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
1653
1670
  }
1654
1671
  const tid = await pageTargetId(page).catch(() => null);
1655
1672
  if (!tid) throw new Error("Failed to get targetId for new page");