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.d.cts
CHANGED
|
@@ -227,6 +227,8 @@ interface ClickOptions {
|
|
|
227
227
|
button?: 'left' | 'right' | 'middle';
|
|
228
228
|
/** Modifier keys to hold during click */
|
|
229
229
|
modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
|
|
230
|
+
/** Delay in ms between hover and click (hovers first, waits, then clicks). Max: `5000` */
|
|
231
|
+
delayMs?: number;
|
|
230
232
|
/** Timeout in milliseconds. Default: `8000` */
|
|
231
233
|
timeoutMs?: number;
|
|
232
234
|
}
|
|
@@ -1238,8 +1240,8 @@ declare function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: Ssrf
|
|
|
1238
1240
|
declare function ensureContextState(context: BrowserContext): ContextState;
|
|
1239
1241
|
/**
|
|
1240
1242
|
* Force-disconnect a Playwright browser connection for a given CDP target.
|
|
1241
|
-
* Clears the connection cache, sends Runtime.terminateExecution via CDP
|
|
1242
|
-
*
|
|
1243
|
+
* Clears the connection cache, sends Runtime.terminateExecution via raw CDP
|
|
1244
|
+
* websocket to kill stuck evals (bypassing Playwright), and closes the browser.
|
|
1243
1245
|
*/
|
|
1244
1246
|
declare function forceDisconnectPlaywrightForTarget(opts: {
|
|
1245
1247
|
cdpUrl: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -227,6 +227,8 @@ interface ClickOptions {
|
|
|
227
227
|
button?: 'left' | 'right' | 'middle';
|
|
228
228
|
/** Modifier keys to hold during click */
|
|
229
229
|
modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
|
|
230
|
+
/** Delay in ms between hover and click (hovers first, waits, then clicks). Max: `5000` */
|
|
231
|
+
delayMs?: number;
|
|
230
232
|
/** Timeout in milliseconds. Default: `8000` */
|
|
231
233
|
timeoutMs?: number;
|
|
232
234
|
}
|
|
@@ -1238,8 +1240,8 @@ declare function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: Ssrf
|
|
|
1238
1240
|
declare function ensureContextState(context: BrowserContext): ContextState;
|
|
1239
1241
|
/**
|
|
1240
1242
|
* Force-disconnect a Playwright browser connection for a given CDP target.
|
|
1241
|
-
* Clears the connection cache, sends Runtime.terminateExecution via CDP
|
|
1242
|
-
*
|
|
1243
|
+
* Clears the connection cache, sends Runtime.terminateExecution via raw CDP
|
|
1244
|
+
* websocket to kill stuck evals (bypassing Playwright), and closes the browser.
|
|
1243
1245
|
*/
|
|
1244
1246
|
declare function forceDisconnectPlaywrightForTarget(opts: {
|
|
1245
1247
|
cdpUrl: string;
|
package/dist/index.js
CHANGED
|
@@ -813,6 +813,7 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
813
813
|
return connected;
|
|
814
814
|
} catch (err) {
|
|
815
815
|
lastErr = err;
|
|
816
|
+
if ((err instanceof Error ? err.message : String(err)).includes("rate limit")) break;
|
|
816
817
|
await new Promise((r) => setTimeout(r, 250 + attempt * 250));
|
|
817
818
|
}
|
|
818
819
|
}
|
|
@@ -838,6 +839,55 @@ async function disconnectBrowser() {
|
|
|
838
839
|
if (cur) await cur.browser.close().catch(() => {
|
|
839
840
|
});
|
|
840
841
|
}
|
|
842
|
+
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
843
|
+
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
844
|
+
const ctrl = new AbortController();
|
|
845
|
+
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
846
|
+
let targets;
|
|
847
|
+
try {
|
|
848
|
+
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
849
|
+
if (!res.ok) return;
|
|
850
|
+
targets = await res.json();
|
|
851
|
+
} catch {
|
|
852
|
+
return;
|
|
853
|
+
} finally {
|
|
854
|
+
clearTimeout(t);
|
|
855
|
+
}
|
|
856
|
+
if (!Array.isArray(targets)) return;
|
|
857
|
+
const target = targets.find((entry) => String(entry?.id ?? "").trim() === targetId);
|
|
858
|
+
const wsUrl = String(target?.webSocketDebuggerUrl ?? "").trim();
|
|
859
|
+
if (!wsUrl) return;
|
|
860
|
+
await new Promise((resolve2) => {
|
|
861
|
+
let done = false;
|
|
862
|
+
const finish = () => {
|
|
863
|
+
if (done) return;
|
|
864
|
+
done = true;
|
|
865
|
+
clearTimeout(timer);
|
|
866
|
+
try {
|
|
867
|
+
ws.close();
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
resolve2();
|
|
871
|
+
};
|
|
872
|
+
const timer = setTimeout(finish, 2e3);
|
|
873
|
+
let ws;
|
|
874
|
+
try {
|
|
875
|
+
ws = new WebSocket(wsUrl);
|
|
876
|
+
} catch {
|
|
877
|
+
finish();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
ws.onopen = () => {
|
|
881
|
+
try {
|
|
882
|
+
ws.send(JSON.stringify({ id: 1, method: "Runtime.terminateExecution" }));
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
setTimeout(finish, 300);
|
|
886
|
+
};
|
|
887
|
+
ws.onerror = () => finish();
|
|
888
|
+
ws.onclose = () => finish();
|
|
889
|
+
});
|
|
890
|
+
}
|
|
841
891
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
842
892
|
const normalized = normalizeCdpUrl(opts.cdpUrl);
|
|
843
893
|
const cur = cached;
|
|
@@ -846,23 +896,8 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
846
896
|
connectingByUrl.delete(normalized);
|
|
847
897
|
const targetId = opts.targetId?.trim() || "";
|
|
848
898
|
if (targetId) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
for (const page of pages) {
|
|
852
|
-
const tid = await pageTargetId(page).catch(() => null);
|
|
853
|
-
if (tid === targetId) {
|
|
854
|
-
const session = await page.context().newCDPSession(page);
|
|
855
|
-
try {
|
|
856
|
-
await session.send("Runtime.terminateExecution");
|
|
857
|
-
} finally {
|
|
858
|
-
await session.detach().catch(() => {
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
} catch {
|
|
865
|
-
}
|
|
899
|
+
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
900
|
+
});
|
|
866
901
|
}
|
|
867
902
|
cur.browser.close().catch(() => {
|
|
868
903
|
});
|
|
@@ -1284,6 +1319,35 @@ async function snapshotRole(opts) {
|
|
|
1284
1319
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1285
1320
|
ensurePageState(page);
|
|
1286
1321
|
const sourceUrl = page.url();
|
|
1322
|
+
if (opts.refsMode === "aria") {
|
|
1323
|
+
if (opts.selector?.trim() || opts.frameSelector?.trim()) {
|
|
1324
|
+
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
|
1325
|
+
}
|
|
1326
|
+
const maybe = page;
|
|
1327
|
+
if (!maybe._snapshotForAI) {
|
|
1328
|
+
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
1329
|
+
}
|
|
1330
|
+
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
1331
|
+
const built2 = buildRoleSnapshotFromAiSnapshot(String(result?.full ?? ""), opts.options);
|
|
1332
|
+
storeRoleRefsForTarget({
|
|
1333
|
+
page,
|
|
1334
|
+
cdpUrl: opts.cdpUrl,
|
|
1335
|
+
targetId: opts.targetId,
|
|
1336
|
+
refs: built2.refs,
|
|
1337
|
+
mode: "aria"
|
|
1338
|
+
});
|
|
1339
|
+
return {
|
|
1340
|
+
snapshot: built2.snapshot,
|
|
1341
|
+
refs: built2.refs,
|
|
1342
|
+
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
1343
|
+
untrusted: true,
|
|
1344
|
+
contentMeta: {
|
|
1345
|
+
sourceUrl,
|
|
1346
|
+
contentType: "browser-snapshot",
|
|
1347
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1287
1351
|
const frameSelector = opts.frameSelector?.trim() || "";
|
|
1288
1352
|
const selector = opts.selector?.trim() || "";
|
|
1289
1353
|
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
@@ -1295,7 +1359,7 @@ async function snapshotRole(opts) {
|
|
|
1295
1359
|
targetId: opts.targetId,
|
|
1296
1360
|
refs: built.refs,
|
|
1297
1361
|
frameSelector: frameSelector || void 0,
|
|
1298
|
-
mode:
|
|
1362
|
+
mode: "role"
|
|
1299
1363
|
});
|
|
1300
1364
|
return {
|
|
1301
1365
|
snapshot: built.snapshot,
|
|
@@ -1383,7 +1447,7 @@ var InvalidBrowserNavigationUrlError = class extends Error {
|
|
|
1383
1447
|
}
|
|
1384
1448
|
};
|
|
1385
1449
|
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1386
|
-
return { ssrfPolicy };
|
|
1450
|
+
return ssrfPolicy ? { ssrfPolicy } : {};
|
|
1387
1451
|
}
|
|
1388
1452
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1389
1453
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
@@ -1496,6 +1560,30 @@ function extractEmbeddedIpv4FromIpv6(v6, opts) {
|
|
|
1496
1560
|
return true;
|
|
1497
1561
|
}
|
|
1498
1562
|
}
|
|
1563
|
+
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 65535 && parts[5] === 0) {
|
|
1564
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1565
|
+
try {
|
|
1566
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1567
|
+
} catch {
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1572
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1573
|
+
try {
|
|
1574
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1575
|
+
} catch {
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if ((parts[4] & 65023) === 0 && parts[5] === 24318) {
|
|
1580
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1581
|
+
try {
|
|
1582
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1583
|
+
} catch {
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1499
1587
|
return null;
|
|
1500
1588
|
}
|
|
1501
1589
|
function isPrivateIpAddress(address, opts) {
|
|
@@ -1518,6 +1606,30 @@ function isPrivateIpAddress(address, opts) {
|
|
|
1518
1606
|
if (normalized.includes(":")) return true;
|
|
1519
1607
|
return false;
|
|
1520
1608
|
}
|
|
1609
|
+
function normalizeHostnameSet(values) {
|
|
1610
|
+
if (!values || values.length === 0) return /* @__PURE__ */ new Set();
|
|
1611
|
+
return new Set(values.map((v) => normalizeHostname(v)).filter(Boolean));
|
|
1612
|
+
}
|
|
1613
|
+
function normalizeHostnameAllowlist(values) {
|
|
1614
|
+
if (!values || values.length === 0) return [];
|
|
1615
|
+
return Array.from(
|
|
1616
|
+
new Set(
|
|
1617
|
+
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
1618
|
+
)
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
1622
|
+
if (pattern.startsWith("*.")) {
|
|
1623
|
+
const suffix = pattern.slice(2);
|
|
1624
|
+
if (!suffix || hostname === suffix) return false;
|
|
1625
|
+
return hostname.endsWith(`.${suffix}`);
|
|
1626
|
+
}
|
|
1627
|
+
return hostname === pattern;
|
|
1628
|
+
}
|
|
1629
|
+
function matchesHostnameAllowlist(hostname, allowlist) {
|
|
1630
|
+
if (allowlist.length === 0) return true;
|
|
1631
|
+
return allowlist.some((pattern) => isHostnameAllowedByPattern(hostname, pattern));
|
|
1632
|
+
}
|
|
1521
1633
|
function dedupeAndPreferIpv4(results) {
|
|
1522
1634
|
const seen = /* @__PURE__ */ new Set();
|
|
1523
1635
|
const ipv4 = [];
|
|
@@ -1563,12 +1675,15 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1563
1675
|
const normalized = normalizeHostname(hostname);
|
|
1564
1676
|
if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
|
|
1565
1677
|
const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
|
|
1566
|
-
const allowedHostnames =
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
].map((h) => normalizeHostname(h));
|
|
1570
|
-
const isExplicitlyAllowed = allowedHostnames.some((h) => h === normalized);
|
|
1678
|
+
const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames);
|
|
1679
|
+
const hostnameAllowlist = normalizeHostnameAllowlist(params.policy?.hostnameAllowlist);
|
|
1680
|
+
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
1571
1681
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
1682
|
+
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
1683
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1684
|
+
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1572
1687
|
if (!skipPrivateNetworkChecks) {
|
|
1573
1688
|
if (isBlockedHostnameNormalized(normalized)) {
|
|
1574
1689
|
throw new InvalidBrowserNavigationUrlError(
|
|
@@ -1635,11 +1750,9 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1635
1750
|
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
|
|
1636
1751
|
);
|
|
1637
1752
|
}
|
|
1638
|
-
const policy = opts.ssrfPolicy;
|
|
1639
|
-
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1640
1753
|
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
1641
1754
|
lookupFn: opts.lookupFn,
|
|
1642
|
-
policy
|
|
1755
|
+
policy: opts.ssrfPolicy
|
|
1643
1756
|
});
|
|
1644
1757
|
}
|
|
1645
1758
|
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
@@ -1771,6 +1884,13 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
1771
1884
|
}
|
|
1772
1885
|
|
|
1773
1886
|
// src/actions/interaction.ts
|
|
1887
|
+
var MAX_CLICK_DELAY_MS = 5e3;
|
|
1888
|
+
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
1889
|
+
const normalized = Math.floor(value ?? 0);
|
|
1890
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
1891
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
1892
|
+
return normalized;
|
|
1893
|
+
}
|
|
1774
1894
|
async function clickViaPlaywright(opts) {
|
|
1775
1895
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1776
1896
|
ensurePageState(page);
|
|
@@ -1778,6 +1898,11 @@ async function clickViaPlaywright(opts) {
|
|
|
1778
1898
|
const locator = refLocator(page, opts.ref);
|
|
1779
1899
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
1780
1900
|
try {
|
|
1901
|
+
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
1902
|
+
if (delayMs > 0) {
|
|
1903
|
+
await locator.hover({ timeout });
|
|
1904
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
1905
|
+
}
|
|
1781
1906
|
if (opts.doubleClick) {
|
|
1782
1907
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
1783
1908
|
} else {
|
|
@@ -2088,12 +2213,14 @@ async function resizeViewportViaPlaywright(opts) {
|
|
|
2088
2213
|
}
|
|
2089
2214
|
|
|
2090
2215
|
// src/actions/wait.ts
|
|
2216
|
+
var MAX_WAIT_TIME_MS = 3e4;
|
|
2091
2217
|
async function waitForViaPlaywright(opts) {
|
|
2092
2218
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2093
2219
|
ensurePageState(page);
|
|
2094
2220
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2095
2221
|
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
2096
|
-
|
|
2222
|
+
const bounded = Math.max(0, Math.min(MAX_WAIT_TIME_MS, Math.floor(opts.timeMs)));
|
|
2223
|
+
await page.waitForTimeout(bounded);
|
|
2097
2224
|
}
|
|
2098
2225
|
if (opts.text) {
|
|
2099
2226
|
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
@@ -2782,6 +2909,7 @@ var CrawlPage = class {
|
|
|
2782
2909
|
doubleClick: opts?.doubleClick,
|
|
2783
2910
|
button: opts?.button,
|
|
2784
2911
|
modifiers: opts?.modifiers,
|
|
2912
|
+
delayMs: opts?.delayMs,
|
|
2785
2913
|
timeoutMs: opts?.timeoutMs
|
|
2786
2914
|
});
|
|
2787
2915
|
}
|