browserclaw 0.3.5 → 0.3.7

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
@@ -7,6 +7,7 @@ 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 promises$1 = require('fs/promises');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
13
 
@@ -355,7 +356,9 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
355
356
  const headers = {};
356
357
  if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
357
358
  const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
358
- return res.ok;
359
+ if (!res.ok) return false;
360
+ const data = await res.json();
361
+ return data != null && typeof data === "object";
359
362
  } catch {
360
363
  return false;
361
364
  } finally {
@@ -371,6 +374,7 @@ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
371
374
  const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
372
375
  if (!res.ok) return null;
373
376
  const data = await res.json();
377
+ if (!data || typeof data !== "object") return null;
374
378
  return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
375
379
  } catch {
376
380
  return null;
@@ -1195,6 +1199,222 @@ function formatAriaNodes(nodes, limit) {
1195
1199
  }
1196
1200
  return out;
1197
1201
  }
1202
+ var InvalidBrowserNavigationUrlError = class extends Error {
1203
+ constructor(message) {
1204
+ super(message);
1205
+ this.name = "InvalidBrowserNavigationUrlError";
1206
+ }
1207
+ };
1208
+ function withBrowserNavigationPolicy(ssrfPolicy) {
1209
+ return { ssrfPolicy };
1210
+ }
1211
+ var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
1212
+ var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
1213
+ async function assertBrowserNavigationAllowed(opts) {
1214
+ const rawUrl = String(opts.url ?? "").trim();
1215
+ let parsed;
1216
+ try {
1217
+ parsed = new URL(rawUrl);
1218
+ } catch {
1219
+ throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
1220
+ }
1221
+ if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
1222
+ if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
1223
+ throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
1224
+ }
1225
+ const policy = opts.ssrfPolicy;
1226
+ if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
1227
+ const allowedHostnames = [
1228
+ ...policy?.allowedHostnames ?? [],
1229
+ ...policy?.hostnameAllowlist ?? []
1230
+ ];
1231
+ if (allowedHostnames.length) {
1232
+ const hostname = parsed.hostname.toLowerCase();
1233
+ if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1234
+ }
1235
+ if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
1236
+ throw new InvalidBrowserNavigationUrlError(
1237
+ `Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1238
+ );
1239
+ }
1240
+ }
1241
+ async function assertSafeOutputPath(path2, allowedRoots) {
1242
+ if (!path2 || typeof path2 !== "string") {
1243
+ throw new Error("Output path is required.");
1244
+ }
1245
+ const normalized = path.normalize(path2);
1246
+ if (normalized.includes("..")) {
1247
+ throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
1248
+ }
1249
+ if (allowedRoots?.length) {
1250
+ const resolved = path.resolve(normalized);
1251
+ let parentReal;
1252
+ try {
1253
+ parentReal = await promises$1.realpath(path.dirname(resolved));
1254
+ } catch {
1255
+ throw new Error(`Unsafe output path: parent directory is inaccessible for "${path2}".`);
1256
+ }
1257
+ try {
1258
+ const targetStat = await promises$1.lstat(resolved);
1259
+ if (targetStat.isSymbolicLink()) {
1260
+ throw new Error(`Unsafe output path: "${path2}" is a symbolic link.`);
1261
+ }
1262
+ } catch (e) {
1263
+ if (e.code !== "ENOENT") throw e;
1264
+ }
1265
+ const results = await Promise.all(
1266
+ allowedRoots.map(async (root) => {
1267
+ try {
1268
+ const rootStat = await promises$1.lstat(path.resolve(root));
1269
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return false;
1270
+ const rootReal = await promises$1.realpath(path.resolve(root));
1271
+ return parentReal === rootReal || parentReal.startsWith(rootReal + path.sep);
1272
+ } catch {
1273
+ return false;
1274
+ }
1275
+ })
1276
+ );
1277
+ if (!results.some(Boolean)) {
1278
+ throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
1279
+ }
1280
+ }
1281
+ }
1282
+ function expandIPv6(ip) {
1283
+ let normalized = ip;
1284
+ const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
1285
+ if (v4Match) {
1286
+ const octets = v4Match[2].split(".").map(Number);
1287
+ if (octets.some((o) => o > 255)) return null;
1288
+ const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
1289
+ const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
1290
+ normalized = v4Match[1] + hexHi + ":" + hexLo;
1291
+ }
1292
+ const halves = normalized.split("::");
1293
+ if (halves.length > 2) return null;
1294
+ if (halves.length === 2) {
1295
+ const left = halves[0] !== "" ? halves[0].split(":") : [];
1296
+ const right = halves[1] !== "" ? halves[1].split(":") : [];
1297
+ const needed = 8 - left.length - right.length;
1298
+ if (needed < 0) return null;
1299
+ const groups2 = [...left, ...Array(needed).fill("0"), ...right];
1300
+ if (groups2.length !== 8) return null;
1301
+ return groups2.map((g) => g.padStart(4, "0")).join(":");
1302
+ }
1303
+ const groups = normalized.split(":");
1304
+ if (groups.length !== 8) return null;
1305
+ return groups.map((g) => g.padStart(4, "0")).join(":");
1306
+ }
1307
+ function hexToIPv4(hiHex, loHex) {
1308
+ const hi = parseInt(hiHex, 16);
1309
+ const lo = parseInt(loHex, 16);
1310
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1311
+ }
1312
+ function extractEmbeddedIPv4(lower) {
1313
+ if (lower.startsWith("::ffff:")) {
1314
+ return lower.slice(7);
1315
+ }
1316
+ const expanded = expandIPv6(lower);
1317
+ if (expanded === null) return "";
1318
+ const groups = expanded.split(":");
1319
+ if (groups.length !== 8) return "";
1320
+ if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
1321
+ return hexToIPv4(groups[6], groups[7]);
1322
+ }
1323
+ if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
1324
+ return hexToIPv4(groups[6], groups[7]);
1325
+ }
1326
+ if (groups[0] === "2002") {
1327
+ return hexToIPv4(groups[1], groups[2]);
1328
+ }
1329
+ if (groups[0] === "2001" && groups[1] === "0000") {
1330
+ const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
1331
+ const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
1332
+ return hexToIPv4(hiXored, loXored);
1333
+ }
1334
+ return null;
1335
+ }
1336
+ function isStrictDecimalOctet(part) {
1337
+ if (!/^[0-9]+$/.test(part)) return false;
1338
+ const n = parseInt(part, 10);
1339
+ if (n < 0 || n > 255) return false;
1340
+ if (String(n) !== part) return false;
1341
+ return true;
1342
+ }
1343
+ function isUnsupportedIPv4Literal(ip) {
1344
+ if (/^[0-9]+$/.test(ip)) return true;
1345
+ const parts = ip.split(".");
1346
+ if (parts.length !== 4) return true;
1347
+ if (!parts.every(isStrictDecimalOctet)) return true;
1348
+ return false;
1349
+ }
1350
+ function isInternalIP(ip) {
1351
+ if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
1352
+ if (/^127\./.test(ip)) return true;
1353
+ if (/^10\./.test(ip)) return true;
1354
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
1355
+ if (/^192\.168\./.test(ip)) return true;
1356
+ if (/^169\.254\./.test(ip)) return true;
1357
+ if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1358
+ if (ip === "0.0.0.0") return true;
1359
+ const lower = ip.toLowerCase();
1360
+ if (lower === "::1") return true;
1361
+ if (lower.startsWith("fe80:")) return true;
1362
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1363
+ if (lower.startsWith("ff")) return true;
1364
+ const embedded = extractEmbeddedIPv4(lower);
1365
+ if (embedded !== null) {
1366
+ if (embedded === "") return true;
1367
+ return isInternalIP(embedded);
1368
+ }
1369
+ return false;
1370
+ }
1371
+ function isInternalUrl(url) {
1372
+ let parsed;
1373
+ try {
1374
+ parsed = new URL(url);
1375
+ } catch {
1376
+ return true;
1377
+ }
1378
+ const hostname = parsed.hostname.toLowerCase();
1379
+ if (hostname === "localhost") return true;
1380
+ if (isInternalIP(hostname)) return true;
1381
+ if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1382
+ return true;
1383
+ }
1384
+ return false;
1385
+ }
1386
+ async function assertSafeUploadPaths(paths) {
1387
+ for (const filePath of paths) {
1388
+ let stat;
1389
+ try {
1390
+ stat = await promises$1.lstat(filePath);
1391
+ } catch {
1392
+ throw new Error(`Upload path does not exist or is inaccessible: "${filePath}".`);
1393
+ }
1394
+ if (stat.isSymbolicLink()) {
1395
+ throw new Error(`Upload path is a symbolic link: "${filePath}".`);
1396
+ }
1397
+ if (!stat.isFile()) {
1398
+ throw new Error(`Upload path is not a regular file: "${filePath}".`);
1399
+ }
1400
+ }
1401
+ }
1402
+ async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1403
+ if (isInternalUrl(url)) return true;
1404
+ let parsed;
1405
+ try {
1406
+ parsed = new URL(url);
1407
+ } catch {
1408
+ return true;
1409
+ }
1410
+ try {
1411
+ const { address } = await lookupFn(parsed.hostname);
1412
+ if (isInternalIP(address)) return true;
1413
+ } catch {
1414
+ return true;
1415
+ }
1416
+ return false;
1417
+ }
1198
1418
 
1199
1419
  // src/actions/interaction.ts
1200
1420
  async function clickViaPlaywright(opts) {
@@ -1327,6 +1547,7 @@ async function setInputFilesViaPlaywright(opts) {
1327
1547
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1328
1548
  const locator = opts.ref ? refLocator(page, opts.ref) : opts.element ? page.locator(opts.element).first() : null;
1329
1549
  if (!locator) throw new Error("Either ref or element is required for setInputFiles");
1550
+ await assertSafeUploadPaths(opts.paths);
1330
1551
  try {
1331
1552
  await locator.setInputFiles(opts.paths);
1332
1553
  } catch (err) {
@@ -1370,7 +1591,9 @@ async function armFileUploadViaPlaywright(opts) {
1370
1591
  const handler = async (fc) => {
1371
1592
  clearTimeout(timer);
1372
1593
  try {
1373
- await fc.setFiles(opts.paths ?? []);
1594
+ const paths = opts.paths ?? [];
1595
+ if (paths.length > 0) await assertSafeUploadPaths(paths);
1596
+ await fc.setFiles(paths);
1374
1597
  resolve2();
1375
1598
  } catch (err) {
1376
1599
  reject(err);
@@ -1388,183 +1611,6 @@ async function pressKeyViaPlaywright(opts) {
1388
1611
  ensurePageState(page);
1389
1612
  await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
1390
1613
  }
1391
- var InvalidBrowserNavigationUrlError = class extends Error {
1392
- constructor(message) {
1393
- super(message);
1394
- this.name = "InvalidBrowserNavigationUrlError";
1395
- }
1396
- };
1397
- function withBrowserNavigationPolicy(ssrfPolicy) {
1398
- return { ssrfPolicy };
1399
- }
1400
- var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
1401
- var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
1402
- async function assertBrowserNavigationAllowed(opts) {
1403
- const rawUrl = String(opts.url ?? "").trim();
1404
- let parsed;
1405
- try {
1406
- parsed = new URL(rawUrl);
1407
- } catch {
1408
- throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
1409
- }
1410
- if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
1411
- if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
1412
- throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
1413
- }
1414
- const policy = opts.ssrfPolicy;
1415
- if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
1416
- const allowedHostnames = [
1417
- ...policy?.allowedHostnames ?? [],
1418
- ...policy?.hostnameAllowlist ?? []
1419
- ];
1420
- if (allowedHostnames.length) {
1421
- const hostname = parsed.hostname.toLowerCase();
1422
- if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1423
- }
1424
- if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
1425
- throw new InvalidBrowserNavigationUrlError(
1426
- `Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1427
- );
1428
- }
1429
- }
1430
- function assertSafeOutputPath(path2, allowedRoots) {
1431
- if (!path2 || typeof path2 !== "string") {
1432
- throw new Error("Output path is required.");
1433
- }
1434
- const normalized = path.normalize(path2);
1435
- if (normalized.includes("..")) {
1436
- throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
1437
- }
1438
- if (allowedRoots?.length) {
1439
- const resolved = path.resolve(normalized);
1440
- const withinRoot = allowedRoots.some((root) => {
1441
- const normalizedRoot = path.resolve(root);
1442
- return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
1443
- });
1444
- if (!withinRoot) {
1445
- throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
1446
- }
1447
- }
1448
- }
1449
- function expandIPv6(ip) {
1450
- let normalized = ip;
1451
- const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
1452
- if (v4Match) {
1453
- const octets = v4Match[2].split(".").map(Number);
1454
- if (octets.some((o) => o > 255)) return null;
1455
- const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
1456
- const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
1457
- normalized = v4Match[1] + hexHi + ":" + hexLo;
1458
- }
1459
- const halves = normalized.split("::");
1460
- if (halves.length > 2) return null;
1461
- if (halves.length === 2) {
1462
- const left = halves[0] !== "" ? halves[0].split(":") : [];
1463
- const right = halves[1] !== "" ? halves[1].split(":") : [];
1464
- const needed = 8 - left.length - right.length;
1465
- if (needed < 0) return null;
1466
- const groups2 = [...left, ...Array(needed).fill("0"), ...right];
1467
- if (groups2.length !== 8) return null;
1468
- return groups2.map((g) => g.padStart(4, "0")).join(":");
1469
- }
1470
- const groups = normalized.split(":");
1471
- if (groups.length !== 8) return null;
1472
- return groups.map((g) => g.padStart(4, "0")).join(":");
1473
- }
1474
- function hexToIPv4(hiHex, loHex) {
1475
- const hi = parseInt(hiHex, 16);
1476
- const lo = parseInt(loHex, 16);
1477
- return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1478
- }
1479
- function extractEmbeddedIPv4(lower) {
1480
- if (lower.startsWith("::ffff:")) {
1481
- return lower.slice(7);
1482
- }
1483
- const expanded = expandIPv6(lower);
1484
- if (expanded === null) return "";
1485
- const groups = expanded.split(":");
1486
- if (groups.length !== 8) return "";
1487
- if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
1488
- return hexToIPv4(groups[6], groups[7]);
1489
- }
1490
- if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
1491
- return hexToIPv4(groups[6], groups[7]);
1492
- }
1493
- if (groups[0] === "2002") {
1494
- return hexToIPv4(groups[1], groups[2]);
1495
- }
1496
- if (groups[0] === "2001" && groups[1] === "0000") {
1497
- const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
1498
- const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
1499
- return hexToIPv4(hiXored, loXored);
1500
- }
1501
- return null;
1502
- }
1503
- function isStrictDecimalOctet(part) {
1504
- if (!/^[0-9]+$/.test(part)) return false;
1505
- const n = parseInt(part, 10);
1506
- if (n < 0 || n > 255) return false;
1507
- if (String(n) !== part) return false;
1508
- return true;
1509
- }
1510
- function isUnsupportedIPv4Literal(ip) {
1511
- if (/^[0-9]+$/.test(ip)) return true;
1512
- const parts = ip.split(".");
1513
- if (parts.length !== 4) return true;
1514
- if (!parts.every(isStrictDecimalOctet)) return true;
1515
- return false;
1516
- }
1517
- function isInternalIP(ip) {
1518
- if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
1519
- if (/^127\./.test(ip)) return true;
1520
- if (/^10\./.test(ip)) return true;
1521
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
1522
- if (/^192\.168\./.test(ip)) return true;
1523
- if (/^169\.254\./.test(ip)) return true;
1524
- if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1525
- if (ip === "0.0.0.0") return true;
1526
- const lower = ip.toLowerCase();
1527
- if (lower === "::1") return true;
1528
- if (lower.startsWith("fe80:")) return true;
1529
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1530
- const embedded = extractEmbeddedIPv4(lower);
1531
- if (embedded !== null) {
1532
- if (embedded === "") return true;
1533
- return isInternalIP(embedded);
1534
- }
1535
- return false;
1536
- }
1537
- function isInternalUrl(url) {
1538
- let parsed;
1539
- try {
1540
- parsed = new URL(url);
1541
- } catch {
1542
- return true;
1543
- }
1544
- const hostname = parsed.hostname.toLowerCase();
1545
- if (hostname === "localhost") return true;
1546
- if (isInternalIP(hostname)) return true;
1547
- if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1548
- return true;
1549
- }
1550
- return false;
1551
- }
1552
- async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1553
- if (isInternalUrl(url)) return true;
1554
- let parsed;
1555
- try {
1556
- parsed = new URL(url);
1557
- } catch {
1558
- return true;
1559
- }
1560
- try {
1561
- const { address } = await lookupFn(parsed.hostname);
1562
- if (isInternalIP(address)) return true;
1563
- } catch {
1564
- return true;
1565
- }
1566
- return false;
1567
- }
1568
1614
 
1569
1615
  // src/actions/navigation.ts
1570
1616
  async function navigateViaPlaywright(opts) {
@@ -1756,7 +1802,7 @@ async function evaluateViaPlaywright(opts) {
1756
1802
 
1757
1803
  // src/actions/download.ts
1758
1804
  async function downloadViaPlaywright(opts) {
1759
- assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1805
+ await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1760
1806
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1761
1807
  ensurePageState(page);
1762
1808
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
@@ -1783,7 +1829,7 @@ async function waitForDownloadViaPlaywright(opts) {
1783
1829
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1784
1830
  const download = await page.waitForEvent("download", { timeout });
1785
1831
  const savePath = opts.path ?? download.suggestedFilename();
1786
- assertSafeOutputPath(savePath, opts.allowedOutputRoots);
1832
+ await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
1787
1833
  await download.saveAs(savePath);
1788
1834
  return {
1789
1835
  url: download.url(),
@@ -1967,7 +2013,7 @@ async function traceStartViaPlaywright(opts) {
1967
2013
  });
1968
2014
  }
1969
2015
  async function traceStopViaPlaywright(opts) {
1970
- assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
2016
+ await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1971
2017
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1972
2018
  ensurePageState(page);
1973
2019
  const context = page.context();