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.cjs
CHANGED
|
@@ -1218,11 +1218,14 @@ function safeWriteJson(filePath, data) {
|
|
|
1218
1218
|
function setDeep(obj, keys, value) {
|
|
1219
1219
|
let node = obj;
|
|
1220
1220
|
for (const key of keys.slice(0, -1)) {
|
|
1221
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return;
|
|
1221
1222
|
const next = node[key];
|
|
1222
1223
|
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
|
|
1223
1224
|
node = node[key];
|
|
1224
1225
|
}
|
|
1225
|
-
|
|
1226
|
+
const lastKey = keys[keys.length - 1];
|
|
1227
|
+
if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") return;
|
|
1228
|
+
node[lastKey] = value;
|
|
1226
1229
|
}
|
|
1227
1230
|
function parseHexRgbToSignedArgbInt(hex) {
|
|
1228
1231
|
const cleaned = hex.trim().replace(/^#/, "");
|
|
@@ -1276,7 +1279,8 @@ function isWebSocketUrl(url) {
|
|
|
1276
1279
|
}
|
|
1277
1280
|
}
|
|
1278
1281
|
function isLoopbackHost(hostname) {
|
|
1279
|
-
|
|
1282
|
+
const h = hostname.replace(/\.+$/, "");
|
|
1283
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
|
|
1280
1284
|
}
|
|
1281
1285
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1282
1286
|
function hasProxyEnvConfigured(env = process.env) {
|
|
@@ -1928,6 +1932,56 @@ function bumpDownloadArmId() {
|
|
|
1928
1932
|
nextDownloadArmId += 1;
|
|
1929
1933
|
return nextDownloadArmId;
|
|
1930
1934
|
}
|
|
1935
|
+
var BlockedBrowserTargetError = class extends Error {
|
|
1936
|
+
constructor() {
|
|
1937
|
+
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
1938
|
+
this.name = "BlockedBrowserTargetError";
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
1942
|
+
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
1943
|
+
function blockedTargetKey(cdpUrl, targetId) {
|
|
1944
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1945
|
+
}
|
|
1946
|
+
function isBlockedTarget(cdpUrl, targetId) {
|
|
1947
|
+
const normalized = targetId?.trim() ?? "";
|
|
1948
|
+
if (normalized === "") return false;
|
|
1949
|
+
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
1950
|
+
}
|
|
1951
|
+
function markTargetBlocked(cdpUrl, targetId) {
|
|
1952
|
+
const normalized = targetId?.trim() ?? "";
|
|
1953
|
+
if (normalized === "") return;
|
|
1954
|
+
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
1955
|
+
}
|
|
1956
|
+
function clearBlockedTarget(cdpUrl, targetId) {
|
|
1957
|
+
const normalized = targetId?.trim() ?? "";
|
|
1958
|
+
if (normalized === "") return;
|
|
1959
|
+
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
1960
|
+
}
|
|
1961
|
+
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
1962
|
+
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
1963
|
+
for (const key of blockedTargetsByCdpUrl) {
|
|
1964
|
+
if (key.startsWith(prefix)) return true;
|
|
1965
|
+
}
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
1969
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1970
|
+
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
1971
|
+
if (existing) return existing;
|
|
1972
|
+
const created = /* @__PURE__ */ new WeakSet();
|
|
1973
|
+
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
1974
|
+
return created;
|
|
1975
|
+
}
|
|
1976
|
+
function isBlockedPageRef(cdpUrl, page) {
|
|
1977
|
+
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
1978
|
+
}
|
|
1979
|
+
function markPageRefBlocked(cdpUrl, page) {
|
|
1980
|
+
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
1981
|
+
}
|
|
1982
|
+
function clearBlockedPageRef(cdpUrl, page) {
|
|
1983
|
+
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
1984
|
+
}
|
|
1931
1985
|
function ensureContextState(context) {
|
|
1932
1986
|
const existing = contextStates.get(context);
|
|
1933
1987
|
if (existing) return existing;
|
|
@@ -2368,11 +2422,43 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
2368
2422
|
if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
|
|
2369
2423
|
return null;
|
|
2370
2424
|
}
|
|
2425
|
+
async function partitionAccessiblePages(opts) {
|
|
2426
|
+
const accessible = [];
|
|
2427
|
+
let blockedCount = 0;
|
|
2428
|
+
for (const page of opts.pages) {
|
|
2429
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) {
|
|
2430
|
+
blockedCount += 1;
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
const targetId = await pageTargetId(page).catch(() => null);
|
|
2434
|
+
if (targetId === null || targetId === "") {
|
|
2435
|
+
if (hasBlockedTargetsForCdpUrl(opts.cdpUrl)) {
|
|
2436
|
+
blockedCount += 1;
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
accessible.push(page);
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if (isBlockedTarget(opts.cdpUrl, targetId)) {
|
|
2443
|
+
blockedCount += 1;
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
accessible.push(page);
|
|
2447
|
+
}
|
|
2448
|
+
return { accessible, blockedCount };
|
|
2449
|
+
}
|
|
2371
2450
|
async function getPageForTargetId(opts) {
|
|
2451
|
+
if (opts.targetId !== void 0 && opts.targetId !== "" && isBlockedTarget(opts.cdpUrl, opts.targetId))
|
|
2452
|
+
throw new BlockedBrowserTargetError();
|
|
2372
2453
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
2373
2454
|
const pages = getAllPages(browser);
|
|
2374
2455
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
2375
|
-
const
|
|
2456
|
+
const { accessible, blockedCount } = await partitionAccessiblePages({ cdpUrl: opts.cdpUrl, pages });
|
|
2457
|
+
if (!accessible.length) {
|
|
2458
|
+
if (blockedCount > 0) throw new BlockedBrowserTargetError();
|
|
2459
|
+
throw new Error("No pages available in the connected browser.");
|
|
2460
|
+
}
|
|
2461
|
+
const first = accessible[0];
|
|
2376
2462
|
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
2377
2463
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
2378
2464
|
if (!found) {
|
|
@@ -2381,6 +2467,10 @@ async function getPageForTargetId(opts) {
|
|
|
2381
2467
|
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2382
2468
|
);
|
|
2383
2469
|
}
|
|
2470
|
+
if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
|
|
2471
|
+
const foundTargetId = await pageTargetId(found).catch(() => null);
|
|
2472
|
+
if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
|
|
2473
|
+
throw new BlockedBrowserTargetError();
|
|
2384
2474
|
return found;
|
|
2385
2475
|
}
|
|
2386
2476
|
async function resolvePageByTargetIdOrThrow(opts) {
|
|
@@ -3508,6 +3598,82 @@ function isRetryableNavigateError(err) {
|
|
|
3508
3598
|
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
3509
3599
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3510
3600
|
}
|
|
3601
|
+
function isPolicyDenyNavigationError(err) {
|
|
3602
|
+
return err instanceof InvalidBrowserNavigationUrlError;
|
|
3603
|
+
}
|
|
3604
|
+
function isTopLevelNavigationRequest(page, request) {
|
|
3605
|
+
if (!request.isNavigationRequest()) return false;
|
|
3606
|
+
try {
|
|
3607
|
+
return request.frame() === page.mainFrame();
|
|
3608
|
+
} catch {
|
|
3609
|
+
return true;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
async function closeBlockedNavigationTarget(opts) {
|
|
3613
|
+
markPageRefBlocked(opts.cdpUrl, opts.page);
|
|
3614
|
+
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
|
|
3615
|
+
const fallbackTargetId = opts.targetId?.trim() ?? "";
|
|
3616
|
+
const targetIdToBlock = resolvedTargetId ?? fallbackTargetId;
|
|
3617
|
+
if (targetIdToBlock) markTargetBlocked(opts.cdpUrl, targetIdToBlock);
|
|
3618
|
+
await opts.page.close().catch((e) => {
|
|
3619
|
+
console.warn("[browserclaw] failed to close blocked page", e);
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
async function assertPageNavigationCompletedSafely(opts) {
|
|
3623
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3624
|
+
try {
|
|
3625
|
+
await assertBrowserNavigationRedirectChainAllowed({ request: opts.response?.request(), ...navigationPolicy });
|
|
3626
|
+
await assertBrowserNavigationResultAllowed({ url: opts.page.url(), ...navigationPolicy });
|
|
3627
|
+
} catch (err) {
|
|
3628
|
+
if (isPolicyDenyNavigationError(err))
|
|
3629
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3630
|
+
throw err;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
async function gotoPageWithNavigationGuard(opts) {
|
|
3634
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3635
|
+
const state = { blocked: null };
|
|
3636
|
+
const handler = async (route, request) => {
|
|
3637
|
+
if (state.blocked !== null) {
|
|
3638
|
+
await route.abort().catch((e) => {
|
|
3639
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3640
|
+
});
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
if (!isTopLevelNavigationRequest(opts.page, request)) {
|
|
3644
|
+
await route.continue();
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
try {
|
|
3648
|
+
await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
|
|
3649
|
+
} catch (err) {
|
|
3650
|
+
if (isPolicyDenyNavigationError(err)) {
|
|
3651
|
+
state.blocked = err;
|
|
3652
|
+
await route.abort().catch((e) => {
|
|
3653
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3654
|
+
});
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
throw err;
|
|
3658
|
+
}
|
|
3659
|
+
await route.continue();
|
|
3660
|
+
};
|
|
3661
|
+
await opts.page.route("**", handler);
|
|
3662
|
+
try {
|
|
3663
|
+
const response = await opts.page.goto(opts.url, { timeout: opts.timeoutMs });
|
|
3664
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3665
|
+
return response;
|
|
3666
|
+
} catch (err) {
|
|
3667
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3668
|
+
throw err;
|
|
3669
|
+
} finally {
|
|
3670
|
+
await opts.page.unroute("**", handler).catch((e) => {
|
|
3671
|
+
console.warn("[browserclaw] route unroute failed", e);
|
|
3672
|
+
});
|
|
3673
|
+
if (state.blocked !== null)
|
|
3674
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3511
3677
|
async function navigateViaPlaywright(opts) {
|
|
3512
3678
|
const url = opts.url.trim();
|
|
3513
3679
|
if (!url) throw new Error("url is required");
|
|
@@ -3516,7 +3682,14 @@ async function navigateViaPlaywright(opts) {
|
|
|
3516
3682
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3517
3683
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3518
3684
|
ensurePageState(page);
|
|
3519
|
-
const navigate = async () => await
|
|
3685
|
+
const navigate = async () => await gotoPageWithNavigationGuard({
|
|
3686
|
+
cdpUrl: opts.cdpUrl,
|
|
3687
|
+
page,
|
|
3688
|
+
url,
|
|
3689
|
+
timeoutMs: timeout,
|
|
3690
|
+
ssrfPolicy: policy,
|
|
3691
|
+
targetId: opts.targetId
|
|
3692
|
+
});
|
|
3520
3693
|
let response;
|
|
3521
3694
|
try {
|
|
3522
3695
|
response = await navigate();
|
|
@@ -3530,21 +3703,23 @@ async function navigateViaPlaywright(opts) {
|
|
|
3530
3703
|
ensurePageState(page);
|
|
3531
3704
|
response = await navigate();
|
|
3532
3705
|
}
|
|
3533
|
-
await
|
|
3534
|
-
|
|
3535
|
-
|
|
3706
|
+
await assertPageNavigationCompletedSafely({
|
|
3707
|
+
cdpUrl: opts.cdpUrl,
|
|
3708
|
+
page,
|
|
3709
|
+
response,
|
|
3710
|
+
ssrfPolicy: policy,
|
|
3711
|
+
targetId: opts.targetId
|
|
3536
3712
|
});
|
|
3537
|
-
|
|
3538
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3539
|
-
return { url: finalUrl };
|
|
3713
|
+
return { url: page.url() };
|
|
3540
3714
|
}
|
|
3541
3715
|
async function listPagesViaPlaywright(opts) {
|
|
3542
3716
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3543
3717
|
const pages = getAllPages(browser);
|
|
3544
3718
|
const results = [];
|
|
3545
3719
|
for (const page of pages) {
|
|
3720
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) continue;
|
|
3546
3721
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3547
|
-
if (tid !== null && tid !== "")
|
|
3722
|
+
if (tid !== null && tid !== "" && !isBlockedTarget(opts.cdpUrl, tid))
|
|
3548
3723
|
results.push({
|
|
3549
3724
|
targetId: tid,
|
|
3550
3725
|
title: await page.title().catch(() => ""),
|
|
@@ -3560,18 +3735,35 @@ async function createPageViaPlaywright(opts) {
|
|
|
3560
3735
|
ensureContextState(context);
|
|
3561
3736
|
const page = await context.newPage();
|
|
3562
3737
|
ensurePageState(page);
|
|
3738
|
+
clearBlockedPageRef(opts.cdpUrl, page);
|
|
3739
|
+
const createdTargetId = await pageTargetId(page).catch(() => null);
|
|
3740
|
+
clearBlockedTarget(opts.cdpUrl, createdTargetId ?? void 0);
|
|
3563
3741
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3564
3742
|
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3565
3743
|
if (targetUrl !== "about:blank") {
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3744
|
+
await assertBrowserNavigationAllowed({ url: targetUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3745
|
+
let response = null;
|
|
3746
|
+
try {
|
|
3747
|
+
response = await gotoPageWithNavigationGuard({
|
|
3748
|
+
cdpUrl: opts.cdpUrl,
|
|
3749
|
+
page,
|
|
3750
|
+
url: targetUrl,
|
|
3751
|
+
timeoutMs: 3e4,
|
|
3752
|
+
ssrfPolicy: policy,
|
|
3753
|
+
targetId: createdTargetId ?? void 0
|
|
3754
|
+
});
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
|
|
3757
|
+
}
|
|
3758
|
+
await assertPageNavigationCompletedSafely({
|
|
3759
|
+
cdpUrl: opts.cdpUrl,
|
|
3760
|
+
page,
|
|
3761
|
+
response,
|
|
3762
|
+
ssrfPolicy: policy,
|
|
3763
|
+
targetId: createdTargetId ?? void 0
|
|
3571
3764
|
});
|
|
3572
|
-
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3573
3765
|
}
|
|
3574
|
-
const tid = await pageTargetId(page).catch(() => null);
|
|
3766
|
+
const tid = createdTargetId ?? await pageTargetId(page).catch(() => null);
|
|
3575
3767
|
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3576
3768
|
return {
|
|
3577
3769
|
targetId: tid,
|