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 +4 -4
- package/dist/index.cjs +29 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +29 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</
|
|
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',
|
|
221
|
-
{ ref: 'e4',
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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");
|