browserclaw 0.4.2 → 0.5.0
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 +633 -255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -10
- package/dist/index.d.ts +58 -10
- package/dist/index.js +612 -259
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
-
import path, {
|
|
2
|
+
import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import net from 'net';
|
|
5
5
|
import { spawn, execFileSync } from 'child_process';
|
|
6
6
|
import { devices, chromium } from 'playwright-core';
|
|
7
|
-
import { lookup } from 'dns/promises';
|
|
8
|
-
import {
|
|
7
|
+
import { lookup as lookup$1 } from 'dns/promises';
|
|
8
|
+
import { lookup } from 'dns';
|
|
9
|
+
import { realpath, rename, rm, lstat } from 'fs/promises';
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
import * as ipaddr from 'ipaddr.js';
|
|
9
12
|
|
|
10
13
|
// src/chrome-launcher.ts
|
|
11
14
|
var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
@@ -615,8 +618,31 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
615
618
|
var cached = null;
|
|
616
619
|
var connectingByUrl = /* @__PURE__ */ new Map();
|
|
617
620
|
var pageStates = /* @__PURE__ */ new WeakMap();
|
|
621
|
+
var contextStates = /* @__PURE__ */ new WeakMap();
|
|
618
622
|
var observedContexts = /* @__PURE__ */ new WeakSet();
|
|
619
623
|
var observedPages = /* @__PURE__ */ new WeakSet();
|
|
624
|
+
var nextUploadArmId = 0;
|
|
625
|
+
var nextDialogArmId = 0;
|
|
626
|
+
var nextDownloadArmId = 0;
|
|
627
|
+
function bumpUploadArmId() {
|
|
628
|
+
nextUploadArmId += 1;
|
|
629
|
+
return nextUploadArmId;
|
|
630
|
+
}
|
|
631
|
+
function bumpDialogArmId() {
|
|
632
|
+
nextDialogArmId += 1;
|
|
633
|
+
return nextDialogArmId;
|
|
634
|
+
}
|
|
635
|
+
function bumpDownloadArmId() {
|
|
636
|
+
nextDownloadArmId += 1;
|
|
637
|
+
return nextDownloadArmId;
|
|
638
|
+
}
|
|
639
|
+
function ensureContextState(context) {
|
|
640
|
+
const existing = contextStates.get(context);
|
|
641
|
+
if (existing) return existing;
|
|
642
|
+
const state = { traceActive: false };
|
|
643
|
+
contextStates.set(context, state);
|
|
644
|
+
return state;
|
|
645
|
+
}
|
|
620
646
|
var roleRefsByTarget = /* @__PURE__ */ new Map();
|
|
621
647
|
var MAX_ROLE_REFS_CACHE = 50;
|
|
622
648
|
var MAX_CONSOLE_MESSAGES = 500;
|
|
@@ -636,7 +662,10 @@ function ensurePageState(page) {
|
|
|
636
662
|
errors: [],
|
|
637
663
|
requests: [],
|
|
638
664
|
requestIds: /* @__PURE__ */ new WeakMap(),
|
|
639
|
-
nextRequestId: 0
|
|
665
|
+
nextRequestId: 0,
|
|
666
|
+
armIdUpload: 0,
|
|
667
|
+
armIdDialog: 0,
|
|
668
|
+
armIdDownload: 0
|
|
640
669
|
};
|
|
641
670
|
pageStates.set(page, state);
|
|
642
671
|
if (!observedPages.has(page)) {
|
|
@@ -713,6 +742,7 @@ function applyStealthToPage(page) {
|
|
|
713
742
|
function observeContext(context) {
|
|
714
743
|
if (observedContexts.has(context)) return;
|
|
715
744
|
observedContexts.add(context);
|
|
745
|
+
ensureContextState(context);
|
|
716
746
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
717
747
|
if (process.env.DEBUG) console.warn("[browserclaw] stealth initScript failed:", e.message);
|
|
718
748
|
});
|
|
@@ -808,6 +838,35 @@ async function disconnectBrowser() {
|
|
|
808
838
|
if (cur) await cur.browser.close().catch(() => {
|
|
809
839
|
});
|
|
810
840
|
}
|
|
841
|
+
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
842
|
+
const normalized = normalizeCdpUrl(opts.cdpUrl);
|
|
843
|
+
const cur = cached;
|
|
844
|
+
if (!cur || cur.cdpUrl !== normalized) return;
|
|
845
|
+
cached = null;
|
|
846
|
+
connectingByUrl.delete(normalized);
|
|
847
|
+
const targetId = opts.targetId?.trim() || "";
|
|
848
|
+
if (targetId) {
|
|
849
|
+
try {
|
|
850
|
+
const pages = await getAllPages(cur.browser);
|
|
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
|
+
}
|
|
866
|
+
}
|
|
867
|
+
cur.browser.close().catch(() => {
|
|
868
|
+
});
|
|
869
|
+
}
|
|
811
870
|
async function getAllPages(browser) {
|
|
812
871
|
return browser.contexts().flatMap((c) => c.pages());
|
|
813
872
|
}
|
|
@@ -1329,6 +1388,11 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
1329
1388
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1330
1389
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1331
1390
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1391
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
1392
|
+
"localhost",
|
|
1393
|
+
"localhost.localdomain",
|
|
1394
|
+
"metadata.google.internal"
|
|
1395
|
+
]);
|
|
1332
1396
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
1333
1397
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
1334
1398
|
}
|
|
@@ -1342,6 +1406,218 @@ function hasProxyEnvConfigured(env = process.env) {
|
|
|
1342
1406
|
}
|
|
1343
1407
|
return false;
|
|
1344
1408
|
}
|
|
1409
|
+
function normalizeHostname(hostname) {
|
|
1410
|
+
let h = String(hostname ?? "").trim().toLowerCase();
|
|
1411
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
1412
|
+
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
1413
|
+
return h;
|
|
1414
|
+
}
|
|
1415
|
+
function isBlockedHostnameNormalized(normalized) {
|
|
1416
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) return true;
|
|
1417
|
+
return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal");
|
|
1418
|
+
}
|
|
1419
|
+
function isStrictDecimalOctet(part) {
|
|
1420
|
+
if (!/^[0-9]+$/.test(part)) return false;
|
|
1421
|
+
const n = parseInt(part, 10);
|
|
1422
|
+
if (n < 0 || n > 255) return false;
|
|
1423
|
+
if (String(n) !== part) return false;
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
function isUnsupportedIPv4Literal(ip) {
|
|
1427
|
+
if (/^[0-9]+$/.test(ip)) return true;
|
|
1428
|
+
const parts = ip.split(".");
|
|
1429
|
+
if (parts.length !== 4) return true;
|
|
1430
|
+
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
1434
|
+
"unspecified",
|
|
1435
|
+
"broadcast",
|
|
1436
|
+
"multicast",
|
|
1437
|
+
"linkLocal",
|
|
1438
|
+
"loopback",
|
|
1439
|
+
"carrierGradeNat",
|
|
1440
|
+
"private",
|
|
1441
|
+
"reserved"
|
|
1442
|
+
]);
|
|
1443
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
1444
|
+
"unspecified",
|
|
1445
|
+
"loopback",
|
|
1446
|
+
"linkLocal",
|
|
1447
|
+
"uniqueLocal",
|
|
1448
|
+
"multicast"
|
|
1449
|
+
]);
|
|
1450
|
+
var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
1451
|
+
function isBlockedSpecialUseIpv4Address(address, opts) {
|
|
1452
|
+
const inRfc2544 = address.match(RFC2544_BENCHMARK_PREFIX);
|
|
1453
|
+
if (inRfc2544 && opts?.allowRfc2544BenchmarkRange === true) return false;
|
|
1454
|
+
return BLOCKED_IPV4_RANGES.has(address.range()) || inRfc2544;
|
|
1455
|
+
}
|
|
1456
|
+
function isBlockedSpecialUseIpv6Address(address) {
|
|
1457
|
+
if (BLOCKED_IPV6_RANGES.has(address.range())) return true;
|
|
1458
|
+
return (address.parts[0] & 65472) === 65216;
|
|
1459
|
+
}
|
|
1460
|
+
function extractEmbeddedIpv4FromIpv6(v6, opts) {
|
|
1461
|
+
if (v6.isIPv4MappedAddress()) {
|
|
1462
|
+
return isBlockedSpecialUseIpv4Address(v6.toIPv4Address(), opts);
|
|
1463
|
+
}
|
|
1464
|
+
const parts = v6.parts;
|
|
1465
|
+
if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1466
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1467
|
+
try {
|
|
1468
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1469
|
+
} catch {
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 1) {
|
|
1474
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1475
|
+
try {
|
|
1476
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1477
|
+
} catch {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (parts[0] === 8194) {
|
|
1482
|
+
const ip4str = `${parts[1] >> 8 & 255}.${parts[1] & 255}.${parts[2] >> 8 & 255}.${parts[2] & 255}`;
|
|
1483
|
+
try {
|
|
1484
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1485
|
+
} catch {
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (parts[0] === 8193 && parts[1] === 0) {
|
|
1490
|
+
const hiXored = parts[6] ^ 65535;
|
|
1491
|
+
const loXored = parts[7] ^ 65535;
|
|
1492
|
+
const ip4str = `${hiXored >> 8 & 255}.${hiXored & 255}.${loXored >> 8 & 255}.${loXored & 255}`;
|
|
1493
|
+
try {
|
|
1494
|
+
return isBlockedSpecialUseIpv4Address(ipaddr.IPv4.parse(ip4str), opts);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
function isPrivateIpAddress(address, opts) {
|
|
1502
|
+
let normalized = address.trim().toLowerCase();
|
|
1503
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
|
|
1504
|
+
if (!normalized) return false;
|
|
1505
|
+
try {
|
|
1506
|
+
const parsed = ipaddr.parse(normalized);
|
|
1507
|
+
if (parsed.kind() === "ipv4") {
|
|
1508
|
+
return isBlockedSpecialUseIpv4Address(parsed, opts);
|
|
1509
|
+
}
|
|
1510
|
+
const v6 = parsed;
|
|
1511
|
+
if (isBlockedSpecialUseIpv6Address(v6)) return true;
|
|
1512
|
+
const embeddedV4 = extractEmbeddedIpv4FromIpv6(v6, opts);
|
|
1513
|
+
if (embeddedV4 !== null) return embeddedV4;
|
|
1514
|
+
return false;
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
if (!normalized.includes(":") && isUnsupportedIPv4Literal(normalized)) return true;
|
|
1518
|
+
if (normalized.includes(":")) return true;
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
function dedupeAndPreferIpv4(results) {
|
|
1522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1523
|
+
const ipv4 = [];
|
|
1524
|
+
const ipv6 = [];
|
|
1525
|
+
for (const r of results) {
|
|
1526
|
+
if (seen.has(r.address)) continue;
|
|
1527
|
+
seen.add(r.address);
|
|
1528
|
+
if (r.family === 4) ipv4.push(r.address);
|
|
1529
|
+
else ipv6.push(r.address);
|
|
1530
|
+
}
|
|
1531
|
+
return [...ipv4, ...ipv6];
|
|
1532
|
+
}
|
|
1533
|
+
function createPinnedLookup(params) {
|
|
1534
|
+
const normalizedHost = normalizeHostname(params.hostname);
|
|
1535
|
+
const fallback = params.fallback ?? lookup;
|
|
1536
|
+
const records = params.addresses.map((address) => ({
|
|
1537
|
+
address,
|
|
1538
|
+
family: address.includes(":") ? 6 : 4
|
|
1539
|
+
}));
|
|
1540
|
+
let index = 0;
|
|
1541
|
+
return ((host, options, callback) => {
|
|
1542
|
+
const cb = typeof options === "function" ? options : callback;
|
|
1543
|
+
if (!cb) return;
|
|
1544
|
+
const normalized = normalizeHostname(host);
|
|
1545
|
+
if (!normalized || normalized !== normalizedHost) {
|
|
1546
|
+
if (typeof options === "function" || options === void 0) return fallback(host, cb);
|
|
1547
|
+
return fallback(host, options, cb);
|
|
1548
|
+
}
|
|
1549
|
+
const opts = typeof options === "object" && options !== null ? options : {};
|
|
1550
|
+
const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0;
|
|
1551
|
+
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
1552
|
+
const usable = candidates.length > 0 ? candidates : records;
|
|
1553
|
+
if (opts.all) {
|
|
1554
|
+
cb(null, usable);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const chosen = usable[index % usable.length];
|
|
1558
|
+
index += 1;
|
|
1559
|
+
cb(null, chosen.address, chosen.family);
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
1563
|
+
const normalized = normalizeHostname(hostname);
|
|
1564
|
+
if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
|
|
1565
|
+
const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
|
|
1566
|
+
const allowedHostnames = [
|
|
1567
|
+
...params.policy?.allowedHostnames ?? [],
|
|
1568
|
+
...params.policy?.hostnameAllowlist ?? []
|
|
1569
|
+
].map((h) => normalizeHostname(h));
|
|
1570
|
+
const isExplicitlyAllowed = allowedHostnames.some((h) => h === normalized);
|
|
1571
|
+
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
1572
|
+
if (!skipPrivateNetworkChecks) {
|
|
1573
|
+
if (isBlockedHostnameNormalized(normalized)) {
|
|
1574
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1575
|
+
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1579
|
+
if (isPrivateIpAddress(normalized, ipOpts)) {
|
|
1580
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1581
|
+
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const lookupFn = params.lookupFn ?? lookup$1;
|
|
1586
|
+
let results;
|
|
1587
|
+
try {
|
|
1588
|
+
results = await lookupFn(normalized, { all: true });
|
|
1589
|
+
} catch {
|
|
1590
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1591
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (!results || results.length === 0) {
|
|
1595
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1596
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
if (!skipPrivateNetworkChecks) {
|
|
1600
|
+
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1601
|
+
for (const r of results) {
|
|
1602
|
+
if (isPrivateIpAddress(r.address, ipOpts)) {
|
|
1603
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1604
|
+
`Navigation to internal/loopback address blocked: "${hostname}" resolves to "${r.address}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const addresses = dedupeAndPreferIpv4(results);
|
|
1610
|
+
if (addresses.length === 0) {
|
|
1611
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1612
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
hostname: normalized,
|
|
1617
|
+
addresses,
|
|
1618
|
+
lookup: createPinnedLookup({ hostname: normalized, addresses })
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1345
1621
|
async function assertBrowserNavigationAllowed(opts) {
|
|
1346
1622
|
const rawUrl = String(opts.url ?? "").trim();
|
|
1347
1623
|
let parsed;
|
|
@@ -1361,20 +1637,10 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1361
1637
|
}
|
|
1362
1638
|
const policy = opts.ssrfPolicy;
|
|
1363
1639
|
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (allowedHostnames.length) {
|
|
1369
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1370
|
-
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1371
|
-
}
|
|
1372
|
-
const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
|
|
1373
|
-
if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
|
|
1374
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1375
|
-
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1640
|
+
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
1641
|
+
lookupFn: opts.lookupFn,
|
|
1642
|
+
policy
|
|
1643
|
+
});
|
|
1378
1644
|
}
|
|
1379
1645
|
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
1380
1646
|
if (!path2 || typeof path2 !== "string") {
|
|
@@ -1417,111 +1683,6 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
1417
1683
|
}
|
|
1418
1684
|
}
|
|
1419
1685
|
}
|
|
1420
|
-
function expandIPv6(ip) {
|
|
1421
|
-
let normalized = ip;
|
|
1422
|
-
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1423
|
-
if (v4Match) {
|
|
1424
|
-
const octets = v4Match[2].split(".").map(Number);
|
|
1425
|
-
if (octets.some((o) => o > 255)) return null;
|
|
1426
|
-
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1427
|
-
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1428
|
-
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1429
|
-
}
|
|
1430
|
-
const halves = normalized.split("::");
|
|
1431
|
-
if (halves.length > 2) return null;
|
|
1432
|
-
if (halves.length === 2) {
|
|
1433
|
-
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1434
|
-
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1435
|
-
const needed = 8 - left.length - right.length;
|
|
1436
|
-
if (needed < 0) return null;
|
|
1437
|
-
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1438
|
-
if (groups2.length !== 8) return null;
|
|
1439
|
-
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1440
|
-
}
|
|
1441
|
-
const groups = normalized.split(":");
|
|
1442
|
-
if (groups.length !== 8) return null;
|
|
1443
|
-
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1444
|
-
}
|
|
1445
|
-
function hexToIPv4(hiHex, loHex) {
|
|
1446
|
-
const hi = parseInt(hiHex, 16);
|
|
1447
|
-
const lo = parseInt(loHex, 16);
|
|
1448
|
-
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1449
|
-
}
|
|
1450
|
-
function extractEmbeddedIPv4(lower) {
|
|
1451
|
-
if (lower.startsWith("::ffff:")) {
|
|
1452
|
-
return lower.slice(7);
|
|
1453
|
-
}
|
|
1454
|
-
const expanded = expandIPv6(lower);
|
|
1455
|
-
if (expanded === null) return "";
|
|
1456
|
-
const groups = expanded.split(":");
|
|
1457
|
-
if (groups.length !== 8) return "";
|
|
1458
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1459
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1460
|
-
}
|
|
1461
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1462
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1463
|
-
}
|
|
1464
|
-
if (groups[0] === "2002") {
|
|
1465
|
-
return hexToIPv4(groups[1], groups[2]);
|
|
1466
|
-
}
|
|
1467
|
-
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1468
|
-
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1469
|
-
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1470
|
-
return hexToIPv4(hiXored, loXored);
|
|
1471
|
-
}
|
|
1472
|
-
return null;
|
|
1473
|
-
}
|
|
1474
|
-
function isStrictDecimalOctet(part) {
|
|
1475
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1476
|
-
const n = parseInt(part, 10);
|
|
1477
|
-
if (n < 0 || n > 255) return false;
|
|
1478
|
-
if (String(n) !== part) return false;
|
|
1479
|
-
return true;
|
|
1480
|
-
}
|
|
1481
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1482
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1483
|
-
const parts = ip.split(".");
|
|
1484
|
-
if (parts.length !== 4) return true;
|
|
1485
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1486
|
-
return false;
|
|
1487
|
-
}
|
|
1488
|
-
function isInternalIP(ip, opts) {
|
|
1489
|
-
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1490
|
-
if (/^127\./.test(ip)) return true;
|
|
1491
|
-
if (/^10\./.test(ip)) return true;
|
|
1492
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1493
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
1494
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
1495
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1496
|
-
if (ip === "0.0.0.0") return true;
|
|
1497
|
-
if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
|
|
1498
|
-
const lower = ip.toLowerCase();
|
|
1499
|
-
if (lower === "::1") return true;
|
|
1500
|
-
if (lower.startsWith("fe80:")) return true;
|
|
1501
|
-
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1502
|
-
if (lower.startsWith("ff")) return true;
|
|
1503
|
-
const embedded = extractEmbeddedIPv4(lower);
|
|
1504
|
-
if (embedded !== null) {
|
|
1505
|
-
if (embedded === "") return true;
|
|
1506
|
-
return isInternalIP(embedded, opts);
|
|
1507
|
-
}
|
|
1508
|
-
return false;
|
|
1509
|
-
}
|
|
1510
|
-
function isInternalUrl(url, opts) {
|
|
1511
|
-
let parsed;
|
|
1512
|
-
try {
|
|
1513
|
-
parsed = new URL(url);
|
|
1514
|
-
} catch {
|
|
1515
|
-
return true;
|
|
1516
|
-
}
|
|
1517
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1518
|
-
if (hostname === "localhost") return true;
|
|
1519
|
-
if (isInternalIP(hostname, opts)) return true;
|
|
1520
|
-
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1521
|
-
return true;
|
|
1522
|
-
}
|
|
1523
|
-
return false;
|
|
1524
|
-
}
|
|
1525
1686
|
async function assertSafeUploadPaths(paths) {
|
|
1526
1687
|
for (const filePath of paths) {
|
|
1527
1688
|
let stat;
|
|
@@ -1538,21 +1699,48 @@ async function assertSafeUploadPaths(paths) {
|
|
|
1538
1699
|
}
|
|
1539
1700
|
}
|
|
1540
1701
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1702
|
+
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
1703
|
+
const trimmed = String(fileName ?? "").trim();
|
|
1704
|
+
if (!trimmed) return fallbackName;
|
|
1705
|
+
let base = posix.basename(trimmed);
|
|
1706
|
+
base = win32.basename(base);
|
|
1707
|
+
let cleaned = "";
|
|
1708
|
+
for (let i = 0; i < base.length; i++) {
|
|
1709
|
+
const code = base.charCodeAt(i);
|
|
1710
|
+
if (code < 32 || code === 127) continue;
|
|
1711
|
+
cleaned += base[i];
|
|
1712
|
+
}
|
|
1713
|
+
base = cleaned.trim();
|
|
1714
|
+
if (!base || base === "." || base === "..") return fallbackName;
|
|
1715
|
+
if (base.length > 200) base = base.slice(0, 200);
|
|
1716
|
+
return base;
|
|
1717
|
+
}
|
|
1718
|
+
function buildSiblingTempPath(targetPath) {
|
|
1719
|
+
const id = randomUUID();
|
|
1720
|
+
const safeTail = sanitizeUntrustedFileName(basename(targetPath), "output.bin");
|
|
1721
|
+
return join(dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
|
|
1722
|
+
}
|
|
1723
|
+
async function writeViaSiblingTempPath(params) {
|
|
1724
|
+
const rootDir = await realpath(resolve(params.rootDir)).catch(() => resolve(params.rootDir));
|
|
1725
|
+
const requestedTargetPath = resolve(params.targetPath);
|
|
1726
|
+
const targetPath = await realpath(dirname(requestedTargetPath)).then((realDir) => join(realDir, basename(requestedTargetPath))).catch(() => requestedTargetPath);
|
|
1727
|
+
const relativeTargetPath = relative(rootDir, targetPath);
|
|
1728
|
+
if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${sep}`) || isAbsolute(relativeTargetPath)) {
|
|
1729
|
+
throw new Error("Target path is outside the allowed root");
|
|
1730
|
+
}
|
|
1731
|
+
const tempPath = buildSiblingTempPath(targetPath);
|
|
1732
|
+
let renameSucceeded = false;
|
|
1544
1733
|
try {
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
if (isInternalIP(address, opts)) return true;
|
|
1552
|
-
} catch {
|
|
1553
|
-
return true;
|
|
1734
|
+
await params.writeTemp(tempPath);
|
|
1735
|
+
await rename(tempPath, targetPath);
|
|
1736
|
+
renameSucceeded = true;
|
|
1737
|
+
} finally {
|
|
1738
|
+
if (!renameSucceeded) await rm(tempPath, { force: true }).catch(() => {
|
|
1739
|
+
});
|
|
1554
1740
|
}
|
|
1555
|
-
|
|
1741
|
+
}
|
|
1742
|
+
function isAbsolute(p) {
|
|
1743
|
+
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
1556
1744
|
}
|
|
1557
1745
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
1558
1746
|
const rawUrl = String(opts.url ?? "").trim();
|
|
@@ -1660,13 +1848,12 @@ async function fillFormViaPlaywright(opts) {
|
|
|
1660
1848
|
ensurePageState(page);
|
|
1661
1849
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1662
1850
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
1663
|
-
for (
|
|
1664
|
-
const field = opts.fields[i];
|
|
1851
|
+
for (const field of opts.fields) {
|
|
1665
1852
|
const ref = field.ref.trim();
|
|
1666
1853
|
const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
|
|
1667
1854
|
const rawValue = field.value;
|
|
1668
|
-
const value = rawValue
|
|
1669
|
-
if (!ref)
|
|
1855
|
+
const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
|
|
1856
|
+
if (!ref) continue;
|
|
1670
1857
|
const locator = refLocator(page, ref);
|
|
1671
1858
|
if (type === "checkbox" || type === "radio") {
|
|
1672
1859
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
@@ -1710,61 +1897,78 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
1710
1897
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1711
1898
|
ensurePageState(page);
|
|
1712
1899
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1713
|
-
|
|
1714
|
-
|
|
1900
|
+
if (!opts.paths.length) throw new Error("paths are required");
|
|
1901
|
+
const inputRef = typeof opts.ref === "string" ? opts.ref.trim() : "";
|
|
1902
|
+
const element = typeof opts.element === "string" ? opts.element.trim() : "";
|
|
1903
|
+
if (inputRef && element) throw new Error("ref and element are mutually exclusive");
|
|
1904
|
+
if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
|
|
1905
|
+
const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
|
|
1715
1906
|
await assertSafeUploadPaths(opts.paths);
|
|
1716
1907
|
try {
|
|
1717
1908
|
await locator.setInputFiles(opts.paths);
|
|
1718
1909
|
} catch (err) {
|
|
1719
|
-
throw toAIFriendlyError(err,
|
|
1910
|
+
throw toAIFriendlyError(err, inputRef || element);
|
|
1911
|
+
}
|
|
1912
|
+
try {
|
|
1913
|
+
const handle = await locator.elementHandle();
|
|
1914
|
+
if (handle) {
|
|
1915
|
+
await handle.evaluate((el) => {
|
|
1916
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1917
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
} catch {
|
|
1720
1921
|
}
|
|
1721
1922
|
}
|
|
1722
1923
|
async function armDialogViaPlaywright(opts) {
|
|
1723
1924
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1724
|
-
ensurePageState(page);
|
|
1725
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
try {
|
|
1734
|
-
if (opts.accept) {
|
|
1735
|
-
await dialog.accept(opts.promptText);
|
|
1736
|
-
} else {
|
|
1737
|
-
await dialog.dismiss();
|
|
1738
|
-
}
|
|
1739
|
-
resolve2();
|
|
1740
|
-
} catch (err) {
|
|
1741
|
-
reject(err);
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
page.once("dialog", handler);
|
|
1925
|
+
const state = ensurePageState(page);
|
|
1926
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1927
|
+
state.armIdDialog = bumpDialogArmId();
|
|
1928
|
+
const armId = state.armIdDialog;
|
|
1929
|
+
page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
|
|
1930
|
+
if (state.armIdDialog !== armId) return;
|
|
1931
|
+
if (opts.accept) await dialog.accept(opts.promptText);
|
|
1932
|
+
else await dialog.dismiss();
|
|
1933
|
+
}).catch(() => {
|
|
1745
1934
|
});
|
|
1746
1935
|
}
|
|
1747
1936
|
async function armFileUploadViaPlaywright(opts) {
|
|
1748
1937
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1749
|
-
ensurePageState(page);
|
|
1750
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const handler = async (fc) => {
|
|
1757
|
-
clearTimeout(timer);
|
|
1938
|
+
const state = ensurePageState(page);
|
|
1939
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1940
|
+
state.armIdUpload = bumpUploadArmId();
|
|
1941
|
+
const armId = state.armIdUpload;
|
|
1942
|
+
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
1943
|
+
if (state.armIdUpload !== armId) return;
|
|
1944
|
+
if (!opts.paths?.length) {
|
|
1758
1945
|
try {
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
await fc.setFiles(paths);
|
|
1762
|
-
resolve2();
|
|
1763
|
-
} catch (err) {
|
|
1764
|
-
reject(err);
|
|
1946
|
+
await page.keyboard.press("Escape");
|
|
1947
|
+
} catch {
|
|
1765
1948
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
try {
|
|
1952
|
+
await assertSafeUploadPaths(opts.paths);
|
|
1953
|
+
} catch {
|
|
1954
|
+
try {
|
|
1955
|
+
await page.keyboard.press("Escape");
|
|
1956
|
+
} catch {
|
|
1957
|
+
}
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
await fileChooser.setFiles(opts.paths);
|
|
1961
|
+
try {
|
|
1962
|
+
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
1963
|
+
if (input) {
|
|
1964
|
+
await input.evaluate((el) => {
|
|
1965
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1966
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
}).catch(() => {
|
|
1768
1972
|
});
|
|
1769
1973
|
}
|
|
1770
1974
|
|
|
@@ -1778,17 +1982,35 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1778
1982
|
}
|
|
1779
1983
|
|
|
1780
1984
|
// src/actions/navigation.ts
|
|
1985
|
+
function isRetryableNavigateError(err) {
|
|
1986
|
+
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
1987
|
+
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
1988
|
+
}
|
|
1781
1989
|
async function navigateViaPlaywright(opts) {
|
|
1782
1990
|
const url = String(opts.url ?? "").trim();
|
|
1783
1991
|
if (!url) throw new Error("url is required");
|
|
1784
1992
|
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
1785
|
-
await assertBrowserNavigationAllowed({ url,
|
|
1786
|
-
const
|
|
1993
|
+
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
1994
|
+
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
1995
|
+
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1787
1996
|
ensurePageState(page);
|
|
1788
|
-
const
|
|
1997
|
+
const navigate = async () => await page.goto(url, { timeout });
|
|
1998
|
+
let response;
|
|
1999
|
+
try {
|
|
2000
|
+
response = await navigate();
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
if (!isRetryableNavigateError(err)) throw err;
|
|
2003
|
+
await forceDisconnectPlaywrightForTarget({
|
|
2004
|
+
cdpUrl: opts.cdpUrl,
|
|
2005
|
+
targetId: opts.targetId}).catch(() => {
|
|
2006
|
+
});
|
|
2007
|
+
page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2008
|
+
ensurePageState(page);
|
|
2009
|
+
response = await navigate();
|
|
2010
|
+
}
|
|
1789
2011
|
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...withBrowserNavigationPolicy(policy) });
|
|
1790
2012
|
const finalUrl = page.url();
|
|
1791
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl,
|
|
2013
|
+
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
1792
2014
|
return { url: finalUrl };
|
|
1793
2015
|
}
|
|
1794
2016
|
async function listPagesViaPlaywright(opts) {
|
|
@@ -1814,11 +2036,12 @@ async function createPageViaPlaywright(opts) {
|
|
|
1814
2036
|
}
|
|
1815
2037
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1816
2038
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
2039
|
+
ensureContextState(context);
|
|
1817
2040
|
const page = await context.newPage();
|
|
1818
2041
|
ensurePageState(page);
|
|
1819
2042
|
if (targetUrl !== "about:blank") {
|
|
1820
2043
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
1821
|
-
const response = await page.goto(targetUrl, { timeout:
|
|
2044
|
+
const response = await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null);
|
|
1822
2045
|
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
|
|
1823
2046
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
|
|
1824
2047
|
}
|
|
@@ -1927,68 +2150,136 @@ async function evaluateInAllFramesViaPlaywright(opts) {
|
|
|
1927
2150
|
}
|
|
1928
2151
|
return results;
|
|
1929
2152
|
}
|
|
2153
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2154
|
+
if (!abortPromise) return await evalPromise;
|
|
2155
|
+
try {
|
|
2156
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
evalPromise.catch(() => {
|
|
2159
|
+
});
|
|
2160
|
+
throw err;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
var BROWSER_EVALUATOR = new Function("args", `
|
|
2164
|
+
"use strict";
|
|
2165
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2166
|
+
try {
|
|
2167
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2168
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2169
|
+
if (result && typeof result.then === "function") {
|
|
2170
|
+
return Promise.race([
|
|
2171
|
+
result,
|
|
2172
|
+
new Promise(function(_, reject) {
|
|
2173
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2174
|
+
})
|
|
2175
|
+
]);
|
|
2176
|
+
}
|
|
2177
|
+
return result;
|
|
2178
|
+
} catch (err) {
|
|
2179
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2180
|
+
}
|
|
2181
|
+
`);
|
|
2182
|
+
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
2183
|
+
"use strict";
|
|
2184
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2185
|
+
try {
|
|
2186
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2187
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2188
|
+
if (result && typeof result.then === "function") {
|
|
2189
|
+
return Promise.race([
|
|
2190
|
+
result,
|
|
2191
|
+
new Promise(function(_, reject) {
|
|
2192
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2193
|
+
})
|
|
2194
|
+
]);
|
|
2195
|
+
}
|
|
2196
|
+
return result;
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2199
|
+
}
|
|
2200
|
+
`);
|
|
1930
2201
|
async function evaluateViaPlaywright(opts) {
|
|
1931
2202
|
const fnText = String(opts.fn ?? "").trim();
|
|
1932
2203
|
if (!fnText) throw new Error("function is required");
|
|
1933
2204
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1934
2205
|
ensurePageState(page);
|
|
1935
2206
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1936
|
-
const
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2207
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2208
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2209
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2210
|
+
const signal = opts.signal;
|
|
2211
|
+
let abortListener;
|
|
2212
|
+
let abortReject;
|
|
2213
|
+
let abortPromise;
|
|
2214
|
+
if (signal) {
|
|
2215
|
+
abortPromise = new Promise((_, reject) => {
|
|
2216
|
+
abortReject = reject;
|
|
2217
|
+
});
|
|
2218
|
+
abortPromise.catch(() => {
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
if (signal) {
|
|
2222
|
+
const disconnect = () => {
|
|
2223
|
+
forceDisconnectPlaywrightForTarget({
|
|
2224
|
+
cdpUrl: opts.cdpUrl,
|
|
2225
|
+
targetId: opts.targetId}).catch(() => {
|
|
2226
|
+
});
|
|
2227
|
+
};
|
|
2228
|
+
if (signal.aborted) {
|
|
2229
|
+
disconnect();
|
|
2230
|
+
throw signal.reason ?? new Error("aborted");
|
|
2231
|
+
}
|
|
2232
|
+
abortListener = () => {
|
|
2233
|
+
disconnect();
|
|
2234
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2235
|
+
};
|
|
2236
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2237
|
+
if (signal.aborted) {
|
|
2238
|
+
abortListener();
|
|
2239
|
+
throw signal.reason ?? new Error("aborted");
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
if (opts.ref) {
|
|
2244
|
+
const locator = refLocator(page, opts.ref);
|
|
2245
|
+
return await awaitEvalWithAbort(
|
|
2246
|
+
locator.evaluate(ELEMENT_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
2247
|
+
abortPromise
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
return await awaitEvalWithAbort(
|
|
2251
|
+
page.evaluate(BROWSER_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
2252
|
+
abortPromise
|
|
1951
2253
|
);
|
|
2254
|
+
} finally {
|
|
2255
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
1952
2256
|
}
|
|
1953
|
-
const evalPromise = page.evaluate(
|
|
1954
|
-
// eslint-disable-next-line no-eval
|
|
1955
|
-
(fnBody) => {
|
|
1956
|
-
try {
|
|
1957
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
1958
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
1959
|
-
} catch (err) {
|
|
1960
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
1961
|
-
}
|
|
1962
|
-
},
|
|
1963
|
-
fnText
|
|
1964
|
-
);
|
|
1965
|
-
if (!opts.signal) return evalPromise;
|
|
1966
|
-
return Promise.race([
|
|
1967
|
-
evalPromise,
|
|
1968
|
-
new Promise((_, reject) => {
|
|
1969
|
-
opts.signal.addEventListener("abort", () => reject(new Error("Evaluate aborted")), { once: true });
|
|
1970
|
-
})
|
|
1971
|
-
]);
|
|
1972
2257
|
}
|
|
1973
|
-
|
|
1974
|
-
// src/actions/download.ts
|
|
1975
2258
|
async function downloadViaPlaywright(opts) {
|
|
1976
2259
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1977
2260
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1978
|
-
ensurePageState(page);
|
|
2261
|
+
const state = ensurePageState(page);
|
|
1979
2262
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1980
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
2263
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1981
2264
|
const locator = refLocator(page, opts.ref);
|
|
2265
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
1982
2266
|
try {
|
|
1983
2267
|
const [download] = await Promise.all([
|
|
1984
2268
|
page.waitForEvent("download", { timeout }),
|
|
1985
2269
|
locator.click({ timeout })
|
|
1986
2270
|
]);
|
|
1987
|
-
|
|
2271
|
+
const outPath = opts.path;
|
|
2272
|
+
await writeViaSiblingTempPath({
|
|
2273
|
+
rootDir: dirname(outPath),
|
|
2274
|
+
targetPath: outPath,
|
|
2275
|
+
writeTemp: async (tempPath) => {
|
|
2276
|
+
await download.saveAs(tempPath);
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
1988
2279
|
return {
|
|
1989
2280
|
url: download.url(),
|
|
1990
2281
|
suggestedFilename: download.suggestedFilename(),
|
|
1991
|
-
path:
|
|
2282
|
+
path: outPath
|
|
1992
2283
|
};
|
|
1993
2284
|
} catch (err) {
|
|
1994
2285
|
throw toAIFriendlyError(err, opts.ref);
|
|
@@ -1997,11 +2288,17 @@ async function downloadViaPlaywright(opts) {
|
|
|
1997
2288
|
async function waitForDownloadViaPlaywright(opts) {
|
|
1998
2289
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1999
2290
|
ensurePageState(page);
|
|
2000
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
2291
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2001
2292
|
const download = await page.waitForEvent("download", { timeout });
|
|
2002
2293
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
2003
2294
|
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
2004
|
-
await
|
|
2295
|
+
await writeViaSiblingTempPath({
|
|
2296
|
+
rootDir: dirname(savePath),
|
|
2297
|
+
targetPath: savePath,
|
|
2298
|
+
writeTemp: async (tempPath) => {
|
|
2299
|
+
await download.saveAs(tempPath);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2005
2302
|
return {
|
|
2006
2303
|
url: download.url(),
|
|
2007
2304
|
suggestedFilename: download.suggestedFilename(),
|
|
@@ -2014,33 +2311,51 @@ async function emulateMediaViaPlaywright(opts) {
|
|
|
2014
2311
|
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
2015
2312
|
}
|
|
2016
2313
|
async function setDeviceViaPlaywright(opts) {
|
|
2017
|
-
const
|
|
2314
|
+
const name = String(opts.name ?? "").trim();
|
|
2315
|
+
if (!name) throw new Error("device name is required");
|
|
2316
|
+
const device = devices[name];
|
|
2018
2317
|
if (!device) {
|
|
2019
|
-
|
|
2020
|
-
throw new Error(`Unknown device "${opts.name}". Some available devices: ${available}...`);
|
|
2318
|
+
throw new Error(`Unknown device "${name}".`);
|
|
2021
2319
|
}
|
|
2022
2320
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2023
2321
|
ensurePageState(page);
|
|
2024
2322
|
if (device.viewport) {
|
|
2025
|
-
await page.setViewportSize(
|
|
2323
|
+
await page.setViewportSize({
|
|
2324
|
+
width: device.viewport.width,
|
|
2325
|
+
height: device.viewport.height
|
|
2326
|
+
});
|
|
2026
2327
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2328
|
+
const session = await page.context().newCDPSession(page);
|
|
2329
|
+
try {
|
|
2330
|
+
const locale = device.locale;
|
|
2331
|
+
if (device.userAgent || locale) {
|
|
2031
2332
|
await session.send("Emulation.setUserAgentOverride", {
|
|
2032
|
-
userAgent: device.userAgent
|
|
2333
|
+
userAgent: device.userAgent ?? "",
|
|
2334
|
+
acceptLanguage: locale ?? void 0
|
|
2033
2335
|
});
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2336
|
+
}
|
|
2337
|
+
if (device.viewport) {
|
|
2338
|
+
await session.send("Emulation.setDeviceMetricsOverride", {
|
|
2339
|
+
mobile: Boolean(device.isMobile),
|
|
2340
|
+
width: device.viewport.width,
|
|
2341
|
+
height: device.viewport.height,
|
|
2342
|
+
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
2343
|
+
screenWidth: device.viewport.width,
|
|
2344
|
+
screenHeight: device.viewport.height
|
|
2036
2345
|
});
|
|
2037
2346
|
}
|
|
2347
|
+
if (device.hasTouch) {
|
|
2348
|
+
await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
2349
|
+
}
|
|
2350
|
+
} finally {
|
|
2351
|
+
await session.detach().catch(() => {
|
|
2352
|
+
});
|
|
2038
2353
|
}
|
|
2039
2354
|
}
|
|
2040
2355
|
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
2041
2356
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2042
2357
|
ensurePageState(page);
|
|
2043
|
-
await page.setExtraHTTPHeaders(opts.headers);
|
|
2358
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
2044
2359
|
}
|
|
2045
2360
|
async function setGeolocationViaPlaywright(opts) {
|
|
2046
2361
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -2048,38 +2363,53 @@ async function setGeolocationViaPlaywright(opts) {
|
|
|
2048
2363
|
const context = page.context();
|
|
2049
2364
|
if (opts.clear) {
|
|
2050
2365
|
await context.setGeolocation(null);
|
|
2051
|
-
await context.clearPermissions()
|
|
2366
|
+
await context.clearPermissions().catch(() => {
|
|
2367
|
+
});
|
|
2052
2368
|
return;
|
|
2053
2369
|
}
|
|
2054
|
-
if (opts.latitude
|
|
2370
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
2055
2371
|
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
2056
2372
|
}
|
|
2057
|
-
await context.grantPermissions(["geolocation"], opts.origin ? { origin: opts.origin } : void 0);
|
|
2058
2373
|
await context.setGeolocation({
|
|
2059
2374
|
latitude: opts.latitude,
|
|
2060
2375
|
longitude: opts.longitude,
|
|
2061
|
-
accuracy: opts.accuracy
|
|
2376
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
2377
|
+
});
|
|
2378
|
+
const origin = opts.origin?.trim() || (() => {
|
|
2379
|
+
try {
|
|
2380
|
+
return new URL(page.url()).origin;
|
|
2381
|
+
} catch {
|
|
2382
|
+
return "";
|
|
2383
|
+
}
|
|
2384
|
+
})();
|
|
2385
|
+
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
2062
2386
|
});
|
|
2063
2387
|
}
|
|
2064
2388
|
async function setHttpCredentialsViaPlaywright(opts) {
|
|
2065
2389
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2066
2390
|
ensurePageState(page);
|
|
2067
|
-
const context = page.context();
|
|
2068
2391
|
if (opts.clear) {
|
|
2069
|
-
await context.setHTTPCredentials(
|
|
2392
|
+
await page.context().setHTTPCredentials(null);
|
|
2070
2393
|
return;
|
|
2071
2394
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
});
|
|
2395
|
+
const username = String(opts.username ?? "");
|
|
2396
|
+
const password = String(opts.password ?? "");
|
|
2397
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
2398
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
2076
2399
|
}
|
|
2077
2400
|
async function setLocaleViaPlaywright(opts) {
|
|
2078
2401
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2079
2402
|
ensurePageState(page);
|
|
2403
|
+
const locale = String(opts.locale ?? "").trim();
|
|
2404
|
+
if (!locale) throw new Error("locale is required");
|
|
2080
2405
|
const session = await page.context().newCDPSession(page);
|
|
2081
2406
|
try {
|
|
2082
|
-
|
|
2407
|
+
try {
|
|
2408
|
+
await session.send("Emulation.setLocaleOverride", { locale });
|
|
2409
|
+
} catch (err) {
|
|
2410
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
2411
|
+
throw err;
|
|
2412
|
+
}
|
|
2083
2413
|
} finally {
|
|
2084
2414
|
await session.detach().catch(() => {
|
|
2085
2415
|
});
|
|
@@ -2093,9 +2423,18 @@ async function setOfflineViaPlaywright(opts) {
|
|
|
2093
2423
|
async function setTimezoneViaPlaywright(opts) {
|
|
2094
2424
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2095
2425
|
ensurePageState(page);
|
|
2426
|
+
const timezoneId = String(opts.timezoneId ?? "").trim();
|
|
2427
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
2096
2428
|
const session = await page.context().newCDPSession(page);
|
|
2097
2429
|
try {
|
|
2098
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
await session.send("Emulation.setTimezoneOverride", { timezoneId });
|
|
2432
|
+
} catch (err) {
|
|
2433
|
+
const msg = String(err);
|
|
2434
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
2435
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
2436
|
+
throw err;
|
|
2437
|
+
}
|
|
2099
2438
|
} finally {
|
|
2100
2439
|
await session.detach().catch(() => {
|
|
2101
2440
|
});
|
|
@@ -2171,24 +2510,38 @@ async function pdfViaPlaywright(opts) {
|
|
|
2171
2510
|
ensurePageState(page);
|
|
2172
2511
|
return { buffer: await page.pdf({ printBackground: true }) };
|
|
2173
2512
|
}
|
|
2174
|
-
|
|
2175
|
-
// src/capture/trace.ts
|
|
2176
2513
|
async function traceStartViaPlaywright(opts) {
|
|
2177
2514
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2178
2515
|
ensurePageState(page);
|
|
2179
2516
|
const context = page.context();
|
|
2517
|
+
const ctxState = ensureContextState(context);
|
|
2518
|
+
if (ctxState.traceActive) {
|
|
2519
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
2520
|
+
}
|
|
2180
2521
|
await context.tracing.start({
|
|
2181
2522
|
screenshots: opts.screenshots ?? true,
|
|
2182
2523
|
snapshots: opts.snapshots ?? true,
|
|
2183
|
-
sources: opts.sources
|
|
2524
|
+
sources: opts.sources ?? false
|
|
2184
2525
|
});
|
|
2526
|
+
ctxState.traceActive = true;
|
|
2185
2527
|
}
|
|
2186
2528
|
async function traceStopViaPlaywright(opts) {
|
|
2187
2529
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2188
2530
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2189
2531
|
ensurePageState(page);
|
|
2190
2532
|
const context = page.context();
|
|
2191
|
-
|
|
2533
|
+
const ctxState = ensureContextState(context);
|
|
2534
|
+
if (!ctxState.traceActive) {
|
|
2535
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
2536
|
+
}
|
|
2537
|
+
await writeViaSiblingTempPath({
|
|
2538
|
+
rootDir: dirname(opts.path),
|
|
2539
|
+
targetPath: opts.path,
|
|
2540
|
+
writeTemp: async (tempPath) => {
|
|
2541
|
+
await context.tracing.stop({ path: tempPath });
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
ctxState.traceActive = false;
|
|
2192
2545
|
}
|
|
2193
2546
|
|
|
2194
2547
|
// src/capture/response.ts
|
|
@@ -2271,8 +2624,8 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
2271
2624
|
const cookie = opts.cookie;
|
|
2272
2625
|
if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
|
|
2273
2626
|
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
2274
|
-
const
|
|
2275
|
-
if (!hasUrl && !
|
|
2627
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
2628
|
+
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
2276
2629
|
await page.context().addCookies([cookie]);
|
|
2277
2630
|
}
|
|
2278
2631
|
async function cookiesClearViaPlaywright(opts) {
|
|
@@ -3345,6 +3698,6 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
3345
3698
|
}
|
|
3346
3699
|
};
|
|
3347
3700
|
|
|
3348
|
-
export { BrowserClaw, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, requiresInspectableBrowserNavigationRedirects, withBrowserNavigationPolicy };
|
|
3701
|
+
export { BrowserClaw, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, createPinnedLookup, ensureContextState, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, requiresInspectableBrowserNavigationRedirects, resolvePinnedHostnameWithPolicy, sanitizeUntrustedFileName, withBrowserNavigationPolicy, writeViaSiblingTempPath };
|
|
3349
3702
|
//# sourceMappingURL=index.js.map
|
|
3350
3703
|
//# sourceMappingURL=index.js.map
|