browserclaw 0.11.0 → 0.11.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 +210 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +210 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1207,11 +1207,14 @@ function safeWriteJson(filePath, data) {
|
|
|
1207
1207
|
function setDeep(obj, keys, value) {
|
|
1208
1208
|
let node = obj;
|
|
1209
1209
|
for (const key of keys.slice(0, -1)) {
|
|
1210
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return;
|
|
1210
1211
|
const next = node[key];
|
|
1211
1212
|
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
|
|
1212
1213
|
node = node[key];
|
|
1213
1214
|
}
|
|
1214
|
-
|
|
1215
|
+
const lastKey = keys[keys.length - 1];
|
|
1216
|
+
if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") return;
|
|
1217
|
+
node[lastKey] = value;
|
|
1215
1218
|
}
|
|
1216
1219
|
function parseHexRgbToSignedArgbInt(hex) {
|
|
1217
1220
|
const cleaned = hex.trim().replace(/^#/, "");
|
|
@@ -1265,7 +1268,8 @@ function isWebSocketUrl(url) {
|
|
|
1265
1268
|
}
|
|
1266
1269
|
}
|
|
1267
1270
|
function isLoopbackHost(hostname) {
|
|
1268
|
-
|
|
1271
|
+
const h = hostname.replace(/\.+$/, "");
|
|
1272
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
|
|
1269
1273
|
}
|
|
1270
1274
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1271
1275
|
function hasProxyEnvConfigured(env = process.env) {
|
|
@@ -1917,6 +1921,56 @@ function bumpDownloadArmId() {
|
|
|
1917
1921
|
nextDownloadArmId += 1;
|
|
1918
1922
|
return nextDownloadArmId;
|
|
1919
1923
|
}
|
|
1924
|
+
var BlockedBrowserTargetError = class extends Error {
|
|
1925
|
+
constructor() {
|
|
1926
|
+
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
1927
|
+
this.name = "BlockedBrowserTargetError";
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
1931
|
+
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
1932
|
+
function blockedTargetKey(cdpUrl, targetId) {
|
|
1933
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1934
|
+
}
|
|
1935
|
+
function isBlockedTarget(cdpUrl, targetId) {
|
|
1936
|
+
const normalized = targetId?.trim() ?? "";
|
|
1937
|
+
if (normalized === "") return false;
|
|
1938
|
+
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
1939
|
+
}
|
|
1940
|
+
function markTargetBlocked(cdpUrl, targetId) {
|
|
1941
|
+
const normalized = targetId?.trim() ?? "";
|
|
1942
|
+
if (normalized === "") return;
|
|
1943
|
+
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
1944
|
+
}
|
|
1945
|
+
function clearBlockedTarget(cdpUrl, targetId) {
|
|
1946
|
+
const normalized = targetId?.trim() ?? "";
|
|
1947
|
+
if (normalized === "") return;
|
|
1948
|
+
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
1949
|
+
}
|
|
1950
|
+
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
1951
|
+
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
1952
|
+
for (const key of blockedTargetsByCdpUrl) {
|
|
1953
|
+
if (key.startsWith(prefix)) return true;
|
|
1954
|
+
}
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
1958
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1959
|
+
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
1960
|
+
if (existing) return existing;
|
|
1961
|
+
const created = /* @__PURE__ */ new WeakSet();
|
|
1962
|
+
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
1963
|
+
return created;
|
|
1964
|
+
}
|
|
1965
|
+
function isBlockedPageRef(cdpUrl, page) {
|
|
1966
|
+
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
1967
|
+
}
|
|
1968
|
+
function markPageRefBlocked(cdpUrl, page) {
|
|
1969
|
+
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
1970
|
+
}
|
|
1971
|
+
function clearBlockedPageRef(cdpUrl, page) {
|
|
1972
|
+
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
1973
|
+
}
|
|
1920
1974
|
function ensureContextState(context) {
|
|
1921
1975
|
const existing = contextStates.get(context);
|
|
1922
1976
|
if (existing) return existing;
|
|
@@ -2357,11 +2411,43 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
2357
2411
|
if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
|
|
2358
2412
|
return null;
|
|
2359
2413
|
}
|
|
2414
|
+
async function partitionAccessiblePages(opts) {
|
|
2415
|
+
const accessible = [];
|
|
2416
|
+
let blockedCount = 0;
|
|
2417
|
+
for (const page of opts.pages) {
|
|
2418
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) {
|
|
2419
|
+
blockedCount += 1;
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
const targetId = await pageTargetId(page).catch(() => null);
|
|
2423
|
+
if (targetId === null || targetId === "") {
|
|
2424
|
+
if (hasBlockedTargetsForCdpUrl(opts.cdpUrl)) {
|
|
2425
|
+
blockedCount += 1;
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
accessible.push(page);
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
if (isBlockedTarget(opts.cdpUrl, targetId)) {
|
|
2432
|
+
blockedCount += 1;
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
accessible.push(page);
|
|
2436
|
+
}
|
|
2437
|
+
return { accessible, blockedCount };
|
|
2438
|
+
}
|
|
2360
2439
|
async function getPageForTargetId(opts) {
|
|
2440
|
+
if (opts.targetId !== void 0 && opts.targetId !== "" && isBlockedTarget(opts.cdpUrl, opts.targetId))
|
|
2441
|
+
throw new BlockedBrowserTargetError();
|
|
2361
2442
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
2362
2443
|
const pages = getAllPages(browser);
|
|
2363
2444
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
2364
|
-
const
|
|
2445
|
+
const { accessible, blockedCount } = await partitionAccessiblePages({ cdpUrl: opts.cdpUrl, pages });
|
|
2446
|
+
if (!accessible.length) {
|
|
2447
|
+
if (blockedCount > 0) throw new BlockedBrowserTargetError();
|
|
2448
|
+
throw new Error("No pages available in the connected browser.");
|
|
2449
|
+
}
|
|
2450
|
+
const first = accessible[0];
|
|
2365
2451
|
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
2366
2452
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
2367
2453
|
if (!found) {
|
|
@@ -2370,6 +2456,10 @@ async function getPageForTargetId(opts) {
|
|
|
2370
2456
|
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2371
2457
|
);
|
|
2372
2458
|
}
|
|
2459
|
+
if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
|
|
2460
|
+
const foundTargetId = await pageTargetId(found).catch(() => null);
|
|
2461
|
+
if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
|
|
2462
|
+
throw new BlockedBrowserTargetError();
|
|
2373
2463
|
return found;
|
|
2374
2464
|
}
|
|
2375
2465
|
async function resolvePageByTargetIdOrThrow(opts) {
|
|
@@ -3497,6 +3587,82 @@ function isRetryableNavigateError(err) {
|
|
|
3497
3587
|
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
3498
3588
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3499
3589
|
}
|
|
3590
|
+
function isPolicyDenyNavigationError(err) {
|
|
3591
|
+
return err instanceof InvalidBrowserNavigationUrlError;
|
|
3592
|
+
}
|
|
3593
|
+
function isTopLevelNavigationRequest(page, request) {
|
|
3594
|
+
if (!request.isNavigationRequest()) return false;
|
|
3595
|
+
try {
|
|
3596
|
+
return request.frame() === page.mainFrame();
|
|
3597
|
+
} catch {
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
async function closeBlockedNavigationTarget(opts) {
|
|
3602
|
+
markPageRefBlocked(opts.cdpUrl, opts.page);
|
|
3603
|
+
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
|
|
3604
|
+
const fallbackTargetId = opts.targetId?.trim() ?? "";
|
|
3605
|
+
const targetIdToBlock = resolvedTargetId ?? fallbackTargetId;
|
|
3606
|
+
if (targetIdToBlock) markTargetBlocked(opts.cdpUrl, targetIdToBlock);
|
|
3607
|
+
await opts.page.close().catch((e) => {
|
|
3608
|
+
console.warn("[browserclaw] failed to close blocked page", e);
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
async function assertPageNavigationCompletedSafely(opts) {
|
|
3612
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3613
|
+
try {
|
|
3614
|
+
await assertBrowserNavigationRedirectChainAllowed({ request: opts.response?.request(), ...navigationPolicy });
|
|
3615
|
+
await assertBrowserNavigationResultAllowed({ url: opts.page.url(), ...navigationPolicy });
|
|
3616
|
+
} catch (err) {
|
|
3617
|
+
if (isPolicyDenyNavigationError(err))
|
|
3618
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3619
|
+
throw err;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
async function gotoPageWithNavigationGuard(opts) {
|
|
3623
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3624
|
+
const state = { blocked: null };
|
|
3625
|
+
const handler = async (route, request) => {
|
|
3626
|
+
if (state.blocked !== null) {
|
|
3627
|
+
await route.abort().catch((e) => {
|
|
3628
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3629
|
+
});
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
if (!isTopLevelNavigationRequest(opts.page, request)) {
|
|
3633
|
+
await route.continue();
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
try {
|
|
3637
|
+
await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
|
|
3638
|
+
} catch (err) {
|
|
3639
|
+
if (isPolicyDenyNavigationError(err)) {
|
|
3640
|
+
state.blocked = err;
|
|
3641
|
+
await route.abort().catch((e) => {
|
|
3642
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3643
|
+
});
|
|
3644
|
+
return;
|
|
3645
|
+
}
|
|
3646
|
+
throw err;
|
|
3647
|
+
}
|
|
3648
|
+
await route.continue();
|
|
3649
|
+
};
|
|
3650
|
+
await opts.page.route("**", handler);
|
|
3651
|
+
try {
|
|
3652
|
+
const response = await opts.page.goto(opts.url, { timeout: opts.timeoutMs });
|
|
3653
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3654
|
+
return response;
|
|
3655
|
+
} catch (err) {
|
|
3656
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3657
|
+
throw err;
|
|
3658
|
+
} finally {
|
|
3659
|
+
await opts.page.unroute("**", handler).catch((e) => {
|
|
3660
|
+
console.warn("[browserclaw] route unroute failed", e);
|
|
3661
|
+
});
|
|
3662
|
+
if (state.blocked !== null)
|
|
3663
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3500
3666
|
async function navigateViaPlaywright(opts) {
|
|
3501
3667
|
const url = opts.url.trim();
|
|
3502
3668
|
if (!url) throw new Error("url is required");
|
|
@@ -3505,7 +3671,14 @@ async function navigateViaPlaywright(opts) {
|
|
|
3505
3671
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3506
3672
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3507
3673
|
ensurePageState(page);
|
|
3508
|
-
const navigate = async () => await
|
|
3674
|
+
const navigate = async () => await gotoPageWithNavigationGuard({
|
|
3675
|
+
cdpUrl: opts.cdpUrl,
|
|
3676
|
+
page,
|
|
3677
|
+
url,
|
|
3678
|
+
timeoutMs: timeout,
|
|
3679
|
+
ssrfPolicy: policy,
|
|
3680
|
+
targetId: opts.targetId
|
|
3681
|
+
});
|
|
3509
3682
|
let response;
|
|
3510
3683
|
try {
|
|
3511
3684
|
response = await navigate();
|
|
@@ -3519,21 +3692,23 @@ async function navigateViaPlaywright(opts) {
|
|
|
3519
3692
|
ensurePageState(page);
|
|
3520
3693
|
response = await navigate();
|
|
3521
3694
|
}
|
|
3522
|
-
await
|
|
3523
|
-
|
|
3524
|
-
|
|
3695
|
+
await assertPageNavigationCompletedSafely({
|
|
3696
|
+
cdpUrl: opts.cdpUrl,
|
|
3697
|
+
page,
|
|
3698
|
+
response,
|
|
3699
|
+
ssrfPolicy: policy,
|
|
3700
|
+
targetId: opts.targetId
|
|
3525
3701
|
});
|
|
3526
|
-
|
|
3527
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3528
|
-
return { url: finalUrl };
|
|
3702
|
+
return { url: page.url() };
|
|
3529
3703
|
}
|
|
3530
3704
|
async function listPagesViaPlaywright(opts) {
|
|
3531
3705
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3532
3706
|
const pages = getAllPages(browser);
|
|
3533
3707
|
const results = [];
|
|
3534
3708
|
for (const page of pages) {
|
|
3709
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) continue;
|
|
3535
3710
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3536
|
-
if (tid !== null && tid !== "")
|
|
3711
|
+
if (tid !== null && tid !== "" && !isBlockedTarget(opts.cdpUrl, tid))
|
|
3537
3712
|
results.push({
|
|
3538
3713
|
targetId: tid,
|
|
3539
3714
|
title: await page.title().catch(() => ""),
|
|
@@ -3549,18 +3724,35 @@ async function createPageViaPlaywright(opts) {
|
|
|
3549
3724
|
ensureContextState(context);
|
|
3550
3725
|
const page = await context.newPage();
|
|
3551
3726
|
ensurePageState(page);
|
|
3727
|
+
clearBlockedPageRef(opts.cdpUrl, page);
|
|
3728
|
+
const createdTargetId = await pageTargetId(page).catch(() => null);
|
|
3729
|
+
clearBlockedTarget(opts.cdpUrl, createdTargetId ?? void 0);
|
|
3552
3730
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3553
3731
|
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3554
3732
|
if (targetUrl !== "about:blank") {
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3733
|
+
await assertBrowserNavigationAllowed({ url: targetUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3734
|
+
let response = null;
|
|
3735
|
+
try {
|
|
3736
|
+
response = await gotoPageWithNavigationGuard({
|
|
3737
|
+
cdpUrl: opts.cdpUrl,
|
|
3738
|
+
page,
|
|
3739
|
+
url: targetUrl,
|
|
3740
|
+
timeoutMs: 3e4,
|
|
3741
|
+
ssrfPolicy: policy,
|
|
3742
|
+
targetId: createdTargetId ?? void 0
|
|
3743
|
+
});
|
|
3744
|
+
} catch (err) {
|
|
3745
|
+
if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
|
|
3746
|
+
}
|
|
3747
|
+
await assertPageNavigationCompletedSafely({
|
|
3748
|
+
cdpUrl: opts.cdpUrl,
|
|
3749
|
+
page,
|
|
3750
|
+
response,
|
|
3751
|
+
ssrfPolicy: policy,
|
|
3752
|
+
targetId: createdTargetId ?? void 0
|
|
3560
3753
|
});
|
|
3561
|
-
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3562
3754
|
}
|
|
3563
|
-
const tid = await pageTargetId(page).catch(() => null);
|
|
3755
|
+
const tid = createdTargetId ?? await pageTargetId(page).catch(() => null);
|
|
3564
3756
|
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3565
3757
|
return {
|
|
3566
3758
|
targetId: tid,
|