browserclaw 0.5.0 → 0.5.2
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 +156 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +156 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -841,6 +841,7 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
841
841
|
return connected;
|
|
842
842
|
} catch (err) {
|
|
843
843
|
lastErr = err;
|
|
844
|
+
if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) break;
|
|
844
845
|
await new Promise((r) => setTimeout(r, 250 + attempt * 250));
|
|
845
846
|
}
|
|
846
847
|
}
|
|
@@ -866,6 +867,55 @@ async function disconnectBrowser() {
|
|
|
866
867
|
if (cur) await cur.browser.close().catch(() => {
|
|
867
868
|
});
|
|
868
869
|
}
|
|
870
|
+
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
871
|
+
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
872
|
+
const ctrl = new AbortController();
|
|
873
|
+
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
874
|
+
let targets;
|
|
875
|
+
try {
|
|
876
|
+
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
877
|
+
if (!res.ok) return;
|
|
878
|
+
targets = await res.json();
|
|
879
|
+
} catch {
|
|
880
|
+
return;
|
|
881
|
+
} finally {
|
|
882
|
+
clearTimeout(t);
|
|
883
|
+
}
|
|
884
|
+
if (!Array.isArray(targets)) return;
|
|
885
|
+
const target = targets.find((entry) => String(entry?.id ?? "").trim() === targetId);
|
|
886
|
+
const wsUrl = String(target?.webSocketDebuggerUrl ?? "").trim();
|
|
887
|
+
if (!wsUrl) return;
|
|
888
|
+
await new Promise((resolve2) => {
|
|
889
|
+
let done = false;
|
|
890
|
+
const finish = () => {
|
|
891
|
+
if (done) return;
|
|
892
|
+
done = true;
|
|
893
|
+
clearTimeout(timer);
|
|
894
|
+
try {
|
|
895
|
+
ws.close();
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
resolve2();
|
|
899
|
+
};
|
|
900
|
+
const timer = setTimeout(finish, 2e3);
|
|
901
|
+
let ws;
|
|
902
|
+
try {
|
|
903
|
+
ws = new WebSocket(wsUrl);
|
|
904
|
+
} catch {
|
|
905
|
+
finish();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
ws.onopen = () => {
|
|
909
|
+
try {
|
|
910
|
+
ws.send(JSON.stringify({ id: 1, method: "Runtime.terminateExecution" }));
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
setTimeout(finish, 300);
|
|
914
|
+
};
|
|
915
|
+
ws.onerror = () => finish();
|
|
916
|
+
ws.onclose = () => finish();
|
|
917
|
+
});
|
|
918
|
+
}
|
|
869
919
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
870
920
|
const normalized = normalizeCdpUrl(opts.cdpUrl);
|
|
871
921
|
const cur = cached;
|
|
@@ -874,23 +924,8 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
874
924
|
connectingByUrl.delete(normalized);
|
|
875
925
|
const targetId = opts.targetId?.trim() || "";
|
|
876
926
|
if (targetId) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
for (const page of pages) {
|
|
880
|
-
const tid = await pageTargetId(page).catch(() => null);
|
|
881
|
-
if (tid === targetId) {
|
|
882
|
-
const session = await page.context().newCDPSession(page);
|
|
883
|
-
try {
|
|
884
|
-
await session.send("Runtime.terminateExecution");
|
|
885
|
-
} finally {
|
|
886
|
-
await session.detach().catch(() => {
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
break;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
} catch {
|
|
893
|
-
}
|
|
927
|
+
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
928
|
+
});
|
|
894
929
|
}
|
|
895
930
|
cur.browser.close().catch(() => {
|
|
896
931
|
});
|
|
@@ -1312,6 +1347,35 @@ async function snapshotRole(opts) {
|
|
|
1312
1347
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1313
1348
|
ensurePageState(page);
|
|
1314
1349
|
const sourceUrl = page.url();
|
|
1350
|
+
if (opts.refsMode === "aria") {
|
|
1351
|
+
if (opts.selector?.trim() || opts.frameSelector?.trim()) {
|
|
1352
|
+
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
|
1353
|
+
}
|
|
1354
|
+
const maybe = page;
|
|
1355
|
+
if (!maybe._snapshotForAI) {
|
|
1356
|
+
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
1357
|
+
}
|
|
1358
|
+
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
1359
|
+
const built2 = buildRoleSnapshotFromAiSnapshot(String(result?.full ?? ""), opts.options);
|
|
1360
|
+
storeRoleRefsForTarget({
|
|
1361
|
+
page,
|
|
1362
|
+
cdpUrl: opts.cdpUrl,
|
|
1363
|
+
targetId: opts.targetId,
|
|
1364
|
+
refs: built2.refs,
|
|
1365
|
+
mode: "aria"
|
|
1366
|
+
});
|
|
1367
|
+
return {
|
|
1368
|
+
snapshot: built2.snapshot,
|
|
1369
|
+
refs: built2.refs,
|
|
1370
|
+
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
1371
|
+
untrusted: true,
|
|
1372
|
+
contentMeta: {
|
|
1373
|
+
sourceUrl,
|
|
1374
|
+
contentType: "browser-snapshot",
|
|
1375
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1315
1379
|
const frameSelector = opts.frameSelector?.trim() || "";
|
|
1316
1380
|
const selector = opts.selector?.trim() || "";
|
|
1317
1381
|
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
@@ -1323,7 +1387,7 @@ async function snapshotRole(opts) {
|
|
|
1323
1387
|
targetId: opts.targetId,
|
|
1324
1388
|
refs: built.refs,
|
|
1325
1389
|
frameSelector: frameSelector || void 0,
|
|
1326
|
-
mode:
|
|
1390
|
+
mode: "role"
|
|
1327
1391
|
});
|
|
1328
1392
|
return {
|
|
1329
1393
|
snapshot: built.snapshot,
|
|
@@ -1411,7 +1475,7 @@ var InvalidBrowserNavigationUrlError = class extends Error {
|
|
|
1411
1475
|
}
|
|
1412
1476
|
};
|
|
1413
1477
|
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1414
|
-
return { ssrfPolicy };
|
|
1478
|
+
return ssrfPolicy ? { ssrfPolicy } : {};
|
|
1415
1479
|
}
|
|
1416
1480
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1417
1481
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
@@ -1524,6 +1588,30 @@ function extractEmbeddedIpv4FromIpv6(v6, opts) {
|
|
|
1524
1588
|
return true;
|
|
1525
1589
|
}
|
|
1526
1590
|
}
|
|
1591
|
+
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 65535 && parts[5] === 0) {
|
|
1592
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1593
|
+
try {
|
|
1594
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1595
|
+
} catch {
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1600
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1601
|
+
try {
|
|
1602
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1603
|
+
} catch {
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if ((parts[4] & 65023) === 0 && parts[5] === 24318) {
|
|
1608
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1609
|
+
try {
|
|
1610
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1611
|
+
} catch {
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1527
1615
|
return null;
|
|
1528
1616
|
}
|
|
1529
1617
|
function isPrivateIpAddress(address, opts) {
|
|
@@ -1546,6 +1634,30 @@ function isPrivateIpAddress(address, opts) {
|
|
|
1546
1634
|
if (normalized.includes(":")) return true;
|
|
1547
1635
|
return false;
|
|
1548
1636
|
}
|
|
1637
|
+
function normalizeHostnameSet(values) {
|
|
1638
|
+
if (!values || values.length === 0) return /* @__PURE__ */ new Set();
|
|
1639
|
+
return new Set(values.map((v) => normalizeHostname(v)).filter(Boolean));
|
|
1640
|
+
}
|
|
1641
|
+
function normalizeHostnameAllowlist(values) {
|
|
1642
|
+
if (!values || values.length === 0) return [];
|
|
1643
|
+
return Array.from(
|
|
1644
|
+
new Set(
|
|
1645
|
+
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
1646
|
+
)
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
1650
|
+
if (pattern.startsWith("*.")) {
|
|
1651
|
+
const suffix = pattern.slice(2);
|
|
1652
|
+
if (!suffix || hostname === suffix) return false;
|
|
1653
|
+
return hostname.endsWith(`.${suffix}`);
|
|
1654
|
+
}
|
|
1655
|
+
return hostname === pattern;
|
|
1656
|
+
}
|
|
1657
|
+
function matchesHostnameAllowlist(hostname, allowlist) {
|
|
1658
|
+
if (allowlist.length === 0) return true;
|
|
1659
|
+
return allowlist.some((pattern) => isHostnameAllowedByPattern(hostname, pattern));
|
|
1660
|
+
}
|
|
1549
1661
|
function dedupeAndPreferIpv4(results) {
|
|
1550
1662
|
const seen = /* @__PURE__ */ new Set();
|
|
1551
1663
|
const ipv4 = [];
|
|
@@ -1591,12 +1703,15 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1591
1703
|
const normalized = normalizeHostname(hostname);
|
|
1592
1704
|
if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
|
|
1593
1705
|
const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
|
|
1594
|
-
const allowedHostnames =
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
].map((h) => normalizeHostname(h));
|
|
1598
|
-
const isExplicitlyAllowed = allowedHostnames.some((h) => h === normalized);
|
|
1706
|
+
const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames);
|
|
1707
|
+
const hostnameAllowlist = normalizeHostnameAllowlist(params.policy?.hostnameAllowlist);
|
|
1708
|
+
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
1599
1709
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
1710
|
+
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
1711
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1712
|
+
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1600
1715
|
if (!skipPrivateNetworkChecks) {
|
|
1601
1716
|
if (isBlockedHostnameNormalized(normalized)) {
|
|
1602
1717
|
throw new InvalidBrowserNavigationUrlError(
|
|
@@ -1663,11 +1778,9 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1663
1778
|
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
|
|
1664
1779
|
);
|
|
1665
1780
|
}
|
|
1666
|
-
const policy = opts.ssrfPolicy;
|
|
1667
|
-
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1668
1781
|
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
1669
1782
|
lookupFn: opts.lookupFn,
|
|
1670
|
-
policy
|
|
1783
|
+
policy: opts.ssrfPolicy
|
|
1671
1784
|
});
|
|
1672
1785
|
}
|
|
1673
1786
|
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
@@ -1799,6 +1912,13 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
1799
1912
|
}
|
|
1800
1913
|
|
|
1801
1914
|
// src/actions/interaction.ts
|
|
1915
|
+
var MAX_CLICK_DELAY_MS = 5e3;
|
|
1916
|
+
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
1917
|
+
const normalized = Math.floor(value ?? 0);
|
|
1918
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
1919
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
1920
|
+
return normalized;
|
|
1921
|
+
}
|
|
1802
1922
|
async function clickViaPlaywright(opts) {
|
|
1803
1923
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1804
1924
|
ensurePageState(page);
|
|
@@ -1806,6 +1926,11 @@ async function clickViaPlaywright(opts) {
|
|
|
1806
1926
|
const locator = refLocator(page, opts.ref);
|
|
1807
1927
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
1808
1928
|
try {
|
|
1929
|
+
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
1930
|
+
if (delayMs > 0) {
|
|
1931
|
+
await locator.hover({ timeout });
|
|
1932
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
1933
|
+
}
|
|
1809
1934
|
if (opts.doubleClick) {
|
|
1810
1935
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
1811
1936
|
} else {
|
|
@@ -2116,12 +2241,14 @@ async function resizeViewportViaPlaywright(opts) {
|
|
|
2116
2241
|
}
|
|
2117
2242
|
|
|
2118
2243
|
// src/actions/wait.ts
|
|
2244
|
+
var MAX_WAIT_TIME_MS = 3e4;
|
|
2119
2245
|
async function waitForViaPlaywright(opts) {
|
|
2120
2246
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2121
2247
|
ensurePageState(page);
|
|
2122
2248
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2123
2249
|
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
2124
|
-
|
|
2250
|
+
const bounded = Math.max(0, Math.min(MAX_WAIT_TIME_MS, Math.floor(opts.timeMs)));
|
|
2251
|
+
await page.waitForTimeout(bounded);
|
|
2125
2252
|
}
|
|
2126
2253
|
if (opts.text) {
|
|
2127
2254
|
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
@@ -2810,6 +2937,7 @@ var CrawlPage = class {
|
|
|
2810
2937
|
doubleClick: opts?.doubleClick,
|
|
2811
2938
|
button: opts?.button,
|
|
2812
2939
|
modifiers: opts?.modifiers,
|
|
2940
|
+
delayMs: opts?.delayMs,
|
|
2813
2941
|
timeoutMs: opts?.timeoutMs
|
|
2814
2942
|
});
|
|
2815
2943
|
}
|