browserclaw 0.2.2 → 0.2.4
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/README.md +7 -2
- package/dist/index.cjs +171 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -4
- package/dist/index.d.ts +60 -4
- package/dist/index.js +172 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
-
import path from 'path';
|
|
2
|
+
import path, { normalize, resolve, sep } 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';
|
|
7
8
|
|
|
8
9
|
// src/chrome-launcher.ts
|
|
9
10
|
var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
@@ -263,12 +264,12 @@ function resolveBrowserExecutable(opts) {
|
|
|
263
264
|
return null;
|
|
264
265
|
}
|
|
265
266
|
async function ensurePortAvailable(port) {
|
|
266
|
-
await new Promise((
|
|
267
|
+
await new Promise((resolve2, reject) => {
|
|
267
268
|
const tester = net.createServer().once("error", (err) => {
|
|
268
269
|
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
269
270
|
else reject(err);
|
|
270
271
|
}).once("listening", () => {
|
|
271
|
-
tester.close(() =>
|
|
272
|
+
tester.close(() => resolve2());
|
|
272
273
|
}).listen(port);
|
|
273
274
|
});
|
|
274
275
|
}
|
|
@@ -338,11 +339,13 @@ function resolveUserDataDir(profileName) {
|
|
|
338
339
|
const configDir = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
339
340
|
return path.join(configDir, "browserclaw", "profiles", profileName, "user-data");
|
|
340
341
|
}
|
|
341
|
-
async function isChromeReachable(cdpUrl, timeoutMs = 500) {
|
|
342
|
+
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
342
343
|
const ctrl = new AbortController();
|
|
343
344
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
344
345
|
try {
|
|
345
|
-
const
|
|
346
|
+
const headers = {};
|
|
347
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
348
|
+
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
346
349
|
return res.ok;
|
|
347
350
|
} catch {
|
|
348
351
|
return false;
|
|
@@ -350,11 +353,13 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500) {
|
|
|
350
353
|
clearTimeout(t);
|
|
351
354
|
}
|
|
352
355
|
}
|
|
353
|
-
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
|
|
356
|
+
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
354
357
|
const ctrl = new AbortController();
|
|
355
358
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
356
359
|
try {
|
|
357
|
-
const
|
|
360
|
+
const headers = {};
|
|
361
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
362
|
+
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
358
363
|
if (!res.ok) return null;
|
|
359
364
|
const data = await res.json();
|
|
360
365
|
return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
|
|
@@ -382,6 +387,7 @@ async function launchChrome(opts = {}) {
|
|
|
382
387
|
"--disable-background-networking",
|
|
383
388
|
"--disable-component-update",
|
|
384
389
|
"--disable-features=Translate,MediaRouter",
|
|
390
|
+
"--disable-blink-features=AutomationControlled",
|
|
385
391
|
"--disable-session-crashed-bubble",
|
|
386
392
|
"--hide-crash-restore-bubble",
|
|
387
393
|
"--password-store=basic"
|
|
@@ -566,11 +572,26 @@ function ensurePageState(page) {
|
|
|
566
572
|
}
|
|
567
573
|
return state;
|
|
568
574
|
}
|
|
575
|
+
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
576
|
+
function applyStealthToPage(page) {
|
|
577
|
+
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
578
|
+
if (process.env.DEBUG) console.warn("[browserclaw] stealth evaluate failed:", e.message);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
569
581
|
function observeContext(context) {
|
|
570
582
|
if (observedContexts.has(context)) return;
|
|
571
583
|
observedContexts.add(context);
|
|
572
|
-
|
|
573
|
-
|
|
584
|
+
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
585
|
+
if (process.env.DEBUG) console.warn("[browserclaw] stealth initScript failed:", e.message);
|
|
586
|
+
});
|
|
587
|
+
for (const page of context.pages()) {
|
|
588
|
+
ensurePageState(page);
|
|
589
|
+
applyStealthToPage(page);
|
|
590
|
+
}
|
|
591
|
+
context.on("page", (page) => {
|
|
592
|
+
ensurePageState(page);
|
|
593
|
+
applyStealthToPage(page);
|
|
594
|
+
});
|
|
574
595
|
}
|
|
575
596
|
function observeBrowser(browser) {
|
|
576
597
|
for (const context of browser.contexts()) observeContext(context);
|
|
@@ -604,7 +625,7 @@ function restoreRoleRefsForTarget(opts) {
|
|
|
604
625
|
state.roleRefsFrameSelector = entry.frameSelector;
|
|
605
626
|
state.roleRefsMode = entry.mode;
|
|
606
627
|
}
|
|
607
|
-
async function connectBrowser(cdpUrl) {
|
|
628
|
+
async function connectBrowser(cdpUrl, authToken) {
|
|
608
629
|
const normalized = normalizeCdpUrl(cdpUrl);
|
|
609
630
|
if (cached?.cdpUrl === normalized) return cached;
|
|
610
631
|
const existing = connectingByUrl.get(normalized);
|
|
@@ -614,9 +635,11 @@ async function connectBrowser(cdpUrl) {
|
|
|
614
635
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
615
636
|
try {
|
|
616
637
|
const timeout = 5e3 + attempt * 2e3;
|
|
617
|
-
const endpoint = await getChromeWebSocketUrl(normalized, timeout).catch(() => null) ?? normalized;
|
|
618
|
-
const
|
|
619
|
-
|
|
638
|
+
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
639
|
+
const headers = {};
|
|
640
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
641
|
+
const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
|
|
642
|
+
const connected = { browser, cdpUrl: normalized, authToken };
|
|
620
643
|
cached = connected;
|
|
621
644
|
observeBrowser(browser);
|
|
622
645
|
browser.on("disconnected", () => {
|
|
@@ -676,7 +699,9 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
676
699
|
if (cdpUrl) {
|
|
677
700
|
try {
|
|
678
701
|
const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
|
|
679
|
-
const
|
|
702
|
+
const headers = {};
|
|
703
|
+
if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
|
|
704
|
+
const response = await fetch(listUrl, { headers });
|
|
680
705
|
if (response.ok) {
|
|
681
706
|
const targets = await response.json();
|
|
682
707
|
const target = targets.find((t) => t.id === targetId);
|
|
@@ -1001,6 +1026,7 @@ async function snapshotAi(opts) {
|
|
|
1001
1026
|
if (!maybe._snapshotForAI) {
|
|
1002
1027
|
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
1003
1028
|
}
|
|
1029
|
+
const sourceUrl = page.url();
|
|
1004
1030
|
const result = await maybe._snapshotForAI({
|
|
1005
1031
|
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
1006
1032
|
track: "response"
|
|
@@ -1026,7 +1052,13 @@ async function snapshotAi(opts) {
|
|
|
1026
1052
|
return {
|
|
1027
1053
|
snapshot: built.snapshot,
|
|
1028
1054
|
refs: built.refs,
|
|
1029
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs)
|
|
1055
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
1056
|
+
untrusted: true,
|
|
1057
|
+
contentMeta: {
|
|
1058
|
+
sourceUrl,
|
|
1059
|
+
contentType: "browser-snapshot",
|
|
1060
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1061
|
+
}
|
|
1030
1062
|
};
|
|
1031
1063
|
}
|
|
1032
1064
|
|
|
@@ -1034,6 +1066,7 @@ async function snapshotAi(opts) {
|
|
|
1034
1066
|
async function snapshotRole(opts) {
|
|
1035
1067
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1036
1068
|
ensurePageState(page);
|
|
1069
|
+
const sourceUrl = page.url();
|
|
1037
1070
|
const frameSelector = opts.frameSelector?.trim() || "";
|
|
1038
1071
|
const selector = opts.selector?.trim() || "";
|
|
1039
1072
|
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
@@ -1050,19 +1083,34 @@ async function snapshotRole(opts) {
|
|
|
1050
1083
|
return {
|
|
1051
1084
|
snapshot: built.snapshot,
|
|
1052
1085
|
refs: built.refs,
|
|
1053
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs)
|
|
1086
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
1087
|
+
untrusted: true,
|
|
1088
|
+
contentMeta: {
|
|
1089
|
+
sourceUrl,
|
|
1090
|
+
contentType: "browser-snapshot",
|
|
1091
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1092
|
+
}
|
|
1054
1093
|
};
|
|
1055
1094
|
}
|
|
1056
1095
|
async function snapshotAria(opts) {
|
|
1057
1096
|
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
1058
1097
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1059
1098
|
ensurePageState(page);
|
|
1099
|
+
const sourceUrl = page.url();
|
|
1060
1100
|
const session = await page.context().newCDPSession(page);
|
|
1061
1101
|
try {
|
|
1062
1102
|
await session.send("Accessibility.enable").catch(() => {
|
|
1063
1103
|
});
|
|
1064
1104
|
const res = await session.send("Accessibility.getFullAXTree");
|
|
1065
|
-
return {
|
|
1105
|
+
return {
|
|
1106
|
+
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
1107
|
+
untrusted: true,
|
|
1108
|
+
contentMeta: {
|
|
1109
|
+
sourceUrl,
|
|
1110
|
+
contentType: "browser-aria-tree",
|
|
1111
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1066
1114
|
} finally {
|
|
1067
1115
|
await session.detach().catch(() => {
|
|
1068
1116
|
});
|
|
@@ -1253,7 +1301,7 @@ async function armDialogViaPlaywright(opts) {
|
|
|
1253
1301
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1254
1302
|
ensurePageState(page);
|
|
1255
1303
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1256
|
-
return new Promise((
|
|
1304
|
+
return new Promise((resolve2, reject) => {
|
|
1257
1305
|
const timer = setTimeout(() => {
|
|
1258
1306
|
page.removeListener("dialog", handler);
|
|
1259
1307
|
reject(new Error(`No dialog appeared within ${timeout}ms`));
|
|
@@ -1266,7 +1314,7 @@ async function armDialogViaPlaywright(opts) {
|
|
|
1266
1314
|
} else {
|
|
1267
1315
|
await dialog.dismiss();
|
|
1268
1316
|
}
|
|
1269
|
-
|
|
1317
|
+
resolve2();
|
|
1270
1318
|
} catch (err) {
|
|
1271
1319
|
reject(err);
|
|
1272
1320
|
}
|
|
@@ -1278,7 +1326,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1278
1326
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1279
1327
|
ensurePageState(page);
|
|
1280
1328
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1281
|
-
return new Promise((
|
|
1329
|
+
return new Promise((resolve2, reject) => {
|
|
1282
1330
|
const timer = setTimeout(() => {
|
|
1283
1331
|
page.removeListener("filechooser", handler);
|
|
1284
1332
|
reject(new Error(`No file chooser appeared within ${timeout}ms`));
|
|
@@ -1287,7 +1335,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1287
1335
|
clearTimeout(timer);
|
|
1288
1336
|
try {
|
|
1289
1337
|
await fc.setFiles(opts.paths ?? []);
|
|
1290
|
-
|
|
1338
|
+
resolve2();
|
|
1291
1339
|
} catch (err) {
|
|
1292
1340
|
reject(err);
|
|
1293
1341
|
}
|
|
@@ -1304,11 +1352,82 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1304
1352
|
ensurePageState(page);
|
|
1305
1353
|
await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
|
|
1306
1354
|
}
|
|
1355
|
+
function assertSafeOutputPath(path2, allowedRoots) {
|
|
1356
|
+
if (!path2 || typeof path2 !== "string") {
|
|
1357
|
+
throw new Error("Output path is required.");
|
|
1358
|
+
}
|
|
1359
|
+
const normalized = normalize(path2);
|
|
1360
|
+
if (normalized.includes("..")) {
|
|
1361
|
+
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1362
|
+
}
|
|
1363
|
+
if (allowedRoots?.length) {
|
|
1364
|
+
const resolved = resolve(normalized);
|
|
1365
|
+
const withinRoot = allowedRoots.some((root) => {
|
|
1366
|
+
const normalizedRoot = resolve(root);
|
|
1367
|
+
return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + sep);
|
|
1368
|
+
});
|
|
1369
|
+
if (!withinRoot) {
|
|
1370
|
+
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function isInternalIP(ip) {
|
|
1375
|
+
if (/^127\./.test(ip)) return true;
|
|
1376
|
+
if (/^10\./.test(ip)) return true;
|
|
1377
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1378
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
1379
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
1380
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1381
|
+
if (ip === "0.0.0.0") return true;
|
|
1382
|
+
const lower = ip.toLowerCase();
|
|
1383
|
+
if (lower === "::1") return true;
|
|
1384
|
+
if (lower.startsWith("fe80:")) return true;
|
|
1385
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1386
|
+
if (lower.startsWith("::ffff:")) {
|
|
1387
|
+
const v4 = lower.replace(/^::ffff:/, "");
|
|
1388
|
+
return isInternalIP(v4);
|
|
1389
|
+
}
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
function isInternalUrl(url) {
|
|
1393
|
+
let parsed;
|
|
1394
|
+
try {
|
|
1395
|
+
parsed = new URL(url);
|
|
1396
|
+
} catch {
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1400
|
+
if (hostname === "localhost") return true;
|
|
1401
|
+
if (isInternalIP(hostname)) return true;
|
|
1402
|
+
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
async function isInternalUrlResolved(url) {
|
|
1408
|
+
if (isInternalUrl(url)) return true;
|
|
1409
|
+
let parsed;
|
|
1410
|
+
try {
|
|
1411
|
+
parsed = new URL(url);
|
|
1412
|
+
} catch {
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
const { address } = await lookup(parsed.hostname);
|
|
1417
|
+
if (isInternalIP(address)) return true;
|
|
1418
|
+
} catch {
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1307
1423
|
|
|
1308
1424
|
// src/actions/navigation.ts
|
|
1309
1425
|
async function navigateViaPlaywright(opts) {
|
|
1310
1426
|
const url = String(opts.url ?? "").trim();
|
|
1311
1427
|
if (!url) throw new Error("url is required");
|
|
1428
|
+
if (!opts.allowInternal && await isInternalUrlResolved(url)) {
|
|
1429
|
+
throw new Error(`Navigation to internal/loopback address blocked: "${url}". Set allowInternal: true if this is intentional.`);
|
|
1430
|
+
}
|
|
1312
1431
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1313
1432
|
ensurePageState(page);
|
|
1314
1433
|
await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -1330,11 +1449,14 @@ async function listPagesViaPlaywright(opts) {
|
|
|
1330
1449
|
return results;
|
|
1331
1450
|
}
|
|
1332
1451
|
async function createPageViaPlaywright(opts) {
|
|
1452
|
+
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
1453
|
+
if (targetUrl !== "about:blank" && !opts.allowInternal && await isInternalUrlResolved(targetUrl)) {
|
|
1454
|
+
throw new Error(`Navigation to internal/loopback address blocked: "${targetUrl}". Set allowInternal: true if this is intentional.`);
|
|
1455
|
+
}
|
|
1333
1456
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1334
1457
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
1335
1458
|
const page = await context.newPage();
|
|
1336
1459
|
ensurePageState(page);
|
|
1337
|
-
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
1338
1460
|
if (targetUrl !== "about:blank") {
|
|
1339
1461
|
await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
|
|
1340
1462
|
}
|
|
@@ -1480,6 +1602,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
1480
1602
|
|
|
1481
1603
|
// src/actions/download.ts
|
|
1482
1604
|
async function downloadViaPlaywright(opts) {
|
|
1605
|
+
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1483
1606
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1484
1607
|
ensurePageState(page);
|
|
1485
1608
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
@@ -1506,6 +1629,7 @@ async function waitForDownloadViaPlaywright(opts) {
|
|
|
1506
1629
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1507
1630
|
const download = await page.waitForEvent("download", { timeout });
|
|
1508
1631
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
1632
|
+
assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1509
1633
|
await download.saveAs(savePath);
|
|
1510
1634
|
return {
|
|
1511
1635
|
url: download.url(),
|
|
@@ -1689,6 +1813,7 @@ async function traceStartViaPlaywright(opts) {
|
|
|
1689
1813
|
});
|
|
1690
1814
|
}
|
|
1691
1815
|
async function traceStopViaPlaywright(opts) {
|
|
1816
|
+
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1692
1817
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1693
1818
|
ensurePageState(page);
|
|
1694
1819
|
const context = page.context();
|
|
@@ -1834,10 +1959,12 @@ async function storageClearViaPlaywright(opts) {
|
|
|
1834
1959
|
var CrawlPage = class {
|
|
1835
1960
|
cdpUrl;
|
|
1836
1961
|
targetId;
|
|
1962
|
+
allowInternal;
|
|
1837
1963
|
/** @internal */
|
|
1838
|
-
constructor(cdpUrl, targetId) {
|
|
1964
|
+
constructor(cdpUrl, targetId, allowInternal = false) {
|
|
1839
1965
|
this.cdpUrl = cdpUrl;
|
|
1840
1966
|
this.targetId = targetId;
|
|
1967
|
+
this.allowInternal = allowInternal;
|
|
1841
1968
|
}
|
|
1842
1969
|
/** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
|
|
1843
1970
|
get id() {
|
|
@@ -2169,7 +2296,8 @@ var CrawlPage = class {
|
|
|
2169
2296
|
cdpUrl: this.cdpUrl,
|
|
2170
2297
|
targetId: this.targetId,
|
|
2171
2298
|
url,
|
|
2172
|
-
timeoutMs: opts?.timeoutMs
|
|
2299
|
+
timeoutMs: opts?.timeoutMs,
|
|
2300
|
+
allowInternal: this.allowInternal
|
|
2173
2301
|
});
|
|
2174
2302
|
}
|
|
2175
2303
|
/**
|
|
@@ -2356,12 +2484,14 @@ var CrawlPage = class {
|
|
|
2356
2484
|
* Stop recording a trace and save it to a file.
|
|
2357
2485
|
*
|
|
2358
2486
|
* @param path - File path to save the trace (e.g. `'trace.zip'`)
|
|
2487
|
+
* @param opts - Options (allowedOutputRoots: constrain output to specific directories)
|
|
2359
2488
|
*/
|
|
2360
|
-
async traceStop(path2) {
|
|
2489
|
+
async traceStop(path2, opts) {
|
|
2361
2490
|
return traceStopViaPlaywright({
|
|
2362
2491
|
cdpUrl: this.cdpUrl,
|
|
2363
2492
|
targetId: this.targetId,
|
|
2364
|
-
path: path2
|
|
2493
|
+
path: path2,
|
|
2494
|
+
allowedOutputRoots: opts?.allowedOutputRoots
|
|
2365
2495
|
});
|
|
2366
2496
|
}
|
|
2367
2497
|
/**
|
|
@@ -2549,7 +2679,8 @@ var CrawlPage = class {
|
|
|
2549
2679
|
targetId: this.targetId,
|
|
2550
2680
|
ref,
|
|
2551
2681
|
path: path2,
|
|
2552
|
-
timeoutMs: opts?.timeoutMs
|
|
2682
|
+
timeoutMs: opts?.timeoutMs,
|
|
2683
|
+
allowedOutputRoots: opts?.allowedOutputRoots
|
|
2553
2684
|
});
|
|
2554
2685
|
}
|
|
2555
2686
|
/**
|
|
@@ -2565,7 +2696,8 @@ var CrawlPage = class {
|
|
|
2565
2696
|
cdpUrl: this.cdpUrl,
|
|
2566
2697
|
targetId: this.targetId,
|
|
2567
2698
|
path: opts?.path,
|
|
2568
|
-
timeoutMs: opts?.timeoutMs
|
|
2699
|
+
timeoutMs: opts?.timeoutMs,
|
|
2700
|
+
allowedOutputRoots: opts?.allowedOutputRoots
|
|
2569
2701
|
});
|
|
2570
2702
|
}
|
|
2571
2703
|
// ── Emulation ───────────────────────────────────────────────
|
|
@@ -2695,10 +2827,12 @@ var CrawlPage = class {
|
|
|
2695
2827
|
};
|
|
2696
2828
|
var BrowserClaw = class _BrowserClaw {
|
|
2697
2829
|
cdpUrl;
|
|
2830
|
+
allowInternal;
|
|
2698
2831
|
chrome;
|
|
2699
|
-
constructor(cdpUrl, chrome) {
|
|
2832
|
+
constructor(cdpUrl, chrome, allowInternal = false) {
|
|
2700
2833
|
this.cdpUrl = cdpUrl;
|
|
2701
2834
|
this.chrome = chrome;
|
|
2835
|
+
this.allowInternal = allowInternal;
|
|
2702
2836
|
}
|
|
2703
2837
|
/**
|
|
2704
2838
|
* Launch a new Chrome instance and connect to it.
|
|
@@ -2726,7 +2860,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2726
2860
|
static async launch(opts = {}) {
|
|
2727
2861
|
const chrome = await launchChrome(opts);
|
|
2728
2862
|
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
2729
|
-
return new _BrowserClaw(cdpUrl, chrome);
|
|
2863
|
+
return new _BrowserClaw(cdpUrl, chrome, opts.allowInternal);
|
|
2730
2864
|
}
|
|
2731
2865
|
/**
|
|
2732
2866
|
* Connect to an already-running Chrome instance via its CDP endpoint.
|
|
@@ -2742,12 +2876,12 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2742
2876
|
* const browser = await BrowserClaw.connect('http://localhost:9222');
|
|
2743
2877
|
* ```
|
|
2744
2878
|
*/
|
|
2745
|
-
static async connect(cdpUrl) {
|
|
2746
|
-
if (!await isChromeReachable(cdpUrl, 3e3)) {
|
|
2879
|
+
static async connect(cdpUrl, opts) {
|
|
2880
|
+
if (!await isChromeReachable(cdpUrl, 3e3, opts?.authToken)) {
|
|
2747
2881
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
2748
2882
|
}
|
|
2749
|
-
await connectBrowser(cdpUrl);
|
|
2750
|
-
return new _BrowserClaw(cdpUrl, null);
|
|
2883
|
+
await connectBrowser(cdpUrl, opts?.authToken);
|
|
2884
|
+
return new _BrowserClaw(cdpUrl, null, opts?.allowInternal);
|
|
2751
2885
|
}
|
|
2752
2886
|
/**
|
|
2753
2887
|
* Open a URL in a new tab and return the page handle.
|
|
@@ -2762,8 +2896,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2762
2896
|
* ```
|
|
2763
2897
|
*/
|
|
2764
2898
|
async open(url) {
|
|
2765
|
-
const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url });
|
|
2766
|
-
return new CrawlPage(this.cdpUrl, tab.targetId);
|
|
2899
|
+
const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, allowInternal: this.allowInternal });
|
|
2900
|
+
return new CrawlPage(this.cdpUrl, tab.targetId, this.allowInternal);
|
|
2767
2901
|
}
|
|
2768
2902
|
/**
|
|
2769
2903
|
* Get a CrawlPage handle for the currently active tab.
|
|
@@ -2776,7 +2910,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2776
2910
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
2777
2911
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
2778
2912
|
if (!tid) throw new Error("Failed to get targetId for the current page.");
|
|
2779
|
-
return new CrawlPage(this.cdpUrl, tid);
|
|
2913
|
+
return new CrawlPage(this.cdpUrl, tid, this.allowInternal);
|
|
2780
2914
|
}
|
|
2781
2915
|
/**
|
|
2782
2916
|
* List all open tabs.
|
|
@@ -2811,7 +2945,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2811
2945
|
* @returns CrawlPage for the specified tab
|
|
2812
2946
|
*/
|
|
2813
2947
|
page(targetId) {
|
|
2814
|
-
return new CrawlPage(this.cdpUrl, targetId);
|
|
2948
|
+
return new CrawlPage(this.cdpUrl, targetId, this.allowInternal);
|
|
2815
2949
|
}
|
|
2816
2950
|
/** The CDP endpoint URL for this browser connection. */
|
|
2817
2951
|
get url() {
|