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.cjs
CHANGED
|
@@ -7,14 +7,36 @@ var net = require('net');
|
|
|
7
7
|
var child_process = require('child_process');
|
|
8
8
|
var playwrightCore = require('playwright-core');
|
|
9
9
|
var promises = require('dns/promises');
|
|
10
|
+
var dns = require('dns');
|
|
10
11
|
var promises$1 = require('fs/promises');
|
|
12
|
+
var crypto = require('crypto');
|
|
13
|
+
var ipaddr = require('ipaddr.js');
|
|
11
14
|
|
|
12
15
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
16
|
|
|
17
|
+
function _interopNamespace(e) {
|
|
18
|
+
if (e && e.__esModule) return e;
|
|
19
|
+
var n = Object.create(null);
|
|
20
|
+
if (e) {
|
|
21
|
+
Object.keys(e).forEach(function (k) {
|
|
22
|
+
if (k !== 'default') {
|
|
23
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
24
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () { return e[k]; }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
n.default = e;
|
|
32
|
+
return Object.freeze(n);
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
15
36
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
16
37
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
17
38
|
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
39
|
+
var ipaddr__namespace = /*#__PURE__*/_interopNamespace(ipaddr);
|
|
18
40
|
|
|
19
41
|
// src/chrome-launcher.ts
|
|
20
42
|
var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
@@ -624,8 +646,31 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
624
646
|
var cached = null;
|
|
625
647
|
var connectingByUrl = /* @__PURE__ */ new Map();
|
|
626
648
|
var pageStates = /* @__PURE__ */ new WeakMap();
|
|
649
|
+
var contextStates = /* @__PURE__ */ new WeakMap();
|
|
627
650
|
var observedContexts = /* @__PURE__ */ new WeakSet();
|
|
628
651
|
var observedPages = /* @__PURE__ */ new WeakSet();
|
|
652
|
+
var nextUploadArmId = 0;
|
|
653
|
+
var nextDialogArmId = 0;
|
|
654
|
+
var nextDownloadArmId = 0;
|
|
655
|
+
function bumpUploadArmId() {
|
|
656
|
+
nextUploadArmId += 1;
|
|
657
|
+
return nextUploadArmId;
|
|
658
|
+
}
|
|
659
|
+
function bumpDialogArmId() {
|
|
660
|
+
nextDialogArmId += 1;
|
|
661
|
+
return nextDialogArmId;
|
|
662
|
+
}
|
|
663
|
+
function bumpDownloadArmId() {
|
|
664
|
+
nextDownloadArmId += 1;
|
|
665
|
+
return nextDownloadArmId;
|
|
666
|
+
}
|
|
667
|
+
function ensureContextState(context) {
|
|
668
|
+
const existing = contextStates.get(context);
|
|
669
|
+
if (existing) return existing;
|
|
670
|
+
const state = { traceActive: false };
|
|
671
|
+
contextStates.set(context, state);
|
|
672
|
+
return state;
|
|
673
|
+
}
|
|
629
674
|
var roleRefsByTarget = /* @__PURE__ */ new Map();
|
|
630
675
|
var MAX_ROLE_REFS_CACHE = 50;
|
|
631
676
|
var MAX_CONSOLE_MESSAGES = 500;
|
|
@@ -645,7 +690,10 @@ function ensurePageState(page) {
|
|
|
645
690
|
errors: [],
|
|
646
691
|
requests: [],
|
|
647
692
|
requestIds: /* @__PURE__ */ new WeakMap(),
|
|
648
|
-
nextRequestId: 0
|
|
693
|
+
nextRequestId: 0,
|
|
694
|
+
armIdUpload: 0,
|
|
695
|
+
armIdDialog: 0,
|
|
696
|
+
armIdDownload: 0
|
|
649
697
|
};
|
|
650
698
|
pageStates.set(page, state);
|
|
651
699
|
if (!observedPages.has(page)) {
|
|
@@ -722,6 +770,7 @@ function applyStealthToPage(page) {
|
|
|
722
770
|
function observeContext(context) {
|
|
723
771
|
if (observedContexts.has(context)) return;
|
|
724
772
|
observedContexts.add(context);
|
|
773
|
+
ensureContextState(context);
|
|
725
774
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
726
775
|
if (process.env.DEBUG) console.warn("[browserclaw] stealth initScript failed:", e.message);
|
|
727
776
|
});
|
|
@@ -817,6 +866,35 @@ async function disconnectBrowser() {
|
|
|
817
866
|
if (cur) await cur.browser.close().catch(() => {
|
|
818
867
|
});
|
|
819
868
|
}
|
|
869
|
+
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
870
|
+
const normalized = normalizeCdpUrl(opts.cdpUrl);
|
|
871
|
+
const cur = cached;
|
|
872
|
+
if (!cur || cur.cdpUrl !== normalized) return;
|
|
873
|
+
cached = null;
|
|
874
|
+
connectingByUrl.delete(normalized);
|
|
875
|
+
const targetId = opts.targetId?.trim() || "";
|
|
876
|
+
if (targetId) {
|
|
877
|
+
try {
|
|
878
|
+
const pages = await getAllPages(cur.browser);
|
|
879
|
+
for (const page of pages) {
|
|
880
|
+
const tid = await pageTargetId(page).catch(() => null);
|
|
881
|
+
if (tid === targetId) {
|
|
882
|
+
const session = await page.context().newCDPSession(page);
|
|
883
|
+
try {
|
|
884
|
+
await session.send("Runtime.terminateExecution");
|
|
885
|
+
} finally {
|
|
886
|
+
await session.detach().catch(() => {
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
cur.browser.close().catch(() => {
|
|
896
|
+
});
|
|
897
|
+
}
|
|
820
898
|
async function getAllPages(browser) {
|
|
821
899
|
return browser.contexts().flatMap((c) => c.pages());
|
|
822
900
|
}
|
|
@@ -1338,6 +1416,11 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
1338
1416
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1339
1417
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1340
1418
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1419
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
1420
|
+
"localhost",
|
|
1421
|
+
"localhost.localdomain",
|
|
1422
|
+
"metadata.google.internal"
|
|
1423
|
+
]);
|
|
1341
1424
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
1342
1425
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
1343
1426
|
}
|
|
@@ -1351,6 +1434,218 @@ function hasProxyEnvConfigured(env = process.env) {
|
|
|
1351
1434
|
}
|
|
1352
1435
|
return false;
|
|
1353
1436
|
}
|
|
1437
|
+
function normalizeHostname(hostname) {
|
|
1438
|
+
let h = String(hostname ?? "").trim().toLowerCase();
|
|
1439
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
1440
|
+
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
1441
|
+
return h;
|
|
1442
|
+
}
|
|
1443
|
+
function isBlockedHostnameNormalized(normalized) {
|
|
1444
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) return true;
|
|
1445
|
+
return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal");
|
|
1446
|
+
}
|
|
1447
|
+
function isStrictDecimalOctet(part) {
|
|
1448
|
+
if (!/^[0-9]+$/.test(part)) return false;
|
|
1449
|
+
const n = parseInt(part, 10);
|
|
1450
|
+
if (n < 0 || n > 255) return false;
|
|
1451
|
+
if (String(n) !== part) return false;
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
function isUnsupportedIPv4Literal(ip) {
|
|
1455
|
+
if (/^[0-9]+$/.test(ip)) return true;
|
|
1456
|
+
const parts = ip.split(".");
|
|
1457
|
+
if (parts.length !== 4) return true;
|
|
1458
|
+
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
1462
|
+
"unspecified",
|
|
1463
|
+
"broadcast",
|
|
1464
|
+
"multicast",
|
|
1465
|
+
"linkLocal",
|
|
1466
|
+
"loopback",
|
|
1467
|
+
"carrierGradeNat",
|
|
1468
|
+
"private",
|
|
1469
|
+
"reserved"
|
|
1470
|
+
]);
|
|
1471
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
1472
|
+
"unspecified",
|
|
1473
|
+
"loopback",
|
|
1474
|
+
"linkLocal",
|
|
1475
|
+
"uniqueLocal",
|
|
1476
|
+
"multicast"
|
|
1477
|
+
]);
|
|
1478
|
+
var RFC2544_BENCHMARK_PREFIX = [ipaddr__namespace.IPv4.parse("198.18.0.0"), 15];
|
|
1479
|
+
function isBlockedSpecialUseIpv4Address(address, opts) {
|
|
1480
|
+
const inRfc2544 = address.match(RFC2544_BENCHMARK_PREFIX);
|
|
1481
|
+
if (inRfc2544 && opts?.allowRfc2544BenchmarkRange === true) return false;
|
|
1482
|
+
return BLOCKED_IPV4_RANGES.has(address.range()) || inRfc2544;
|
|
1483
|
+
}
|
|
1484
|
+
function isBlockedSpecialUseIpv6Address(address) {
|
|
1485
|
+
if (BLOCKED_IPV6_RANGES.has(address.range())) return true;
|
|
1486
|
+
return (address.parts[0] & 65472) === 65216;
|
|
1487
|
+
}
|
|
1488
|
+
function extractEmbeddedIpv4FromIpv6(v6, opts) {
|
|
1489
|
+
if (v6.isIPv4MappedAddress()) {
|
|
1490
|
+
return isBlockedSpecialUseIpv4Address(v6.toIPv4Address(), opts);
|
|
1491
|
+
}
|
|
1492
|
+
const parts = v6.parts;
|
|
1493
|
+
if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1494
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1495
|
+
try {
|
|
1496
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1497
|
+
} catch {
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 1) {
|
|
1502
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1503
|
+
try {
|
|
1504
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1505
|
+
} catch {
|
|
1506
|
+
return true;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (parts[0] === 8194) {
|
|
1510
|
+
const ip4str = `${parts[1] >> 8 & 255}.${parts[1] & 255}.${parts[2] >> 8 & 255}.${parts[2] & 255}`;
|
|
1511
|
+
try {
|
|
1512
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1513
|
+
} catch {
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (parts[0] === 8193 && parts[1] === 0) {
|
|
1518
|
+
const hiXored = parts[6] ^ 65535;
|
|
1519
|
+
const loXored = parts[7] ^ 65535;
|
|
1520
|
+
const ip4str = `${hiXored >> 8 & 255}.${hiXored & 255}.${loXored >> 8 & 255}.${loXored & 255}`;
|
|
1521
|
+
try {
|
|
1522
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1523
|
+
} catch {
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
function isPrivateIpAddress(address, opts) {
|
|
1530
|
+
let normalized = address.trim().toLowerCase();
|
|
1531
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
|
|
1532
|
+
if (!normalized) return false;
|
|
1533
|
+
try {
|
|
1534
|
+
const parsed = ipaddr__namespace.parse(normalized);
|
|
1535
|
+
if (parsed.kind() === "ipv4") {
|
|
1536
|
+
return isBlockedSpecialUseIpv4Address(parsed, opts);
|
|
1537
|
+
}
|
|
1538
|
+
const v6 = parsed;
|
|
1539
|
+
if (isBlockedSpecialUseIpv6Address(v6)) return true;
|
|
1540
|
+
const embeddedV4 = extractEmbeddedIpv4FromIpv6(v6, opts);
|
|
1541
|
+
if (embeddedV4 !== null) return embeddedV4;
|
|
1542
|
+
return false;
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
if (!normalized.includes(":") && isUnsupportedIPv4Literal(normalized)) return true;
|
|
1546
|
+
if (normalized.includes(":")) return true;
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
function dedupeAndPreferIpv4(results) {
|
|
1550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1551
|
+
const ipv4 = [];
|
|
1552
|
+
const ipv6 = [];
|
|
1553
|
+
for (const r of results) {
|
|
1554
|
+
if (seen.has(r.address)) continue;
|
|
1555
|
+
seen.add(r.address);
|
|
1556
|
+
if (r.family === 4) ipv4.push(r.address);
|
|
1557
|
+
else ipv6.push(r.address);
|
|
1558
|
+
}
|
|
1559
|
+
return [...ipv4, ...ipv6];
|
|
1560
|
+
}
|
|
1561
|
+
function createPinnedLookup(params) {
|
|
1562
|
+
const normalizedHost = normalizeHostname(params.hostname);
|
|
1563
|
+
const fallback = params.fallback ?? dns.lookup;
|
|
1564
|
+
const records = params.addresses.map((address) => ({
|
|
1565
|
+
address,
|
|
1566
|
+
family: address.includes(":") ? 6 : 4
|
|
1567
|
+
}));
|
|
1568
|
+
let index = 0;
|
|
1569
|
+
return ((host, options, callback) => {
|
|
1570
|
+
const cb = typeof options === "function" ? options : callback;
|
|
1571
|
+
if (!cb) return;
|
|
1572
|
+
const normalized = normalizeHostname(host);
|
|
1573
|
+
if (!normalized || normalized !== normalizedHost) {
|
|
1574
|
+
if (typeof options === "function" || options === void 0) return fallback(host, cb);
|
|
1575
|
+
return fallback(host, options, cb);
|
|
1576
|
+
}
|
|
1577
|
+
const opts = typeof options === "object" && options !== null ? options : {};
|
|
1578
|
+
const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0;
|
|
1579
|
+
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
1580
|
+
const usable = candidates.length > 0 ? candidates : records;
|
|
1581
|
+
if (opts.all) {
|
|
1582
|
+
cb(null, usable);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const chosen = usable[index % usable.length];
|
|
1586
|
+
index += 1;
|
|
1587
|
+
cb(null, chosen.address, chosen.family);
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
1591
|
+
const normalized = normalizeHostname(hostname);
|
|
1592
|
+
if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
|
|
1593
|
+
const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
|
|
1594
|
+
const allowedHostnames = [
|
|
1595
|
+
...params.policy?.allowedHostnames ?? [],
|
|
1596
|
+
...params.policy?.hostnameAllowlist ?? []
|
|
1597
|
+
].map((h) => normalizeHostname(h));
|
|
1598
|
+
const isExplicitlyAllowed = allowedHostnames.some((h) => h === normalized);
|
|
1599
|
+
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
1600
|
+
if (!skipPrivateNetworkChecks) {
|
|
1601
|
+
if (isBlockedHostnameNormalized(normalized)) {
|
|
1602
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1603
|
+
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1607
|
+
if (isPrivateIpAddress(normalized, ipOpts)) {
|
|
1608
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1609
|
+
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const lookupFn = params.lookupFn ?? promises.lookup;
|
|
1614
|
+
let results;
|
|
1615
|
+
try {
|
|
1616
|
+
results = await lookupFn(normalized, { all: true });
|
|
1617
|
+
} catch {
|
|
1618
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1619
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
if (!results || results.length === 0) {
|
|
1623
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1624
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
if (!skipPrivateNetworkChecks) {
|
|
1628
|
+
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1629
|
+
for (const r of results) {
|
|
1630
|
+
if (isPrivateIpAddress(r.address, ipOpts)) {
|
|
1631
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1632
|
+
`Navigation to internal/loopback address blocked: "${hostname}" resolves to "${r.address}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const addresses = dedupeAndPreferIpv4(results);
|
|
1638
|
+
if (addresses.length === 0) {
|
|
1639
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1640
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
hostname: normalized,
|
|
1645
|
+
addresses,
|
|
1646
|
+
lookup: createPinnedLookup({ hostname: normalized, addresses })
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1354
1649
|
async function assertBrowserNavigationAllowed(opts) {
|
|
1355
1650
|
const rawUrl = String(opts.url ?? "").trim();
|
|
1356
1651
|
let parsed;
|
|
@@ -1370,20 +1665,10 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1370
1665
|
}
|
|
1371
1666
|
const policy = opts.ssrfPolicy;
|
|
1372
1667
|
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if (allowedHostnames.length) {
|
|
1378
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1379
|
-
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1380
|
-
}
|
|
1381
|
-
const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
|
|
1382
|
-
if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
|
|
1383
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1384
|
-
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1668
|
+
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
1669
|
+
lookupFn: opts.lookupFn,
|
|
1670
|
+
policy
|
|
1671
|
+
});
|
|
1387
1672
|
}
|
|
1388
1673
|
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
1389
1674
|
if (!path2 || typeof path2 !== "string") {
|
|
@@ -1426,111 +1711,6 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
1426
1711
|
}
|
|
1427
1712
|
}
|
|
1428
1713
|
}
|
|
1429
|
-
function expandIPv6(ip) {
|
|
1430
|
-
let normalized = ip;
|
|
1431
|
-
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1432
|
-
if (v4Match) {
|
|
1433
|
-
const octets = v4Match[2].split(".").map(Number);
|
|
1434
|
-
if (octets.some((o) => o > 255)) return null;
|
|
1435
|
-
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1436
|
-
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1437
|
-
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1438
|
-
}
|
|
1439
|
-
const halves = normalized.split("::");
|
|
1440
|
-
if (halves.length > 2) return null;
|
|
1441
|
-
if (halves.length === 2) {
|
|
1442
|
-
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1443
|
-
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1444
|
-
const needed = 8 - left.length - right.length;
|
|
1445
|
-
if (needed < 0) return null;
|
|
1446
|
-
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1447
|
-
if (groups2.length !== 8) return null;
|
|
1448
|
-
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1449
|
-
}
|
|
1450
|
-
const groups = normalized.split(":");
|
|
1451
|
-
if (groups.length !== 8) return null;
|
|
1452
|
-
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1453
|
-
}
|
|
1454
|
-
function hexToIPv4(hiHex, loHex) {
|
|
1455
|
-
const hi = parseInt(hiHex, 16);
|
|
1456
|
-
const lo = parseInt(loHex, 16);
|
|
1457
|
-
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1458
|
-
}
|
|
1459
|
-
function extractEmbeddedIPv4(lower) {
|
|
1460
|
-
if (lower.startsWith("::ffff:")) {
|
|
1461
|
-
return lower.slice(7);
|
|
1462
|
-
}
|
|
1463
|
-
const expanded = expandIPv6(lower);
|
|
1464
|
-
if (expanded === null) return "";
|
|
1465
|
-
const groups = expanded.split(":");
|
|
1466
|
-
if (groups.length !== 8) return "";
|
|
1467
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1468
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1469
|
-
}
|
|
1470
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1471
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1472
|
-
}
|
|
1473
|
-
if (groups[0] === "2002") {
|
|
1474
|
-
return hexToIPv4(groups[1], groups[2]);
|
|
1475
|
-
}
|
|
1476
|
-
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1477
|
-
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1478
|
-
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1479
|
-
return hexToIPv4(hiXored, loXored);
|
|
1480
|
-
}
|
|
1481
|
-
return null;
|
|
1482
|
-
}
|
|
1483
|
-
function isStrictDecimalOctet(part) {
|
|
1484
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1485
|
-
const n = parseInt(part, 10);
|
|
1486
|
-
if (n < 0 || n > 255) return false;
|
|
1487
|
-
if (String(n) !== part) return false;
|
|
1488
|
-
return true;
|
|
1489
|
-
}
|
|
1490
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1491
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1492
|
-
const parts = ip.split(".");
|
|
1493
|
-
if (parts.length !== 4) return true;
|
|
1494
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1495
|
-
return false;
|
|
1496
|
-
}
|
|
1497
|
-
function isInternalIP(ip, opts) {
|
|
1498
|
-
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1499
|
-
if (/^127\./.test(ip)) return true;
|
|
1500
|
-
if (/^10\./.test(ip)) return true;
|
|
1501
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1502
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
1503
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
1504
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1505
|
-
if (ip === "0.0.0.0") return true;
|
|
1506
|
-
if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
|
|
1507
|
-
const lower = ip.toLowerCase();
|
|
1508
|
-
if (lower === "::1") return true;
|
|
1509
|
-
if (lower.startsWith("fe80:")) return true;
|
|
1510
|
-
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1511
|
-
if (lower.startsWith("ff")) return true;
|
|
1512
|
-
const embedded = extractEmbeddedIPv4(lower);
|
|
1513
|
-
if (embedded !== null) {
|
|
1514
|
-
if (embedded === "") return true;
|
|
1515
|
-
return isInternalIP(embedded, opts);
|
|
1516
|
-
}
|
|
1517
|
-
return false;
|
|
1518
|
-
}
|
|
1519
|
-
function isInternalUrl(url, opts) {
|
|
1520
|
-
let parsed;
|
|
1521
|
-
try {
|
|
1522
|
-
parsed = new URL(url);
|
|
1523
|
-
} catch {
|
|
1524
|
-
return true;
|
|
1525
|
-
}
|
|
1526
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1527
|
-
if (hostname === "localhost") return true;
|
|
1528
|
-
if (isInternalIP(hostname, opts)) return true;
|
|
1529
|
-
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1530
|
-
return true;
|
|
1531
|
-
}
|
|
1532
|
-
return false;
|
|
1533
|
-
}
|
|
1534
1714
|
async function assertSafeUploadPaths(paths) {
|
|
1535
1715
|
for (const filePath of paths) {
|
|
1536
1716
|
let stat;
|
|
@@ -1547,21 +1727,48 @@ async function assertSafeUploadPaths(paths) {
|
|
|
1547
1727
|
}
|
|
1548
1728
|
}
|
|
1549
1729
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1730
|
+
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
1731
|
+
const trimmed = String(fileName ?? "").trim();
|
|
1732
|
+
if (!trimmed) return fallbackName;
|
|
1733
|
+
let base = path.posix.basename(trimmed);
|
|
1734
|
+
base = path.win32.basename(base);
|
|
1735
|
+
let cleaned = "";
|
|
1736
|
+
for (let i = 0; i < base.length; i++) {
|
|
1737
|
+
const code = base.charCodeAt(i);
|
|
1738
|
+
if (code < 32 || code === 127) continue;
|
|
1739
|
+
cleaned += base[i];
|
|
1740
|
+
}
|
|
1741
|
+
base = cleaned.trim();
|
|
1742
|
+
if (!base || base === "." || base === "..") return fallbackName;
|
|
1743
|
+
if (base.length > 200) base = base.slice(0, 200);
|
|
1744
|
+
return base;
|
|
1745
|
+
}
|
|
1746
|
+
function buildSiblingTempPath(targetPath) {
|
|
1747
|
+
const id = crypto.randomUUID();
|
|
1748
|
+
const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), "output.bin");
|
|
1749
|
+
return path.join(path.dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
|
|
1750
|
+
}
|
|
1751
|
+
async function writeViaSiblingTempPath(params) {
|
|
1752
|
+
const rootDir = await promises$1.realpath(path.resolve(params.rootDir)).catch(() => path.resolve(params.rootDir));
|
|
1753
|
+
const requestedTargetPath = path.resolve(params.targetPath);
|
|
1754
|
+
const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
|
|
1755
|
+
const relativeTargetPath = path.relative(rootDir, targetPath);
|
|
1756
|
+
if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || isAbsolute(relativeTargetPath)) {
|
|
1757
|
+
throw new Error("Target path is outside the allowed root");
|
|
1758
|
+
}
|
|
1759
|
+
const tempPath = buildSiblingTempPath(targetPath);
|
|
1760
|
+
let renameSucceeded = false;
|
|
1558
1761
|
try {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1762
|
+
await params.writeTemp(tempPath);
|
|
1763
|
+
await promises$1.rename(tempPath, targetPath);
|
|
1764
|
+
renameSucceeded = true;
|
|
1765
|
+
} finally {
|
|
1766
|
+
if (!renameSucceeded) await promises$1.rm(tempPath, { force: true }).catch(() => {
|
|
1767
|
+
});
|
|
1563
1768
|
}
|
|
1564
|
-
|
|
1769
|
+
}
|
|
1770
|
+
function isAbsolute(p) {
|
|
1771
|
+
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
1565
1772
|
}
|
|
1566
1773
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
1567
1774
|
const rawUrl = String(opts.url ?? "").trim();
|
|
@@ -1669,13 +1876,12 @@ async function fillFormViaPlaywright(opts) {
|
|
|
1669
1876
|
ensurePageState(page);
|
|
1670
1877
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1671
1878
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
1672
|
-
for (
|
|
1673
|
-
const field = opts.fields[i];
|
|
1879
|
+
for (const field of opts.fields) {
|
|
1674
1880
|
const ref = field.ref.trim();
|
|
1675
1881
|
const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
|
|
1676
1882
|
const rawValue = field.value;
|
|
1677
|
-
const value = rawValue
|
|
1678
|
-
if (!ref)
|
|
1883
|
+
const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
|
|
1884
|
+
if (!ref) continue;
|
|
1679
1885
|
const locator = refLocator(page, ref);
|
|
1680
1886
|
if (type === "checkbox" || type === "radio") {
|
|
1681
1887
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
@@ -1719,61 +1925,78 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
1719
1925
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1720
1926
|
ensurePageState(page);
|
|
1721
1927
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1722
|
-
|
|
1723
|
-
|
|
1928
|
+
if (!opts.paths.length) throw new Error("paths are required");
|
|
1929
|
+
const inputRef = typeof opts.ref === "string" ? opts.ref.trim() : "";
|
|
1930
|
+
const element = typeof opts.element === "string" ? opts.element.trim() : "";
|
|
1931
|
+
if (inputRef && element) throw new Error("ref and element are mutually exclusive");
|
|
1932
|
+
if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
|
|
1933
|
+
const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
|
|
1724
1934
|
await assertSafeUploadPaths(opts.paths);
|
|
1725
1935
|
try {
|
|
1726
1936
|
await locator.setInputFiles(opts.paths);
|
|
1727
1937
|
} catch (err) {
|
|
1728
|
-
throw toAIFriendlyError(err,
|
|
1938
|
+
throw toAIFriendlyError(err, inputRef || element);
|
|
1939
|
+
}
|
|
1940
|
+
try {
|
|
1941
|
+
const handle = await locator.elementHandle();
|
|
1942
|
+
if (handle) {
|
|
1943
|
+
await handle.evaluate((el) => {
|
|
1944
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1945
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
} catch {
|
|
1729
1949
|
}
|
|
1730
1950
|
}
|
|
1731
1951
|
async function armDialogViaPlaywright(opts) {
|
|
1732
1952
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1733
|
-
ensurePageState(page);
|
|
1734
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
try {
|
|
1743
|
-
if (opts.accept) {
|
|
1744
|
-
await dialog.accept(opts.promptText);
|
|
1745
|
-
} else {
|
|
1746
|
-
await dialog.dismiss();
|
|
1747
|
-
}
|
|
1748
|
-
resolve2();
|
|
1749
|
-
} catch (err) {
|
|
1750
|
-
reject(err);
|
|
1751
|
-
}
|
|
1752
|
-
};
|
|
1753
|
-
page.once("dialog", handler);
|
|
1953
|
+
const state = ensurePageState(page);
|
|
1954
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1955
|
+
state.armIdDialog = bumpDialogArmId();
|
|
1956
|
+
const armId = state.armIdDialog;
|
|
1957
|
+
page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
|
|
1958
|
+
if (state.armIdDialog !== armId) return;
|
|
1959
|
+
if (opts.accept) await dialog.accept(opts.promptText);
|
|
1960
|
+
else await dialog.dismiss();
|
|
1961
|
+
}).catch(() => {
|
|
1754
1962
|
});
|
|
1755
1963
|
}
|
|
1756
1964
|
async function armFileUploadViaPlaywright(opts) {
|
|
1757
1965
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1758
|
-
ensurePageState(page);
|
|
1759
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const handler = async (fc) => {
|
|
1766
|
-
clearTimeout(timer);
|
|
1966
|
+
const state = ensurePageState(page);
|
|
1967
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1968
|
+
state.armIdUpload = bumpUploadArmId();
|
|
1969
|
+
const armId = state.armIdUpload;
|
|
1970
|
+
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
1971
|
+
if (state.armIdUpload !== armId) return;
|
|
1972
|
+
if (!opts.paths?.length) {
|
|
1767
1973
|
try {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
await fc.setFiles(paths);
|
|
1771
|
-
resolve2();
|
|
1772
|
-
} catch (err) {
|
|
1773
|
-
reject(err);
|
|
1974
|
+
await page.keyboard.press("Escape");
|
|
1975
|
+
} catch {
|
|
1774
1976
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
await assertSafeUploadPaths(opts.paths);
|
|
1981
|
+
} catch {
|
|
1982
|
+
try {
|
|
1983
|
+
await page.keyboard.press("Escape");
|
|
1984
|
+
} catch {
|
|
1985
|
+
}
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
await fileChooser.setFiles(opts.paths);
|
|
1989
|
+
try {
|
|
1990
|
+
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
1991
|
+
if (input) {
|
|
1992
|
+
await input.evaluate((el) => {
|
|
1993
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1994
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
} catch {
|
|
1998
|
+
}
|
|
1999
|
+
}).catch(() => {
|
|
1777
2000
|
});
|
|
1778
2001
|
}
|
|
1779
2002
|
|
|
@@ -1787,17 +2010,35 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1787
2010
|
}
|
|
1788
2011
|
|
|
1789
2012
|
// src/actions/navigation.ts
|
|
2013
|
+
function isRetryableNavigateError(err) {
|
|
2014
|
+
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
2015
|
+
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
2016
|
+
}
|
|
1790
2017
|
async function navigateViaPlaywright(opts) {
|
|
1791
2018
|
const url = String(opts.url ?? "").trim();
|
|
1792
2019
|
if (!url) throw new Error("url is required");
|
|
1793
2020
|
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
1794
|
-
await assertBrowserNavigationAllowed({ url,
|
|
1795
|
-
const
|
|
2021
|
+
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
2022
|
+
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
2023
|
+
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1796
2024
|
ensurePageState(page);
|
|
1797
|
-
const
|
|
2025
|
+
const navigate = async () => await page.goto(url, { timeout });
|
|
2026
|
+
let response;
|
|
2027
|
+
try {
|
|
2028
|
+
response = await navigate();
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
if (!isRetryableNavigateError(err)) throw err;
|
|
2031
|
+
await forceDisconnectPlaywrightForTarget({
|
|
2032
|
+
cdpUrl: opts.cdpUrl,
|
|
2033
|
+
targetId: opts.targetId}).catch(() => {
|
|
2034
|
+
});
|
|
2035
|
+
page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2036
|
+
ensurePageState(page);
|
|
2037
|
+
response = await navigate();
|
|
2038
|
+
}
|
|
1798
2039
|
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...withBrowserNavigationPolicy(policy) });
|
|
1799
2040
|
const finalUrl = page.url();
|
|
1800
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl,
|
|
2041
|
+
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
1801
2042
|
return { url: finalUrl };
|
|
1802
2043
|
}
|
|
1803
2044
|
async function listPagesViaPlaywright(opts) {
|
|
@@ -1823,11 +2064,12 @@ async function createPageViaPlaywright(opts) {
|
|
|
1823
2064
|
}
|
|
1824
2065
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1825
2066
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
2067
|
+
ensureContextState(context);
|
|
1826
2068
|
const page = await context.newPage();
|
|
1827
2069
|
ensurePageState(page);
|
|
1828
2070
|
if (targetUrl !== "about:blank") {
|
|
1829
2071
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
1830
|
-
const response = await page.goto(targetUrl, { timeout:
|
|
2072
|
+
const response = await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null);
|
|
1831
2073
|
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
|
|
1832
2074
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
|
|
1833
2075
|
}
|
|
@@ -1936,68 +2178,136 @@ async function evaluateInAllFramesViaPlaywright(opts) {
|
|
|
1936
2178
|
}
|
|
1937
2179
|
return results;
|
|
1938
2180
|
}
|
|
2181
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2182
|
+
if (!abortPromise) return await evalPromise;
|
|
2183
|
+
try {
|
|
2184
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2185
|
+
} catch (err) {
|
|
2186
|
+
evalPromise.catch(() => {
|
|
2187
|
+
});
|
|
2188
|
+
throw err;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
var BROWSER_EVALUATOR = new Function("args", `
|
|
2192
|
+
"use strict";
|
|
2193
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2194
|
+
try {
|
|
2195
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2196
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2197
|
+
if (result && typeof result.then === "function") {
|
|
2198
|
+
return Promise.race([
|
|
2199
|
+
result,
|
|
2200
|
+
new Promise(function(_, reject) {
|
|
2201
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2202
|
+
})
|
|
2203
|
+
]);
|
|
2204
|
+
}
|
|
2205
|
+
return result;
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2208
|
+
}
|
|
2209
|
+
`);
|
|
2210
|
+
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
2211
|
+
"use strict";
|
|
2212
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2213
|
+
try {
|
|
2214
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2215
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2216
|
+
if (result && typeof result.then === "function") {
|
|
2217
|
+
return Promise.race([
|
|
2218
|
+
result,
|
|
2219
|
+
new Promise(function(_, reject) {
|
|
2220
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2221
|
+
})
|
|
2222
|
+
]);
|
|
2223
|
+
}
|
|
2224
|
+
return result;
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2227
|
+
}
|
|
2228
|
+
`);
|
|
1939
2229
|
async function evaluateViaPlaywright(opts) {
|
|
1940
2230
|
const fnText = String(opts.fn ?? "").trim();
|
|
1941
2231
|
if (!fnText) throw new Error("function is required");
|
|
1942
2232
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1943
2233
|
ensurePageState(page);
|
|
1944
2234
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2235
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2236
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2237
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2238
|
+
const signal = opts.signal;
|
|
2239
|
+
let abortListener;
|
|
2240
|
+
let abortReject;
|
|
2241
|
+
let abortPromise;
|
|
2242
|
+
if (signal) {
|
|
2243
|
+
abortPromise = new Promise((_, reject) => {
|
|
2244
|
+
abortReject = reject;
|
|
2245
|
+
});
|
|
2246
|
+
abortPromise.catch(() => {
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
if (signal) {
|
|
2250
|
+
const disconnect = () => {
|
|
2251
|
+
forceDisconnectPlaywrightForTarget({
|
|
2252
|
+
cdpUrl: opts.cdpUrl,
|
|
2253
|
+
targetId: opts.targetId}).catch(() => {
|
|
2254
|
+
});
|
|
2255
|
+
};
|
|
2256
|
+
if (signal.aborted) {
|
|
2257
|
+
disconnect();
|
|
2258
|
+
throw signal.reason ?? new Error("aborted");
|
|
2259
|
+
}
|
|
2260
|
+
abortListener = () => {
|
|
2261
|
+
disconnect();
|
|
2262
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2263
|
+
};
|
|
2264
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2265
|
+
if (signal.aborted) {
|
|
2266
|
+
abortListener();
|
|
2267
|
+
throw signal.reason ?? new Error("aborted");
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
if (opts.ref) {
|
|
2272
|
+
const locator = refLocator(page, opts.ref);
|
|
2273
|
+
return await awaitEvalWithAbort(
|
|
2274
|
+
locator.evaluate(ELEMENT_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
2275
|
+
abortPromise
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
return await awaitEvalWithAbort(
|
|
2279
|
+
page.evaluate(BROWSER_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
2280
|
+
abortPromise
|
|
1960
2281
|
);
|
|
2282
|
+
} finally {
|
|
2283
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
1961
2284
|
}
|
|
1962
|
-
const evalPromise = page.evaluate(
|
|
1963
|
-
// eslint-disable-next-line no-eval
|
|
1964
|
-
(fnBody) => {
|
|
1965
|
-
try {
|
|
1966
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
1967
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
1968
|
-
} catch (err) {
|
|
1969
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
1970
|
-
}
|
|
1971
|
-
},
|
|
1972
|
-
fnText
|
|
1973
|
-
);
|
|
1974
|
-
if (!opts.signal) return evalPromise;
|
|
1975
|
-
return Promise.race([
|
|
1976
|
-
evalPromise,
|
|
1977
|
-
new Promise((_, reject) => {
|
|
1978
|
-
opts.signal.addEventListener("abort", () => reject(new Error("Evaluate aborted")), { once: true });
|
|
1979
|
-
})
|
|
1980
|
-
]);
|
|
1981
2285
|
}
|
|
1982
|
-
|
|
1983
|
-
// src/actions/download.ts
|
|
1984
2286
|
async function downloadViaPlaywright(opts) {
|
|
1985
2287
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1986
2288
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1987
|
-
ensurePageState(page);
|
|
2289
|
+
const state = ensurePageState(page);
|
|
1988
2290
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1989
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
2291
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1990
2292
|
const locator = refLocator(page, opts.ref);
|
|
2293
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
1991
2294
|
try {
|
|
1992
2295
|
const [download] = await Promise.all([
|
|
1993
2296
|
page.waitForEvent("download", { timeout }),
|
|
1994
2297
|
locator.click({ timeout })
|
|
1995
2298
|
]);
|
|
1996
|
-
|
|
2299
|
+
const outPath = opts.path;
|
|
2300
|
+
await writeViaSiblingTempPath({
|
|
2301
|
+
rootDir: path.dirname(outPath),
|
|
2302
|
+
targetPath: outPath,
|
|
2303
|
+
writeTemp: async (tempPath) => {
|
|
2304
|
+
await download.saveAs(tempPath);
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
1997
2307
|
return {
|
|
1998
2308
|
url: download.url(),
|
|
1999
2309
|
suggestedFilename: download.suggestedFilename(),
|
|
2000
|
-
path:
|
|
2310
|
+
path: outPath
|
|
2001
2311
|
};
|
|
2002
2312
|
} catch (err) {
|
|
2003
2313
|
throw toAIFriendlyError(err, opts.ref);
|
|
@@ -2006,11 +2316,17 @@ async function downloadViaPlaywright(opts) {
|
|
|
2006
2316
|
async function waitForDownloadViaPlaywright(opts) {
|
|
2007
2317
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2008
2318
|
ensurePageState(page);
|
|
2009
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
2319
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2010
2320
|
const download = await page.waitForEvent("download", { timeout });
|
|
2011
2321
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
2012
2322
|
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
2013
|
-
await
|
|
2323
|
+
await writeViaSiblingTempPath({
|
|
2324
|
+
rootDir: path.dirname(savePath),
|
|
2325
|
+
targetPath: savePath,
|
|
2326
|
+
writeTemp: async (tempPath) => {
|
|
2327
|
+
await download.saveAs(tempPath);
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2014
2330
|
return {
|
|
2015
2331
|
url: download.url(),
|
|
2016
2332
|
suggestedFilename: download.suggestedFilename(),
|
|
@@ -2023,33 +2339,51 @@ async function emulateMediaViaPlaywright(opts) {
|
|
|
2023
2339
|
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
2024
2340
|
}
|
|
2025
2341
|
async function setDeviceViaPlaywright(opts) {
|
|
2026
|
-
const
|
|
2342
|
+
const name = String(opts.name ?? "").trim();
|
|
2343
|
+
if (!name) throw new Error("device name is required");
|
|
2344
|
+
const device = playwrightCore.devices[name];
|
|
2027
2345
|
if (!device) {
|
|
2028
|
-
|
|
2029
|
-
throw new Error(`Unknown device "${opts.name}". Some available devices: ${available}...`);
|
|
2346
|
+
throw new Error(`Unknown device "${name}".`);
|
|
2030
2347
|
}
|
|
2031
2348
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2032
2349
|
ensurePageState(page);
|
|
2033
2350
|
if (device.viewport) {
|
|
2034
|
-
await page.setViewportSize(
|
|
2351
|
+
await page.setViewportSize({
|
|
2352
|
+
width: device.viewport.width,
|
|
2353
|
+
height: device.viewport.height
|
|
2354
|
+
});
|
|
2035
2355
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2356
|
+
const session = await page.context().newCDPSession(page);
|
|
2357
|
+
try {
|
|
2358
|
+
const locale = device.locale;
|
|
2359
|
+
if (device.userAgent || locale) {
|
|
2040
2360
|
await session.send("Emulation.setUserAgentOverride", {
|
|
2041
|
-
userAgent: device.userAgent
|
|
2361
|
+
userAgent: device.userAgent ?? "",
|
|
2362
|
+
acceptLanguage: locale ?? void 0
|
|
2042
2363
|
});
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2364
|
+
}
|
|
2365
|
+
if (device.viewport) {
|
|
2366
|
+
await session.send("Emulation.setDeviceMetricsOverride", {
|
|
2367
|
+
mobile: Boolean(device.isMobile),
|
|
2368
|
+
width: device.viewport.width,
|
|
2369
|
+
height: device.viewport.height,
|
|
2370
|
+
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
2371
|
+
screenWidth: device.viewport.width,
|
|
2372
|
+
screenHeight: device.viewport.height
|
|
2045
2373
|
});
|
|
2046
2374
|
}
|
|
2375
|
+
if (device.hasTouch) {
|
|
2376
|
+
await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
2377
|
+
}
|
|
2378
|
+
} finally {
|
|
2379
|
+
await session.detach().catch(() => {
|
|
2380
|
+
});
|
|
2047
2381
|
}
|
|
2048
2382
|
}
|
|
2049
2383
|
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
2050
2384
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2051
2385
|
ensurePageState(page);
|
|
2052
|
-
await page.setExtraHTTPHeaders(opts.headers);
|
|
2386
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
2053
2387
|
}
|
|
2054
2388
|
async function setGeolocationViaPlaywright(opts) {
|
|
2055
2389
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -2057,38 +2391,53 @@ async function setGeolocationViaPlaywright(opts) {
|
|
|
2057
2391
|
const context = page.context();
|
|
2058
2392
|
if (opts.clear) {
|
|
2059
2393
|
await context.setGeolocation(null);
|
|
2060
|
-
await context.clearPermissions()
|
|
2394
|
+
await context.clearPermissions().catch(() => {
|
|
2395
|
+
});
|
|
2061
2396
|
return;
|
|
2062
2397
|
}
|
|
2063
|
-
if (opts.latitude
|
|
2398
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
2064
2399
|
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
2065
2400
|
}
|
|
2066
|
-
await context.grantPermissions(["geolocation"], opts.origin ? { origin: opts.origin } : void 0);
|
|
2067
2401
|
await context.setGeolocation({
|
|
2068
2402
|
latitude: opts.latitude,
|
|
2069
2403
|
longitude: opts.longitude,
|
|
2070
|
-
accuracy: opts.accuracy
|
|
2404
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
2405
|
+
});
|
|
2406
|
+
const origin = opts.origin?.trim() || (() => {
|
|
2407
|
+
try {
|
|
2408
|
+
return new URL(page.url()).origin;
|
|
2409
|
+
} catch {
|
|
2410
|
+
return "";
|
|
2411
|
+
}
|
|
2412
|
+
})();
|
|
2413
|
+
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
2071
2414
|
});
|
|
2072
2415
|
}
|
|
2073
2416
|
async function setHttpCredentialsViaPlaywright(opts) {
|
|
2074
2417
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2075
2418
|
ensurePageState(page);
|
|
2076
|
-
const context = page.context();
|
|
2077
2419
|
if (opts.clear) {
|
|
2078
|
-
await context.setHTTPCredentials(
|
|
2420
|
+
await page.context().setHTTPCredentials(null);
|
|
2079
2421
|
return;
|
|
2080
2422
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
});
|
|
2423
|
+
const username = String(opts.username ?? "");
|
|
2424
|
+
const password = String(opts.password ?? "");
|
|
2425
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
2426
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
2085
2427
|
}
|
|
2086
2428
|
async function setLocaleViaPlaywright(opts) {
|
|
2087
2429
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2088
2430
|
ensurePageState(page);
|
|
2431
|
+
const locale = String(opts.locale ?? "").trim();
|
|
2432
|
+
if (!locale) throw new Error("locale is required");
|
|
2089
2433
|
const session = await page.context().newCDPSession(page);
|
|
2090
2434
|
try {
|
|
2091
|
-
|
|
2435
|
+
try {
|
|
2436
|
+
await session.send("Emulation.setLocaleOverride", { locale });
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
2439
|
+
throw err;
|
|
2440
|
+
}
|
|
2092
2441
|
} finally {
|
|
2093
2442
|
await session.detach().catch(() => {
|
|
2094
2443
|
});
|
|
@@ -2102,9 +2451,18 @@ async function setOfflineViaPlaywright(opts) {
|
|
|
2102
2451
|
async function setTimezoneViaPlaywright(opts) {
|
|
2103
2452
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2104
2453
|
ensurePageState(page);
|
|
2454
|
+
const timezoneId = String(opts.timezoneId ?? "").trim();
|
|
2455
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
2105
2456
|
const session = await page.context().newCDPSession(page);
|
|
2106
2457
|
try {
|
|
2107
|
-
|
|
2458
|
+
try {
|
|
2459
|
+
await session.send("Emulation.setTimezoneOverride", { timezoneId });
|
|
2460
|
+
} catch (err) {
|
|
2461
|
+
const msg = String(err);
|
|
2462
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
2463
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
2464
|
+
throw err;
|
|
2465
|
+
}
|
|
2108
2466
|
} finally {
|
|
2109
2467
|
await session.detach().catch(() => {
|
|
2110
2468
|
});
|
|
@@ -2180,24 +2538,38 @@ async function pdfViaPlaywright(opts) {
|
|
|
2180
2538
|
ensurePageState(page);
|
|
2181
2539
|
return { buffer: await page.pdf({ printBackground: true }) };
|
|
2182
2540
|
}
|
|
2183
|
-
|
|
2184
|
-
// src/capture/trace.ts
|
|
2185
2541
|
async function traceStartViaPlaywright(opts) {
|
|
2186
2542
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2187
2543
|
ensurePageState(page);
|
|
2188
2544
|
const context = page.context();
|
|
2545
|
+
const ctxState = ensureContextState(context);
|
|
2546
|
+
if (ctxState.traceActive) {
|
|
2547
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
2548
|
+
}
|
|
2189
2549
|
await context.tracing.start({
|
|
2190
2550
|
screenshots: opts.screenshots ?? true,
|
|
2191
2551
|
snapshots: opts.snapshots ?? true,
|
|
2192
|
-
sources: opts.sources
|
|
2552
|
+
sources: opts.sources ?? false
|
|
2193
2553
|
});
|
|
2554
|
+
ctxState.traceActive = true;
|
|
2194
2555
|
}
|
|
2195
2556
|
async function traceStopViaPlaywright(opts) {
|
|
2196
2557
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2197
2558
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2198
2559
|
ensurePageState(page);
|
|
2199
2560
|
const context = page.context();
|
|
2200
|
-
|
|
2561
|
+
const ctxState = ensureContextState(context);
|
|
2562
|
+
if (!ctxState.traceActive) {
|
|
2563
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
2564
|
+
}
|
|
2565
|
+
await writeViaSiblingTempPath({
|
|
2566
|
+
rootDir: path.dirname(opts.path),
|
|
2567
|
+
targetPath: opts.path,
|
|
2568
|
+
writeTemp: async (tempPath) => {
|
|
2569
|
+
await context.tracing.stop({ path: tempPath });
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
ctxState.traceActive = false;
|
|
2201
2573
|
}
|
|
2202
2574
|
|
|
2203
2575
|
// src/capture/response.ts
|
|
@@ -2280,8 +2652,8 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
2280
2652
|
const cookie = opts.cookie;
|
|
2281
2653
|
if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
|
|
2282
2654
|
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
2283
|
-
const
|
|
2284
|
-
if (!hasUrl && !
|
|
2655
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
2656
|
+
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
2285
2657
|
await page.context().addCookies([cookie]);
|
|
2286
2658
|
}
|
|
2287
2659
|
async function cookiesClearViaPlaywright(opts) {
|
|
@@ -3360,11 +3732,17 @@ exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
|
|
|
3360
3732
|
exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
|
|
3361
3733
|
exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
|
|
3362
3734
|
exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
|
|
3735
|
+
exports.createPinnedLookup = createPinnedLookup;
|
|
3736
|
+
exports.ensureContextState = ensureContextState;
|
|
3737
|
+
exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
|
|
3363
3738
|
exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
|
|
3364
3739
|
exports.isChromeCdpReady = isChromeCdpReady;
|
|
3365
3740
|
exports.isChromeReachable = isChromeReachable;
|
|
3366
3741
|
exports.normalizeCdpHttpBaseForJsonEndpoints = normalizeCdpHttpBaseForJsonEndpoints;
|
|
3367
3742
|
exports.requiresInspectableBrowserNavigationRedirects = requiresInspectableBrowserNavigationRedirects;
|
|
3743
|
+
exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
|
|
3744
|
+
exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
|
|
3368
3745
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
3746
|
+
exports.writeViaSiblingTempPath = writeViaSiblingTempPath;
|
|
3369
3747
|
//# sourceMappingURL=index.cjs.map
|
|
3370
3748
|
//# sourceMappingURL=index.cjs.map
|