browserclaw 0.2.3 → 0.2.5

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 CHANGED
@@ -6,6 +6,7 @@ var fs = require('fs');
6
6
  var net = require('net');
7
7
  var child_process = require('child_process');
8
8
  var playwrightCore = require('playwright-core');
9
+ var promises = require('dns/promises');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
@@ -272,12 +273,12 @@ function resolveBrowserExecutable(opts) {
272
273
  return null;
273
274
  }
274
275
  async function ensurePortAvailable(port) {
275
- await new Promise((resolve, reject) => {
276
+ await new Promise((resolve2, reject) => {
276
277
  const tester = net__default.default.createServer().once("error", (err) => {
277
278
  if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
278
279
  else reject(err);
279
280
  }).once("listening", () => {
280
- tester.close(() => resolve());
281
+ tester.close(() => resolve2());
281
282
  }).listen(port);
282
283
  });
283
284
  }
@@ -347,11 +348,13 @@ function resolveUserDataDir(profileName) {
347
348
  const configDir = process.env.XDG_CONFIG_HOME ?? path__default.default.join(os__default.default.homedir(), ".config");
348
349
  return path__default.default.join(configDir, "browserclaw", "profiles", profileName, "user-data");
349
350
  }
350
- async function isChromeReachable(cdpUrl, timeoutMs = 500) {
351
+ async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
351
352
  const ctrl = new AbortController();
352
353
  const t = setTimeout(() => ctrl.abort(), timeoutMs);
353
354
  try {
354
- const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
355
+ const headers = {};
356
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
357
+ const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
355
358
  return res.ok;
356
359
  } catch {
357
360
  return false;
@@ -359,11 +362,13 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500) {
359
362
  clearTimeout(t);
360
363
  }
361
364
  }
362
- async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
365
+ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
363
366
  const ctrl = new AbortController();
364
367
  const t = setTimeout(() => ctrl.abort(), timeoutMs);
365
368
  try {
366
- const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
369
+ const headers = {};
370
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
371
+ const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
367
372
  if (!res.ok) return null;
368
373
  const data = await res.json();
369
374
  return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
@@ -629,7 +634,7 @@ function restoreRoleRefsForTarget(opts) {
629
634
  state.roleRefsFrameSelector = entry.frameSelector;
630
635
  state.roleRefsMode = entry.mode;
631
636
  }
632
- async function connectBrowser(cdpUrl) {
637
+ async function connectBrowser(cdpUrl, authToken) {
633
638
  const normalized = normalizeCdpUrl(cdpUrl);
634
639
  if (cached?.cdpUrl === normalized) return cached;
635
640
  const existing = connectingByUrl.get(normalized);
@@ -639,9 +644,11 @@ async function connectBrowser(cdpUrl) {
639
644
  for (let attempt = 0; attempt < 3; attempt++) {
640
645
  try {
641
646
  const timeout = 5e3 + attempt * 2e3;
642
- const endpoint = await getChromeWebSocketUrl(normalized, timeout).catch(() => null) ?? normalized;
643
- const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout });
644
- const connected = { browser, cdpUrl: normalized };
647
+ const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
648
+ const headers = {};
649
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
650
+ const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers });
651
+ const connected = { browser, cdpUrl: normalized, authToken };
645
652
  cached = connected;
646
653
  observeBrowser(browser);
647
654
  browser.on("disconnected", () => {
@@ -694,14 +701,26 @@ async function pageTargetId(page) {
694
701
  }
695
702
  async function findPageByTargetId(browser, targetId, cdpUrl) {
696
703
  const pages = await getAllPages(browser);
704
+ let resolvedViaCdp = false;
697
705
  for (const page of pages) {
698
- const tid = await pageTargetId(page).catch(() => null);
706
+ let tid = null;
707
+ try {
708
+ tid = await pageTargetId(page);
709
+ resolvedViaCdp = true;
710
+ } catch {
711
+ tid = null;
712
+ }
699
713
  if (tid && tid === targetId) return page;
700
714
  }
715
+ if (!resolvedViaCdp && pages.length === 1) {
716
+ return pages[0];
717
+ }
701
718
  if (cdpUrl) {
702
719
  try {
703
720
  const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
704
- const response = await fetch(listUrl);
721
+ const headers = {};
722
+ if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
723
+ const response = await fetch(listUrl, { headers });
705
724
  if (response.ok) {
706
725
  const targets = await response.json();
707
726
  const target = targets.find((t) => t.id === targetId);
@@ -826,6 +845,27 @@ function getIndentLevel(line) {
826
845
  const match = line.match(/^(\s*)/);
827
846
  return match ? Math.floor(match[1].length / 2) : 0;
828
847
  }
848
+ function matchInteractiveSnapshotLine(line, options) {
849
+ const depth = getIndentLevel(line);
850
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
851
+ return null;
852
+ }
853
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
854
+ if (!match) {
855
+ return null;
856
+ }
857
+ const [, , roleRaw, name, suffix] = match;
858
+ if (roleRaw.startsWith("/")) {
859
+ return null;
860
+ }
861
+ const role = roleRaw.toLowerCase();
862
+ return {
863
+ roleRaw,
864
+ role,
865
+ ...name ? { name } : {},
866
+ suffix
867
+ };
868
+ }
829
869
  function createRoleNameTracker() {
830
870
  const counts = /* @__PURE__ */ new Map();
831
871
  const refsByKey = /* @__PURE__ */ new Map();
@@ -899,14 +939,11 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
899
939
  if (options.interactive) {
900
940
  const result2 = [];
901
941
  for (const line of lines) {
902
- const depth = getIndentLevel(line);
903
- if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
904
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
905
- if (!match) continue;
906
- const [, prefix, roleRaw, name, suffix] = match;
907
- if (roleRaw.startsWith("/")) continue;
908
- const role = roleRaw.toLowerCase();
942
+ const parsed = matchInteractiveSnapshotLine(line, options);
943
+ if (!parsed) continue;
944
+ const { roleRaw, role, name, suffix } = parsed;
909
945
  if (!INTERACTIVE_ROLES.has(role)) continue;
946
+ const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
910
947
  const ref = nextRef();
911
948
  const nth = tracker.getNextIndex(role, name);
912
949
  tracker.trackRef(role, name, ref);
@@ -969,16 +1006,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
969
1006
  if (options.interactive) {
970
1007
  const out2 = [];
971
1008
  for (const line of lines) {
972
- const depth = getIndentLevel(line);
973
- if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
974
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
975
- if (!match) continue;
976
- const [, prefix, roleRaw, name, suffix] = match;
977
- if (roleRaw.startsWith("/")) continue;
978
- const role = roleRaw.toLowerCase();
1009
+ const parsed = matchInteractiveSnapshotLine(line, options);
1010
+ if (!parsed) continue;
1011
+ const { roleRaw, role, name, suffix } = parsed;
979
1012
  if (!INTERACTIVE_ROLES.has(role)) continue;
980
1013
  const ref = parseAiSnapshotRef(suffix);
981
1014
  if (!ref) continue;
1015
+ const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
982
1016
  refs[ref] = { role, ...name ? { name } : {} };
983
1017
  out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
984
1018
  }
@@ -1026,6 +1060,7 @@ async function snapshotAi(opts) {
1026
1060
  if (!maybe._snapshotForAI) {
1027
1061
  throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
1028
1062
  }
1063
+ const sourceUrl = page.url();
1029
1064
  const result = await maybe._snapshotForAI({
1030
1065
  timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
1031
1066
  track: "response"
@@ -1052,7 +1087,12 @@ async function snapshotAi(opts) {
1052
1087
  snapshot: built.snapshot,
1053
1088
  refs: built.refs,
1054
1089
  stats: getRoleSnapshotStats(built.snapshot, built.refs),
1055
- untrusted: true
1090
+ untrusted: true,
1091
+ contentMeta: {
1092
+ sourceUrl,
1093
+ contentType: "browser-snapshot",
1094
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1095
+ }
1056
1096
  };
1057
1097
  }
1058
1098
 
@@ -1060,6 +1100,7 @@ async function snapshotAi(opts) {
1060
1100
  async function snapshotRole(opts) {
1061
1101
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1062
1102
  ensurePageState(page);
1103
+ const sourceUrl = page.url();
1063
1104
  const frameSelector = opts.frameSelector?.trim() || "";
1064
1105
  const selector = opts.selector?.trim() || "";
1065
1106
  const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
@@ -1077,19 +1118,33 @@ async function snapshotRole(opts) {
1077
1118
  snapshot: built.snapshot,
1078
1119
  refs: built.refs,
1079
1120
  stats: getRoleSnapshotStats(built.snapshot, built.refs),
1080
- untrusted: true
1121
+ untrusted: true,
1122
+ contentMeta: {
1123
+ sourceUrl,
1124
+ contentType: "browser-snapshot",
1125
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1126
+ }
1081
1127
  };
1082
1128
  }
1083
1129
  async function snapshotAria(opts) {
1084
1130
  const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
1085
1131
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1086
1132
  ensurePageState(page);
1133
+ const sourceUrl = page.url();
1087
1134
  const session = await page.context().newCDPSession(page);
1088
1135
  try {
1089
1136
  await session.send("Accessibility.enable").catch(() => {
1090
1137
  });
1091
1138
  const res = await session.send("Accessibility.getFullAXTree");
1092
- return { nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit), untrusted: true };
1139
+ return {
1140
+ nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
1141
+ untrusted: true,
1142
+ contentMeta: {
1143
+ sourceUrl,
1144
+ contentType: "browser-aria-tree",
1145
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1146
+ }
1147
+ };
1093
1148
  } finally {
1094
1149
  await session.detach().catch(() => {
1095
1150
  });
@@ -1280,7 +1335,7 @@ async function armDialogViaPlaywright(opts) {
1280
1335
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1281
1336
  ensurePageState(page);
1282
1337
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1283
- return new Promise((resolve, reject) => {
1338
+ return new Promise((resolve2, reject) => {
1284
1339
  const timer = setTimeout(() => {
1285
1340
  page.removeListener("dialog", handler);
1286
1341
  reject(new Error(`No dialog appeared within ${timeout}ms`));
@@ -1293,7 +1348,7 @@ async function armDialogViaPlaywright(opts) {
1293
1348
  } else {
1294
1349
  await dialog.dismiss();
1295
1350
  }
1296
- resolve();
1351
+ resolve2();
1297
1352
  } catch (err) {
1298
1353
  reject(err);
1299
1354
  }
@@ -1305,7 +1360,7 @@ async function armFileUploadViaPlaywright(opts) {
1305
1360
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1306
1361
  ensurePageState(page);
1307
1362
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1308
- return new Promise((resolve, reject) => {
1363
+ return new Promise((resolve2, reject) => {
1309
1364
  const timer = setTimeout(() => {
1310
1365
  page.removeListener("filechooser", handler);
1311
1366
  reject(new Error(`No file chooser appeared within ${timeout}ms`));
@@ -1314,7 +1369,7 @@ async function armFileUploadViaPlaywright(opts) {
1314
1369
  clearTimeout(timer);
1315
1370
  try {
1316
1371
  await fc.setFiles(opts.paths ?? []);
1317
- resolve();
1372
+ resolve2();
1318
1373
  } catch (err) {
1319
1374
  reject(err);
1320
1375
  }
@@ -1331,11 +1386,82 @@ async function pressKeyViaPlaywright(opts) {
1331
1386
  ensurePageState(page);
1332
1387
  await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
1333
1388
  }
1389
+ function assertSafeOutputPath(path2, allowedRoots) {
1390
+ if (!path2 || typeof path2 !== "string") {
1391
+ throw new Error("Output path is required.");
1392
+ }
1393
+ const normalized = path.normalize(path2);
1394
+ if (normalized.includes("..")) {
1395
+ throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
1396
+ }
1397
+ if (allowedRoots?.length) {
1398
+ const resolved = path.resolve(normalized);
1399
+ const withinRoot = allowedRoots.some((root) => {
1400
+ const normalizedRoot = path.resolve(root);
1401
+ return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
1402
+ });
1403
+ if (!withinRoot) {
1404
+ throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
1405
+ }
1406
+ }
1407
+ }
1408
+ function isInternalIP(ip) {
1409
+ if (/^127\./.test(ip)) return true;
1410
+ if (/^10\./.test(ip)) return true;
1411
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
1412
+ if (/^192\.168\./.test(ip)) return true;
1413
+ if (/^169\.254\./.test(ip)) return true;
1414
+ if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1415
+ if (ip === "0.0.0.0") return true;
1416
+ const lower = ip.toLowerCase();
1417
+ if (lower === "::1") return true;
1418
+ if (lower.startsWith("fe80:")) return true;
1419
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1420
+ if (lower.startsWith("::ffff:")) {
1421
+ const v4 = lower.replace(/^::ffff:/, "");
1422
+ return isInternalIP(v4);
1423
+ }
1424
+ return false;
1425
+ }
1426
+ function isInternalUrl(url) {
1427
+ let parsed;
1428
+ try {
1429
+ parsed = new URL(url);
1430
+ } catch {
1431
+ return true;
1432
+ }
1433
+ const hostname = parsed.hostname.toLowerCase();
1434
+ if (hostname === "localhost") return true;
1435
+ if (isInternalIP(hostname)) return true;
1436
+ if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1437
+ return true;
1438
+ }
1439
+ return false;
1440
+ }
1441
+ async function isInternalUrlResolved(url) {
1442
+ if (isInternalUrl(url)) return true;
1443
+ let parsed;
1444
+ try {
1445
+ parsed = new URL(url);
1446
+ } catch {
1447
+ return true;
1448
+ }
1449
+ try {
1450
+ const { address } = await promises.lookup(parsed.hostname);
1451
+ if (isInternalIP(address)) return true;
1452
+ } catch {
1453
+ return true;
1454
+ }
1455
+ return false;
1456
+ }
1334
1457
 
1335
1458
  // src/actions/navigation.ts
1336
1459
  async function navigateViaPlaywright(opts) {
1337
1460
  const url = String(opts.url ?? "").trim();
1338
1461
  if (!url) throw new Error("url is required");
1462
+ if (!opts.allowInternal && await isInternalUrlResolved(url)) {
1463
+ throw new Error(`Navigation to internal/loopback address blocked: "${url}". Set allowInternal: true if this is intentional.`);
1464
+ }
1339
1465
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1340
1466
  ensurePageState(page);
1341
1467
  await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
@@ -1357,11 +1483,14 @@ async function listPagesViaPlaywright(opts) {
1357
1483
  return results;
1358
1484
  }
1359
1485
  async function createPageViaPlaywright(opts) {
1486
+ const targetUrl = (opts.url ?? "").trim() || "about:blank";
1487
+ if (targetUrl !== "about:blank" && !opts.allowInternal && await isInternalUrlResolved(targetUrl)) {
1488
+ throw new Error(`Navigation to internal/loopback address blocked: "${targetUrl}". Set allowInternal: true if this is intentional.`);
1489
+ }
1360
1490
  const { browser } = await connectBrowser(opts.cdpUrl);
1361
1491
  const context = browser.contexts()[0] ?? await browser.newContext();
1362
1492
  const page = await context.newPage();
1363
1493
  ensurePageState(page);
1364
- const targetUrl = (opts.url ?? "").trim() || "about:blank";
1365
1494
  if (targetUrl !== "about:blank") {
1366
1495
  await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
1367
1496
  }
@@ -1507,6 +1636,7 @@ async function evaluateViaPlaywright(opts) {
1507
1636
 
1508
1637
  // src/actions/download.ts
1509
1638
  async function downloadViaPlaywright(opts) {
1639
+ assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1510
1640
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1511
1641
  ensurePageState(page);
1512
1642
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
@@ -1533,6 +1663,7 @@ async function waitForDownloadViaPlaywright(opts) {
1533
1663
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1534
1664
  const download = await page.waitForEvent("download", { timeout });
1535
1665
  const savePath = opts.path ?? download.suggestedFilename();
1666
+ assertSafeOutputPath(savePath, opts.allowedOutputRoots);
1536
1667
  await download.saveAs(savePath);
1537
1668
  return {
1538
1669
  url: download.url(),
@@ -1716,6 +1847,7 @@ async function traceStartViaPlaywright(opts) {
1716
1847
  });
1717
1848
  }
1718
1849
  async function traceStopViaPlaywright(opts) {
1850
+ assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1719
1851
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1720
1852
  ensurePageState(page);
1721
1853
  const context = page.context();
@@ -1861,10 +1993,12 @@ async function storageClearViaPlaywright(opts) {
1861
1993
  var CrawlPage = class {
1862
1994
  cdpUrl;
1863
1995
  targetId;
1996
+ allowInternal;
1864
1997
  /** @internal */
1865
- constructor(cdpUrl, targetId) {
1998
+ constructor(cdpUrl, targetId, allowInternal = false) {
1866
1999
  this.cdpUrl = cdpUrl;
1867
2000
  this.targetId = targetId;
2001
+ this.allowInternal = allowInternal;
1868
2002
  }
1869
2003
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
1870
2004
  get id() {
@@ -2196,7 +2330,8 @@ var CrawlPage = class {
2196
2330
  cdpUrl: this.cdpUrl,
2197
2331
  targetId: this.targetId,
2198
2332
  url,
2199
- timeoutMs: opts?.timeoutMs
2333
+ timeoutMs: opts?.timeoutMs,
2334
+ allowInternal: this.allowInternal
2200
2335
  });
2201
2336
  }
2202
2337
  /**
@@ -2383,12 +2518,14 @@ var CrawlPage = class {
2383
2518
  * Stop recording a trace and save it to a file.
2384
2519
  *
2385
2520
  * @param path - File path to save the trace (e.g. `'trace.zip'`)
2521
+ * @param opts - Options (allowedOutputRoots: constrain output to specific directories)
2386
2522
  */
2387
- async traceStop(path2) {
2523
+ async traceStop(path2, opts) {
2388
2524
  return traceStopViaPlaywright({
2389
2525
  cdpUrl: this.cdpUrl,
2390
2526
  targetId: this.targetId,
2391
- path: path2
2527
+ path: path2,
2528
+ allowedOutputRoots: opts?.allowedOutputRoots
2392
2529
  });
2393
2530
  }
2394
2531
  /**
@@ -2576,7 +2713,8 @@ var CrawlPage = class {
2576
2713
  targetId: this.targetId,
2577
2714
  ref,
2578
2715
  path: path2,
2579
- timeoutMs: opts?.timeoutMs
2716
+ timeoutMs: opts?.timeoutMs,
2717
+ allowedOutputRoots: opts?.allowedOutputRoots
2580
2718
  });
2581
2719
  }
2582
2720
  /**
@@ -2592,7 +2730,8 @@ var CrawlPage = class {
2592
2730
  cdpUrl: this.cdpUrl,
2593
2731
  targetId: this.targetId,
2594
2732
  path: opts?.path,
2595
- timeoutMs: opts?.timeoutMs
2733
+ timeoutMs: opts?.timeoutMs,
2734
+ allowedOutputRoots: opts?.allowedOutputRoots
2596
2735
  });
2597
2736
  }
2598
2737
  // ── Emulation ───────────────────────────────────────────────
@@ -2722,10 +2861,12 @@ var CrawlPage = class {
2722
2861
  };
2723
2862
  var BrowserClaw = class _BrowserClaw {
2724
2863
  cdpUrl;
2864
+ allowInternal;
2725
2865
  chrome;
2726
- constructor(cdpUrl, chrome) {
2866
+ constructor(cdpUrl, chrome, allowInternal = false) {
2727
2867
  this.cdpUrl = cdpUrl;
2728
2868
  this.chrome = chrome;
2869
+ this.allowInternal = allowInternal;
2729
2870
  }
2730
2871
  /**
2731
2872
  * Launch a new Chrome instance and connect to it.
@@ -2753,7 +2894,7 @@ var BrowserClaw = class _BrowserClaw {
2753
2894
  static async launch(opts = {}) {
2754
2895
  const chrome = await launchChrome(opts);
2755
2896
  const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
2756
- return new _BrowserClaw(cdpUrl, chrome);
2897
+ return new _BrowserClaw(cdpUrl, chrome, opts.allowInternal);
2757
2898
  }
2758
2899
  /**
2759
2900
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -2769,12 +2910,12 @@ var BrowserClaw = class _BrowserClaw {
2769
2910
  * const browser = await BrowserClaw.connect('http://localhost:9222');
2770
2911
  * ```
2771
2912
  */
2772
- static async connect(cdpUrl) {
2773
- if (!await isChromeReachable(cdpUrl, 3e3)) {
2913
+ static async connect(cdpUrl, opts) {
2914
+ if (!await isChromeReachable(cdpUrl, 3e3, opts?.authToken)) {
2774
2915
  throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
2775
2916
  }
2776
- await connectBrowser(cdpUrl);
2777
- return new _BrowserClaw(cdpUrl, null);
2917
+ await connectBrowser(cdpUrl, opts?.authToken);
2918
+ return new _BrowserClaw(cdpUrl, null, opts?.allowInternal);
2778
2919
  }
2779
2920
  /**
2780
2921
  * Open a URL in a new tab and return the page handle.
@@ -2789,8 +2930,8 @@ var BrowserClaw = class _BrowserClaw {
2789
2930
  * ```
2790
2931
  */
2791
2932
  async open(url) {
2792
- const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url });
2793
- return new CrawlPage(this.cdpUrl, tab.targetId);
2933
+ const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, allowInternal: this.allowInternal });
2934
+ return new CrawlPage(this.cdpUrl, tab.targetId, this.allowInternal);
2794
2935
  }
2795
2936
  /**
2796
2937
  * Get a CrawlPage handle for the currently active tab.
@@ -2803,7 +2944,7 @@ var BrowserClaw = class _BrowserClaw {
2803
2944
  if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
2804
2945
  const tid = await pageTargetId(pages[0]).catch(() => null);
2805
2946
  if (!tid) throw new Error("Failed to get targetId for the current page.");
2806
- return new CrawlPage(this.cdpUrl, tid);
2947
+ return new CrawlPage(this.cdpUrl, tid, this.allowInternal);
2807
2948
  }
2808
2949
  /**
2809
2950
  * List all open tabs.
@@ -2838,7 +2979,7 @@ var BrowserClaw = class _BrowserClaw {
2838
2979
  * @returns CrawlPage for the specified tab
2839
2980
  */
2840
2981
  page(targetId) {
2841
- return new CrawlPage(this.cdpUrl, targetId);
2982
+ return new CrawlPage(this.cdpUrl, targetId, this.allowInternal);
2842
2983
  }
2843
2984
  /** The CDP endpoint URL for this browser connection. */
2844
2985
  get url() {