browser-debugger-cli 0.14.0 → 0.15.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.
Files changed (69) hide show
  1. package/.claude/skills/bdg/SKILL.md +1 -1
  2. package/dist/commands/cdp.js +1 -0
  3. package/dist/commands/cleanup.js +3 -0
  4. package/dist/commands/dom/eval.d.ts +2 -1
  5. package/dist/commands/dom/eval.js +6 -21
  6. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  7. package/dist/commands/dom/helpers/evalResult.js +59 -0
  8. package/dist/commands/helpJson.d.ts +1 -1
  9. package/dist/commands/helpJson.js +3 -3
  10. package/dist/commands/helpTopic.js +10 -4
  11. package/dist/commands/network/har.js +18 -14
  12. package/dist/commands/optionBehaviors.js +8 -3
  13. package/dist/commands/shared/optionTypes.d.ts +1 -0
  14. package/dist/commands/shared/outputFile.d.ts +2 -1
  15. package/dist/commands/shared/outputFile.js +7 -4
  16. package/dist/commands/status.js +3 -1
  17. package/dist/commands/stop.js +2 -1
  18. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  19. package/dist/connection/launcher/flagsBuilder.js +107 -23
  20. package/dist/connection/launcher.d.ts +1 -1
  21. package/dist/connection/launcher.js +1 -2
  22. package/dist/constants.d.ts +2 -4
  23. package/dist/constants.js +2 -4
  24. package/dist/daemon/launcher.d.ts +17 -3
  25. package/dist/daemon/launcher.js +37 -7
  26. package/dist/daemon/session/commandRegistry.js +2 -2
  27. package/dist/daemon.js +8107 -7962
  28. package/dist/errors/messages.d.ts +23 -0
  29. package/dist/errors/messages.js +86 -6
  30. package/dist/index.js +555 -166
  31. package/dist/ipc/client.d.ts +6 -1
  32. package/dist/ipc/client.js +11 -2
  33. package/dist/ipc/protocol/commands.d.ts +4 -0
  34. package/dist/ipc/transport/index.d.ts +6 -0
  35. package/dist/ipc/transport/index.js +16 -1
  36. package/dist/runtime/dom/elementInfo.d.ts +7 -0
  37. package/dist/runtime/dom/elementInfo.js +8 -1
  38. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  39. package/dist/runtime/dom/evalHelpers.js +40 -12
  40. package/dist/runtime/dom/frames.d.ts +2 -1
  41. package/dist/runtime/dom/frames.js +3 -1
  42. package/dist/runtime/page/emulation.js +6 -5
  43. package/dist/runtime/page/userAgent.d.ts +86 -2
  44. package/dist/runtime/page/userAgent.js +154 -33
  45. package/dist/session/paths.d.ts +38 -3
  46. package/dist/session/paths.js +154 -7
  47. package/dist/session/portClaims.d.ts +0 -8
  48. package/dist/session/portClaims.js +1 -22
  49. package/dist/session/sessionList.d.ts +5 -1
  50. package/dist/session/sessionList.js +5 -1
  51. package/dist/telemetry/har/builder.d.ts +12 -1
  52. package/dist/telemetry/har/builder.js +10 -2
  53. package/dist/telemetry/har/sanitize.d.ts +24 -0
  54. package/dist/telemetry/har/sanitize.js +138 -0
  55. package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
  56. package/dist/telemetry/har/sanitizeBody.js +168 -0
  57. package/dist/ui/formatters/sessions.d.ts +3 -2
  58. package/dist/ui/formatters/sessions.js +10 -3
  59. package/dist/ui/messages/chrome.d.ts +14 -6
  60. package/dist/ui/messages/chrome.js +52 -12
  61. package/dist/ui/messages/networkMessages.d.ts +26 -0
  62. package/dist/ui/messages/networkMessages.js +21 -0
  63. package/dist/ui/messages/session.d.ts +8 -0
  64. package/dist/ui/messages/session.js +10 -0
  65. package/dist/utils/atomicFile.d.ts +2 -1
  66. package/dist/utils/atomicFile.js +5 -2
  67. package/dist/utils/directories.d.ts +41 -0
  68. package/dist/utils/directories.js +48 -0
  69. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -960,7 +960,7 @@ var require_node = __commonJS({
960
960
  var tty2 = __require("tty");
961
961
  var util = __require("util");
962
962
  exports.init = init;
963
- exports.log = log34;
963
+ exports.log = log36;
964
964
  exports.formatArgs = formatArgs;
965
965
  exports.save = save;
966
966
  exports.load = load;
@@ -1095,7 +1095,7 @@ var require_node = __commonJS({
1095
1095
  }
1096
1096
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
1097
1097
  }
1098
- function log34(...args) {
1098
+ function log36(...args) {
1099
1099
  return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + "\n");
1100
1100
  }
1101
1101
  function save(namespaces) {
@@ -1287,14 +1287,14 @@ var require_is_docker = __commonJS({
1287
1287
  var require_is_wsl = __commonJS({
1288
1288
  "node_modules/is-wsl/index.js"(exports, module) {
1289
1289
  "use strict";
1290
- var os4 = __require("os");
1290
+ var os5 = __require("os");
1291
1291
  var fs19 = __require("fs");
1292
1292
  var isDocker = require_is_docker();
1293
1293
  var isWsl3 = () => {
1294
1294
  if (process.platform !== "linux") {
1295
1295
  return false;
1296
1296
  }
1297
- if (os4.release().toLowerCase().includes("microsoft")) {
1297
+ if (os5.release().toLowerCase().includes("microsoft")) {
1298
1298
  if (isDocker()) {
1299
1299
  return false;
1300
1300
  }
@@ -1327,7 +1327,7 @@ function resourceTypeFromName(name) {
1327
1327
  ([type, abbreviation]) => type.toLowerCase() === wanted || abbreviation.toLowerCase() === wanted
1328
1328
  )?.[0];
1329
1329
  }
1330
- var HTTP_LOCALHOST, MAX_NETWORK_REQUESTS, MAX_CONSOLE_MESSAGES, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, MAX_VALUE_LENGTH, MAX_CONSOLE_TEXT_LENGTH, MAX_CONSOLE_JSON_TEXT_LENGTH, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, QUERY_JSON_LIST_LIMIT, LAYOUT_ELEMENT_LIMIT, MAX_TRIGGERED_REQUESTS, MAX_JSONL_BUFFER_SIZE, PORT_OPTION_DESCRIPTION, RESOURCE_TYPE_ABBREVIATIONS, MIME_TYPE_RULES;
1330
+ var HTTP_LOCALHOST, MAX_NETWORK_REQUESTS, MAX_CONSOLE_MESSAGES, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, MAX_VALUE_LENGTH, MAX_CONSOLE_TEXT_LENGTH, MAX_CONSOLE_JSON_TEXT_LENGTH, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, QUERY_JSON_LIST_LIMIT, EVAL_JSON_ARRAY_LIMIT, LAYOUT_ELEMENT_LIMIT, MAX_TRIGGERED_REQUESTS, MAX_JSONL_BUFFER_SIZE, PORT_OPTION_DESCRIPTION, RESOURCE_TYPE_ABBREVIATIONS, MIME_TYPE_RULES;
1331
1331
  var init_constants = __esm({
1332
1332
  "src/constants.ts"() {
1333
1333
  "use strict";
@@ -1343,6 +1343,7 @@ var init_constants = __esm({
1343
1343
  CHROME_NETWORK_BUFFER_PER_RESOURCE = 10 * 1024 * 1024;
1344
1344
  CHROME_POST_DATA_LIMIT = 1 * 1024 * 1024;
1345
1345
  QUERY_JSON_LIST_LIMIT = 100;
1346
+ EVAL_JSON_ARRAY_LIMIT = 100;
1346
1347
  LAYOUT_ELEMENT_LIMIT = 100;
1347
1348
  MAX_TRIGGERED_REQUESTS = 50;
1348
1349
  MAX_JSONL_BUFFER_SIZE = 256 * 1024 * 1024;
@@ -1421,8 +1422,8 @@ function createLogger(context) {
1421
1422
  logger.debug = (message) => logMessage(message, "debug");
1422
1423
  return logger;
1423
1424
  }
1424
- function logDebugError(log34, context, error) {
1425
- log34.debug(`Failed to ${context}: ${getErrorMessage(error)}`);
1425
+ function logDebugError(log36, context, error) {
1426
+ log36.debug(`Failed to ${context}: ${getErrorMessage(error)}`);
1426
1427
  }
1427
1428
  var debugEnabled;
1428
1429
  var init_logger = __esm({
@@ -1489,11 +1490,30 @@ function makeDirectory(dir, mode) {
1489
1490
  }
1490
1491
  fs2.mkdirSync(dir, { recursive: true, ...mode !== void 0 && { mode } });
1491
1492
  }
1492
- var PSEUDO_FILESYSTEM;
1493
+ function dirTrustProblem(dir, options = {}) {
1494
+ const stat = fs2.lstatSync(dir);
1495
+ const problem = (reason, kind) => ({ reason, kind });
1496
+ if (stat.isSymbolicLink()) return problem("it is a symbolic link", "symlink");
1497
+ if (!stat.isDirectory()) return problem("not a directory", "not-directory");
1498
+ const uid = process.getuid?.();
1499
+ if (uid !== void 0 && stat.uid !== uid) return problem(`owned by uid ${stat.uid}`, "owner");
1500
+ if (process.platform === "win32") return null;
1501
+ const writeBits = options.allowGroupWrite ? OTHER_WRITE : GROUP_OTHER_WRITE;
1502
+ if ((stat.mode & writeBits) === 0) return null;
1503
+ const mode = (stat.mode & 4095).toString(8);
1504
+ if ((stat.mode & STICKY) !== 0 && (stat.mode & OTHER_WRITE) !== 0) {
1505
+ return problem(`a shared sticky directory (mode ${mode})`, "shared");
1506
+ }
1507
+ return problem(`writable by others (mode ${mode})`, "writable");
1508
+ }
1509
+ var PSEUDO_FILESYSTEM, GROUP_OTHER_WRITE, OTHER_WRITE, STICKY;
1493
1510
  var init_directories = __esm({
1494
1511
  "src/utils/directories.ts"() {
1495
1512
  "use strict";
1496
1513
  PSEUDO_FILESYSTEM = /^\/(proc|sys)(\/|$)/;
1514
+ GROUP_OTHER_WRITE = 18;
1515
+ OTHER_WRITE = 2;
1516
+ STICKY = 512;
1497
1517
  }
1498
1518
  });
1499
1519
 
@@ -1502,12 +1522,16 @@ import * as fs3 from "fs";
1502
1522
  import * as os2 from "os";
1503
1523
  import * as path3 from "path";
1504
1524
  function getSessionBaseDir() {
1505
- const override = process.env[SESSION_DIR_OVERRIDE_ENV];
1506
- if (override && override.trim().length > 0) {
1525
+ const override = sessionDirOverride();
1526
+ if (override !== null) {
1507
1527
  return path3.isAbsolute(override) ? override : path3.resolve(override);
1508
1528
  }
1509
1529
  return path3.join(os2.homedir(), ".bdg");
1510
1530
  }
1531
+ function sessionDirOverride() {
1532
+ const override = process.env[SESSION_DIR_OVERRIDE_ENV];
1533
+ return override && override.trim().length > 0 ? override : null;
1534
+ }
1511
1535
  function getSessionName() {
1512
1536
  const name = process.env[SESSION_NAME_ENV]?.trim();
1513
1537
  if (!name) return null;
@@ -1543,14 +1567,81 @@ function getDaemonSocketPath() {
1543
1567
  return getSessionFilePath("DAEMON_SOCKET");
1544
1568
  }
1545
1569
  function ensureSessionDir() {
1546
- makeDirectory(getSessionDir());
1570
+ makeDirectory(getSessionDir(), PRIVATE_DIR_MODE);
1547
1571
  }
1548
- var log, SESSION_FILES, SESSION_DIR_OVERRIDE_ENV, SESSION_NAME_ENV, NAMED_SESSIONS_DIR, MAX_SOCKET_PATH_BYTES, MAX_DAEMON_SOCKET_PATH_BYTES, SESSION_STATE_FILES;
1572
+ function sessionDirChain(dir) {
1573
+ const base = getSessionBaseDir();
1574
+ const relative2 = path3.relative(base, dir);
1575
+ if (relative2 === ".." || relative2.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative2)) {
1576
+ return [{ dir, bdgOwned: false, isBase: false }];
1577
+ }
1578
+ const chain = [{ dir: base, bdgOwned: sessionDirOverride() === null, isBase: true }];
1579
+ let current = base;
1580
+ for (const part of relative2.split(path3.sep).filter(Boolean)) {
1581
+ current = path3.join(current, part);
1582
+ chain.push({ dir: current, bdgOwned: true, isBase: false });
1583
+ }
1584
+ return chain;
1585
+ }
1586
+ function tightenDir(dir) {
1587
+ if (process.platform === "win32") return;
1588
+ let fd;
1589
+ try {
1590
+ const { O_RDONLY, O_DIRECTORY, O_NOFOLLOW } = fs3.constants;
1591
+ fd = fs3.openSync(dir, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
1592
+ const stat = fs3.fstatSync(fd);
1593
+ if (stat.uid !== process.getuid?.() || (stat.mode & GROUP_OTHER_ACCESS) === 0) return;
1594
+ fs3.fchmodSync(fd, PRIVATE_DIR_MODE);
1595
+ log.debug(`Session directory ${dir} restricted to mode 700`);
1596
+ } catch (error) {
1597
+ log.debug(`Session directory ${dir} not restricted: ${getErrorMessage(error)}`);
1598
+ } finally {
1599
+ if (fd !== void 0) fs3.closeSync(fd);
1600
+ }
1601
+ }
1602
+ function chainDirProblem(entry) {
1603
+ const problem = dirTrustProblem(entry.dir, SESSION_DIR_TRUST);
1604
+ if (problem?.kind !== "symlink" || !entry.isBase) return problem;
1605
+ const target = fs3.realpathSync(entry.dir);
1606
+ const targetProblem = dirTrustProblem(target, SESSION_DIR_TRUST);
1607
+ return targetProblem && {
1608
+ ...targetProblem,
1609
+ reason: `it links to ${target}, which is ${targetProblem.reason}`
1610
+ };
1611
+ }
1612
+ function acceptTrustedDir(entry) {
1613
+ if (entry.bdgOwned) {
1614
+ tightenDir(entry.dir);
1615
+ return;
1616
+ }
1617
+ const mode = fs3.statSync(entry.dir, { throwIfNoEntry: false })?.mode ?? 0;
1618
+ if ((mode & GROUP_OTHER_ACCESS) === 0) return;
1619
+ log.debug(
1620
+ `Session directory ${entry.dir} is open to group or others; left as is (chosen with ${SESSION_DIR_OVERRIDE_ENV})`
1621
+ );
1622
+ }
1623
+ function secureSessionDir(dir = getSessionDir()) {
1624
+ for (const entry of sessionDirChain(dir)) {
1625
+ let problem;
1626
+ try {
1627
+ problem = chainDirProblem(entry);
1628
+ } catch (error) {
1629
+ const code = error.code;
1630
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
1631
+ problem = { reason: getErrorMessage(error), kind: "not-directory" };
1632
+ }
1633
+ if (problem !== null) return { dir: entry.dir, ...problem, bdgOwned: entry.bdgOwned };
1634
+ acceptTrustedDir(entry);
1635
+ }
1636
+ return null;
1637
+ }
1638
+ var log, SESSION_FILES, SESSION_DIR_OVERRIDE_ENV, SESSION_NAME_ENV, NAMED_SESSIONS_DIR, MAX_SOCKET_PATH_BYTES, MAX_DAEMON_SOCKET_PATH_BYTES, SESSION_STATE_FILES, PRIVATE_DIR_MODE, GROUP_OTHER_ACCESS, SESSION_DIR_TRUST;
1549
1639
  var init_paths = __esm({
1550
1640
  "src/session/paths.ts"() {
1551
1641
  "use strict";
1552
1642
  init_logging();
1553
1643
  init_directories();
1644
+ init_errors();
1554
1645
  log = createLogger("session");
1555
1646
  SESSION_FILES = {
1556
1647
  OUTPUT: "session.json",
@@ -1572,6 +1663,9 @@ var init_paths = __esm({
1572
1663
  "CHROME_PID",
1573
1664
  "METADATA"
1574
1665
  ];
1666
+ PRIVATE_DIR_MODE = 448;
1667
+ GROUP_OTHER_ACCESS = 63;
1668
+ SESSION_DIR_TRUST = { allowGroupWrite: true };
1575
1669
  }
1576
1670
  });
1577
1671
 
@@ -1653,14 +1747,17 @@ var init_atomicFile = __esm({
1653
1747
  *
1654
1748
  * @param filePath - Target file path
1655
1749
  * @param data - Data to write
1656
- * @param options - Write options
1750
+ * @param options - Write options (`mode`: permissions of the new file, less the umask)
1657
1751
  * @returns Promise that resolves when write completes
1658
1752
  * @throws Error if write operation fails
1659
1753
  */
1660
1754
  static async writeAsync(filePath, data, options = {}) {
1661
1755
  const tmpPath = this.getTempPath(filePath);
1662
1756
  try {
1663
- await fs4.promises.writeFile(tmpPath, data, { encoding: options.encoding ?? "utf-8" });
1757
+ await fs4.promises.writeFile(tmpPath, data, {
1758
+ encoding: options.encoding ?? "utf-8",
1759
+ ...options.mode !== void 0 && { mode: options.mode }
1760
+ });
1664
1761
  await fs4.promises.rename(tmpPath, filePath);
1665
1762
  } catch (error) {
1666
1763
  try {
@@ -2192,8 +2289,8 @@ function chromeClosedMessage(pid) {
2192
2289
  function orphanedDaemonsCleanedMessage(count) {
2193
2290
  return `Cleaned up ${count} orphaned daemon process${count === 1 ? "" : "es"}`;
2194
2291
  }
2195
- function skillBackupMessage(path7) {
2196
- return `previous copy kept in ${path7}`;
2292
+ function skillBackupMessage(path8) {
2293
+ return `previous copy kept in ${path8}`;
2197
2294
  }
2198
2295
  function fieldLabelList(labels) {
2199
2296
  const shown = labels.slice(0, 5).map((label) => label.replace(/\s*:\s*$/, "")).join(", ");
@@ -3120,6 +3217,7 @@ var init_shellDetection = __esm({
3120
3217
  });
3121
3218
 
3122
3219
  // src/errors/messages.ts
3220
+ import * as os3 from "os";
3123
3221
  import * as path4 from "path";
3124
3222
  function sessionAlreadyRunningError(pid, duration, targetUrl) {
3125
3223
  return joinLines(
@@ -3291,8 +3389,13 @@ function unknownHelpTopicError(topic, closest) {
3291
3389
  suggestion: closest ? `Did you mean: bdg help ${closest}?` : `Run "bdg ${parent ? `${parent} ` : ""}--help" for commands`
3292
3390
  };
3293
3391
  }
3294
- function usageHelpSuggestion(path7) {
3295
- return `Run "${path7} --help" for usage`;
3392
+ function didYouMeanSuggestion(candidates) {
3393
+ const last = candidates.at(-1) ?? "";
3394
+ const rest = candidates.slice(0, -1);
3395
+ return `Did you mean: ${rest.length > 0 ? `${rest.join(", ")} or ${last}` : last}?`;
3396
+ }
3397
+ function usageHelpSuggestion(path8) {
3398
+ return `Run "${path8} --help" for usage`;
3296
3399
  }
3297
3400
  function missingSubcommandMessage() {
3298
3401
  return "Missing subcommand";
@@ -3349,7 +3452,7 @@ function invalidSessionNameError(name, maxLength) {
3349
3452
  function sessionNameSocketTooLongError(name, socketPath, max) {
3350
3453
  return {
3351
3454
  message: `Session name "${name}" makes the daemon socket path too long (${Buffer.byteLength(socketPath)} bytes, at most ${max}): ${socketPath}`,
3352
- suggestion: "Use a shorter session name, or a shorter BDG_SESSION_DIR (e.g. /tmp/bdg)"
3455
+ suggestion: `Use a shorter session name, or a shorter BDG_SESSION_DIR (e.g. ${privateSessionDirExample()})`
3353
3456
  };
3354
3457
  }
3355
3458
  function purgeNeedsNamedSessionError() {
@@ -3364,19 +3467,52 @@ function purgeRefusedError(dir, reason) {
3364
3467
  suggestion: `End the session first (${sessionCommand("bdg cleanup --force")}), then retry: ${sessionCommand("bdg cleanup --purge")}`
3365
3468
  };
3366
3469
  }
3470
+ function privateSessionDirExample() {
3471
+ const runtimeDir = process.env["XDG_RUNTIME_DIR"]?.trim();
3472
+ if (runtimeDir) return path4.join(runtimeDir, "bdg");
3473
+ const uid = process.getuid?.();
3474
+ return path4.join(os3.tmpdir(), uid === void 0 ? "bdg" : `bdg-${uid}`);
3475
+ }
3476
+ function sessionDirSuggestion() {
3477
+ return `Set BDG_SESSION_DIR to a short directory only you can write to, e.g. BDG_SESSION_DIR=${privateSessionDirExample()}`;
3478
+ }
3367
3479
  function sessionDirIsFileError(dir) {
3368
- return { message: `Session directory ${dir} is a file`, suggestion: SESSION_DIR_SUGGESTION };
3480
+ return { message: `Session directory ${dir} is a file`, suggestion: sessionDirSuggestion() };
3369
3481
  }
3370
3482
  function sessionDirNotWritableError(dir, reason) {
3371
3483
  return {
3372
3484
  message: `Session directory ${dir} is not writable (${reason})`,
3373
- suggestion: SESSION_DIR_SUGGESTION
3485
+ suggestion: sessionDirSuggestion()
3486
+ };
3487
+ }
3488
+ function untrustedSessionDirError(untrusted) {
3489
+ return {
3490
+ message: `Session directory ${untrusted.dir} is not safe to use: ${untrusted.reason}`,
3491
+ suggestion: untrustedSessionDirSuggestion(untrusted)
3492
+ };
3493
+ }
3494
+ function untrustedSessionDirSuggestion(untrusted) {
3495
+ const { dir, kind, bdgOwned } = untrusted;
3496
+ const example = `BDG_SESSION_DIR=${privateSessionDirExample()}`;
3497
+ if (kind === "symlink") {
3498
+ return `Remove the link (rm ${dir}) or point BDG_SESSION_DIR at the real directory`;
3499
+ }
3500
+ if (kind === "shared") {
3501
+ return `${dir} is shared by all users; use a subdirectory only you can write to, e.g. ${example}`;
3502
+ }
3503
+ if (bdgOwned) return `Run chmod 700 ${dir} (or remove it if it is not yours), then retry`;
3504
+ return `Use a directory you own that others cannot write to, e.g. ${example} (or chmod 700 ${dir} if it is yours)`;
3505
+ }
3506
+ function daemonLogNotOpenedError(logPath, code) {
3507
+ return {
3508
+ message: `Cannot open the daemon log ${logPath} (${code === "ELOOP" ? "it is a symbolic link" : code})`,
3509
+ suggestion: `Remove ${logPath} if you did not create it, then retry`
3374
3510
  };
3375
3511
  }
3376
3512
  function socketPathTooLongError(socketPath, max) {
3377
3513
  return {
3378
3514
  message: `Session directory path is too long for the daemon socket (${Buffer.byteLength(socketPath)} bytes, at most ${max}): ${socketPath}`,
3379
- suggestion: SESSION_DIR_SUGGESTION
3515
+ suggestion: sessionDirSuggestion()
3380
3516
  };
3381
3517
  }
3382
3518
  function externalChromeUnreachableError(endpoint, secure) {
@@ -3715,10 +3851,10 @@ function skillSourceMissingError(source) {
3715
3851
  suggestion: "Reinstall bdg: npm install -g browser-debugger-cli"
3716
3852
  };
3717
3853
  }
3718
- function skillWriteFailedError(path7, reason, pathIsFile, otherTargetFlag) {
3854
+ function skillWriteFailedError(path8, reason, pathIsFile, otherTargetFlag) {
3719
3855
  const fix = pathIsFile ? "A part of that path is a file, not a directory: move it away" : "Check the permissions of that directory";
3720
3856
  return {
3721
- message: `Could not write ${path7}: ${reason}`,
3857
+ message: `Could not write ${path8}: ${reason}`,
3722
3858
  suggestion: otherTargetFlag ? `${fix}, or install for the other agent only (${otherTargetFlag})` : fix
3723
3859
  };
3724
3860
  }
@@ -3731,7 +3867,7 @@ function intervalWithoutFollowError() {
3731
3867
  function unknownAuditCheckMessage(value, similar, checks) {
3732
3868
  return `Unknown check "${value}"${similar.length > 0 ? `; did you mean ${similar[0]}?` : ""} (checks: ${checks.join(", ")})`;
3733
3869
  }
3734
- var LAUNCHED_CHROME_DESCRIPTION, A11Y_NAME_QUERY_EXAMPLE, SCOPED_FILTER_EXAMPLES, PLAYWRIGHT_ONLY_SYNTAX, MALFORMED_SELECTOR_DETAILS, SESSION_DIR_SUGGESTION, SCROLL_PROBLEMS, VIA_LABEL_SUFFIX, NAME_QUERY_PLACEHOLDER, LABEL_WITHOUT_CONTROL, REDECLARED_PATTERN;
3870
+ var LAUNCHED_CHROME_DESCRIPTION, A11Y_NAME_QUERY_EXAMPLE, SCOPED_FILTER_EXAMPLES, PLAYWRIGHT_ONLY_SYNTAX, MALFORMED_SELECTOR_DETAILS, SCROLL_PROBLEMS, VIA_LABEL_SUFFIX, NAME_QUERY_PLACEHOLDER, LABEL_WITHOUT_CONTROL, REDECLARED_PATTERN;
3735
3871
  var init_messages = __esm({
3736
3872
  "src/errors/messages.ts"() {
3737
3873
  "use strict";
@@ -3752,7 +3888,6 @@ var init_messages = __esm({
3752
3888
  "empty-has": () => ":has() has an empty selector",
3753
3889
  "scope-in-has": () => ":has() with filters is already relative to the element; write :has(> a:visible) instead of :has(:scope > a:visible)"
3754
3890
  };
3755
- SESSION_DIR_SUGGESTION = "Set BDG_SESSION_DIR to a short, writable directory, e.g. BDG_SESSION_DIR=/tmp/bdg";
3756
3891
  SCROLL_PROBLEMS = {
3757
3892
  "index-without-selector": [
3758
3893
  "--index requires a selector",
@@ -4075,7 +4210,17 @@ var init_validation2 = __esm({
4075
4210
  });
4076
4211
 
4077
4212
  // src/ipc/transport/index.ts
4213
+ import * as path5 from "path";
4078
4214
  async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath()) {
4215
+ const untrusted = secureSessionDir(path5.dirname(socketPath));
4216
+ if (untrusted) {
4217
+ const err = untrustedSessionDirError(untrusted);
4218
+ throw new CommandError(
4219
+ err.message,
4220
+ { suggestion: err.suggestion },
4221
+ EXIT_CODES.SESSION_FILE_ERROR
4222
+ );
4223
+ }
4079
4224
  return new Promise((resolve4, reject2) => {
4080
4225
  const buffer = new JSONLBuffer();
4081
4226
  let resolved = false;
@@ -4134,8 +4279,11 @@ var init_transport = __esm({
4134
4279
  "src/ipc/transport/index.ts"() {
4135
4280
  "use strict";
4136
4281
  init_constants();
4282
+ init_errors3();
4283
+ init_messages();
4137
4284
  init_paths();
4138
4285
  init_logging();
4286
+ init_exitCodes();
4139
4287
  init_errors4();
4140
4288
  init_jsonl();
4141
4289
  init_socket();
@@ -4311,8 +4459,12 @@ async function callCDP(method, params, options = {}) {
4311
4459
  function callBdgScript(method, params) {
4312
4460
  return callCDP(method, params, { isolated: true });
4313
4461
  }
4314
- async function domEval(script, frame) {
4315
- return sendCommand("dom_eval", { script, ...frame !== void 0 && { frame } });
4462
+ async function domEval(script, frame, full) {
4463
+ return sendCommand("dom_eval", {
4464
+ script,
4465
+ ...frame !== void 0 && { frame },
4466
+ ...full && { full }
4467
+ });
4316
4468
  }
4317
4469
  async function domFrames() {
4318
4470
  return sendCommand("dom_frames", {});
@@ -8109,7 +8261,7 @@ function textPreview2(text, length = PREVIEW_LENGTH) {
8109
8261
  const characters = Array.from(collapsed);
8110
8262
  return characters.length > length ? characters.slice(0, length).join("") + "..." : collapsed;
8111
8263
  }
8112
- var CLASS_FRAGMENT, LABEL_CLASSES_JS, PREVIEW_LENGTH, ELEMENT_TEXT_LENGTH, WITHOUT_DECORATIONS_JS, COMPOSED_JS, FLAT_TEXT_JS, ELEMENT_TEXT_JS, MASKED_VALUE, SENSITIVE_FIELD_JS, ELEMENT_STATE_JS, ELEMENT_DESCRIPTION_JS, SIBLING_POSITION_JS, ELEMENT_IDENTITY_JS, ELEMENT_CONTEXT_JS;
8264
+ var CLASS_FRAGMENT, LABEL_CLASSES_JS, PREVIEW_LENGTH, ELEMENT_TEXT_LENGTH, WITHOUT_DECORATIONS_JS, COMPOSED_JS, FLAT_TEXT_JS, ELEMENT_TEXT_JS, MASKED_VALUE, SENSITIVE_NAME_SOURCE, SENSITIVE_FIELD_JS, ELEMENT_STATE_JS, ELEMENT_DESCRIPTION_JS, SIBLING_POSITION_JS, ELEMENT_IDENTITY_JS, ELEMENT_CONTEXT_JS;
8113
8265
  var init_elementInfo = __esm({
8114
8266
  "src/runtime/dom/elementInfo.ts"() {
8115
8267
  "use strict";
@@ -8200,12 +8352,13 @@ var init_elementInfo = __esm({
8200
8352
  return withoutDecorations(el, start);
8201
8353
  }`;
8202
8354
  MASKED_VALUE = "\u2022\u2022\u2022\u2022";
8355
+ SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc";
8203
8356
  SENSITIVE_FIELD_JS = `(el) => {
8204
8357
  const autocomplete = el.getAttribute('autocomplete') || '';
8205
8358
  if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
8206
8359
  if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
8207
8360
  const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
8208
- if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
8361
+ if (/${SENSITIVE_NAME_SOURCE}/i.test(names)) return true;
8209
8362
  try {
8210
8363
  const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
8211
8364
  return Boolean(security) && security !== 'none';
@@ -8990,10 +9143,10 @@ function findHasFilters(text, selector) {
8990
9143
  }
8991
9144
  function parseHasArgument(argument, selector) {
8992
9145
  if (!FILTER_NAME_HINT.test(argument)) return null;
8993
- const selectors = splitSelectorList(argument).map((relative) => {
8994
- if (relative.trim() === "") reject(malformedSelectorError(selector, "empty-has"));
8995
- if (SCOPE_PSEUDO.test(relative)) reject(malformedSelectorError(selector, "scope-in-has"));
8996
- const { leading, steps } = parseChain(relative, selector);
9146
+ const selectors = splitSelectorList(argument).map((relative2) => {
9147
+ if (relative2.trim() === "") reject(malformedSelectorError(selector, "empty-has"));
9148
+ if (SCOPE_PSEUDO.test(relative2)) reject(malformedSelectorError(selector, "scope-in-has"));
9149
+ const { leading, steps } = parseChain(relative2, selector);
8997
9150
  if (leading && leading !== ">") reject(siblingInHasError(selector, leading));
8998
9151
  const [first, ...rest] = steps;
8999
9152
  return first ? [{ ...first, combinator: leading || " " }, ...rest] : [];
@@ -9761,7 +9914,7 @@ var init_query = __esm({
9761
9914
 
9762
9915
  // src/commands/shared/outputFile.ts
9763
9916
  import * as fs15 from "fs";
9764
- import * as path5 from "path";
9917
+ import * as path6 from "path";
9765
9918
  function outputPathError(filePath, error, extension2) {
9766
9919
  const code = error.code ?? "";
9767
9920
  const problem = PATH_PROBLEMS[code];
@@ -9790,13 +9943,16 @@ function assertFilePath(filePath, extension2) {
9790
9943
  );
9791
9944
  }
9792
9945
  }
9793
- async function writeOutputFile(filePath, data, extension2) {
9946
+ async function writeOutputFile(filePath, data, extension2, mode) {
9794
9947
  assertFilePath(filePath, extension2);
9795
- const absolutePath = path5.resolve(filePath);
9948
+ const absolutePath = path6.resolve(filePath);
9796
9949
  try {
9797
- makeDirectory(path5.dirname(absolutePath));
9798
- if (typeof data === "string") await AtomicFileWriter.writeAsync(absolutePath, data);
9799
- else await AtomicFileWriter.writeBufferAsync(absolutePath, data);
9950
+ makeDirectory(path6.dirname(absolutePath));
9951
+ if (typeof data === "string") {
9952
+ await AtomicFileWriter.writeAsync(absolutePath, data, mode === void 0 ? {} : { mode });
9953
+ } else {
9954
+ await AtomicFileWriter.writeBufferAsync(absolutePath, data);
9955
+ }
9800
9956
  } catch (error) {
9801
9957
  throw outputPathError(filePath, error, extension2);
9802
9958
  }
@@ -9830,10 +9986,13 @@ var init_outputFile = __esm({
9830
9986
  });
9831
9987
 
9832
9988
  // src/runtime/page/userAgent.ts
9989
+ var log24;
9833
9990
  var init_userAgent = __esm({
9834
9991
  "src/runtime/page/userAgent.ts"() {
9835
9992
  "use strict";
9993
+ init_logging();
9836
9994
  init_errors();
9995
+ log24 = createLogger("session");
9837
9996
  }
9838
9997
  });
9839
9998
 
@@ -9847,7 +10006,7 @@ function viewportOverride(viewport, deviceScaleFactor = 0) {
9847
10006
  mobile
9848
10007
  };
9849
10008
  }
9850
- var log24, MOBILE_PIXEL_RATIO;
10009
+ var log25, MOBILE_PIXEL_RATIO;
9851
10010
  var init_emulation = __esm({
9852
10011
  "src/runtime/page/emulation.ts"() {
9853
10012
  "use strict";
@@ -9855,7 +10014,7 @@ var init_emulation = __esm({
9855
10014
  init_userAgent();
9856
10015
  init_logging();
9857
10016
  init_errors();
9858
- log24 = createLogger("session");
10017
+ log25 = createLogger("session");
9859
10018
  MOBILE_PIXEL_RATIO = 3;
9860
10019
  }
9861
10020
  });
@@ -9920,12 +10079,12 @@ async function waitForPostScrollStability() {
9920
10079
  const networkIdle = value?.networkIdle ?? 0;
9921
10080
  const domIdle = value?.domIdle ?? 0;
9922
10081
  if (networkIdle >= POST_SCROLL_NETWORK_IDLE_MS && domIdle >= POST_SCROLL_DOM_STABLE_MS) {
9923
- log25.debug(`Post-scroll stable: network ${networkIdle}ms, DOM ${domIdle}ms`);
10082
+ log26.debug(`Post-scroll stable: network ${networkIdle}ms, DOM ${domIdle}ms`);
9924
10083
  return;
9925
10084
  }
9926
10085
  await new Promise((resolve4) => setTimeout(resolve4, STABILITY_CHECK_INTERVAL_MS));
9927
10086
  }
9928
- log25.debug("Post-scroll stability timeout, proceeding anyway");
10087
+ log26.debug("Post-scroll stability timeout, proceeding anyway");
9929
10088
  } finally {
9930
10089
  await callBdgScript("Runtime.evaluate", {
9931
10090
  expression: `
@@ -10224,7 +10383,7 @@ async function paintedArea(ref, bounds) {
10224
10383
  height: bounds.height + top + bottom
10225
10384
  };
10226
10385
  } catch (error) {
10227
- log25.debug(`Could not measure overflowing content: ${String(error)}`);
10386
+ log26.debug(`Could not measure overflowing content: ${String(error)}`);
10228
10387
  return bounds;
10229
10388
  } finally {
10230
10389
  await callCDP("Runtime.releaseObjectGroup", { objectGroup }).catch(() => void 0);
@@ -10321,7 +10480,7 @@ function roundBounds(bounds) {
10321
10480
  height: Math.round(bounds.height)
10322
10481
  };
10323
10482
  }
10324
- var log25, POST_SCROLL_NETWORK_IDLE_MS, POST_SCROLL_DOM_STABLE_MS, POST_SCROLL_MAX_WAIT_MS, STABILITY_CHECK_INTERVAL_MS, OVERFLOW_SCAN_LIMIT, CONTENT_OVERFLOW_JS, OVERFLOW_SLACK;
10483
+ var log26, POST_SCROLL_NETWORK_IDLE_MS, POST_SCROLL_DOM_STABLE_MS, POST_SCROLL_MAX_WAIT_MS, STABILITY_CHECK_INTERVAL_MS, OVERFLOW_SCAN_LIMIT, CONTENT_OVERFLOW_JS, OVERFLOW_SLACK;
10325
10484
  var init_screenshot = __esm({
10326
10485
  "src/commands/dom/helpers/screenshot.ts"() {
10327
10486
  "use strict";
@@ -10336,7 +10495,7 @@ var init_screenshot = __esm({
10336
10495
  init_metadata();
10337
10496
  init_logging();
10338
10497
  init_exitCodes();
10339
- log25 = createLogger("dom");
10498
+ log26 = createLogger("dom");
10340
10499
  POST_SCROLL_NETWORK_IDLE_MS = 150;
10341
10500
  POST_SCROLL_DOM_STABLE_MS = 200;
10342
10501
  POST_SCROLL_MAX_WAIT_MS = 2e3;
@@ -13537,9 +13696,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
13537
13696
  * @param {string} [path]
13538
13697
  * @return {(string|null|Command)}
13539
13698
  */
13540
- executableDir(path7) {
13541
- if (path7 === void 0) return this._executableDir;
13542
- this._executableDir = path7;
13699
+ executableDir(path8) {
13700
+ if (path8 === void 0) return this._executableDir;
13701
+ this._executableDir = path8;
13543
13702
  return this;
13544
13703
  }
13545
13704
  /**
@@ -13882,9 +14041,9 @@ var OPTION_BEHAVIORS = {
13882
14041
  automaticBehavior: "The value is matched as: a 0-based index (bdg dom frames order: document order of the <iframe> elements, nested ones depth-first, main page not counted), else an exact name/id attribute, else a case-insensitive part of the name, id or URL. Several matches fail with 81 listing them; none fails with 83 listing all frames. Frames are looked up on every call (a reloaded iframe is found again). An index that names another frame than in the last bdg dom frames listing (iframes added, removed or moved, or the page navigated) fails with 87 STALE_CACHE: re-run bdg dom frames or pick the frame by name."
13883
14042
  },
13884
14043
  "eval:--full": {
13885
- default: 'Human output prints the first 20000 characters of the value followed by "\u2026 N more chars (use --full)"; in JSON a string result is cut to 20000 characters with truncatedFrom (the original length). Objects and arrays in JSON are not cut',
13886
- whenEnabled: "Prints the whole value, byte for byte",
13887
- tokenImpact: "dom eval document.documentElement.outerHTML on Wikipedia is about 3.6 MB with --full; select what you need in the expression instead"
14044
+ default: 'Human output prints the first 20000 characters of the value followed by "\u2026 N more chars (use --full)". In JSON a string result is cut to 20000 characters with truncatedFrom (the original length); an array result lists its first 100 elements with count (all of them) and omitted; an object or array whose JSON is still over 20000 characters becomes the first 20000 characters of its JSON text (a string; arrays keep count) with truncatedFrom (the length of the JSON text of the copy, which holds at most 1000 entries per list or object): a string result with truncatedFrom while type is object is such a JSON-text start',
14045
+ whenEnabled: "Prints the whole value, byte for byte; objects and arrays are copied with every entry (without --full at most 1000 per list or object)",
14046
+ tokenImpact: 'dom eval document.documentElement.outerHTML on Wikipedia is about 3.6 MB with --full; on a page with 20000 elements, [...document.querySelectorAll("*")].map(e => e.outerHTML) --json is 3 MB with --full and 23 KB without; select what you need in the expression instead'
13888
14047
  },
13889
14048
  "console:--full": {
13890
14049
  default: 'Message texts are cut: human output (summary, --list, --follow) at 200 characters followed by "\u2026 N more chars (use --full)"; JSON text at 10000 characters with truncatedFrom (the original length)',
@@ -14111,6 +14270,11 @@ var OPTION_BEHAVIORS = {
14111
14270
  whenEnabled: "Streams requests as they finish",
14112
14271
  automaticBehavior: FOLLOW_BEHAVIOR
14113
14272
  },
14273
+ "har:--include-sensitive": {
14274
+ default: 'The HAR is sanitized: values become "[redacted]" for Authorization, Proxy-Authorization, Authentication, Cookie and Set-Cookie headers, X-*key/token/secret/auth headers and headers with an api-key/apikey/token/secret/jwt/subscription-key/session(-id) segment (www-authenticate is kept); every cookie value; query and fragment parameters named like credentials (plus code, sig, key) in the request URL, queryString, redirectURL and Location/Referer headers ("%5Bredacted%5D" in URLs); and password/token/secret/key/session/signature fields of JSON (primitives at any depth under such a name), form-urlencoded (also sniffed when the Content-Type says otherwise) and multipart request bodies. A JSON body too deep to walk becomes "[redacted]" whole. Header, cookie and parameter names, cookie attributes, headersSize and bodySize stay; log.comment and JSON sanitized: true say so',
14275
+ whenEnabled: "Writes every captured value (JSON sanitized: false); human output warns that the file holds credentials",
14276
+ automaticBehavior: "Matching is by name, so it over-redacts: harmless values under credential-looking names (tokenCount: 5, sessionLength) are replaced too, and credentials under other names are kept. Unlike Chrome DevTools, which drops these headers and empties cookies, names are kept so the HAR still shows a request was authenticated. Response bodies and WebSocket messages are not redacted. HAR files are written readable by their owner only (0600). network headers and network getCookies always show real values"
14277
+ },
14114
14278
  "peek:--verbose": {
14115
14279
  default: "Compact output (truncated URLs, no resource types)",
14116
14280
  whenEnabled: "Verbose output with full URLs and resource types"
@@ -14231,25 +14395,25 @@ var loggingBufferColumns = 25;
14231
14395
  var level_;
14232
14396
  var Log = class _Log {
14233
14397
  static _logToStdErr(title, argsArray) {
14234
- const log34 = _Log.loggerfn(title);
14235
- log34(...argsArray);
14398
+ const log36 = _Log.loggerfn(title);
14399
+ log36(...argsArray);
14236
14400
  }
14237
14401
  /**
14238
14402
  * @param {string} title
14239
14403
  */
14240
14404
  static loggerfn(title) {
14241
14405
  title = `LH:${title}`;
14242
- let log34 = loggersByTitle[title];
14243
- if (!log34) {
14244
- log34 = (0, import_debug.default)(title);
14245
- loggersByTitle[title] = log34;
14406
+ let log36 = loggersByTitle[title];
14407
+ if (!log36) {
14408
+ log36 = (0, import_debug.default)(title);
14409
+ loggersByTitle[title] = log36;
14246
14410
  if (title.endsWith("error")) {
14247
- log34.color = colors.red;
14411
+ log36.color = colors.red;
14248
14412
  } else if (title.endsWith("warn")) {
14249
- log34.color = colors.yellow;
14413
+ log36.color = colors.yellow;
14250
14414
  }
14251
14415
  }
14252
- return log34;
14416
+ return log36;
14253
14417
  }
14254
14418
  /**
14255
14419
  * @param {string} level
@@ -14846,12 +15010,12 @@ async function waitUntil(condition, timeoutMs, pollMs = 25) {
14846
15010
  // src/utils/file.ts
14847
15011
  init_logging();
14848
15012
  import * as fs9 from "fs";
14849
- function safeRemoveFile(path7, context, log34) {
15013
+ function safeRemoveFile(path8, context, log36) {
14850
15014
  try {
14851
- fs9.rmSync(path7, { force: true });
15015
+ fs9.rmSync(path8, { force: true });
14852
15016
  return true;
14853
15017
  } catch (error) {
14854
- logDebugError(log34, `remove ${context}`, error);
15018
+ logDebugError(log36, `remove ${context}`, error);
14855
15019
  return false;
14856
15020
  }
14857
15021
  }
@@ -15342,7 +15506,7 @@ function convertCompactCommand(command) {
15342
15506
  return {
15343
15507
  name: command.name(),
15344
15508
  ...aliases.length > 0 && { aliases },
15345
- description: command.description().split("\n")[0] ?? "",
15509
+ description: command.summary() || (command.description().split("\n")[0] ?? ""),
15346
15510
  ...args && { arguments: args },
15347
15511
  ...options.length > 0 && {
15348
15512
  options: Object.fromEntries(options.map((option) => [option.flags, option.description]))
@@ -15439,14 +15603,14 @@ init_errors3();
15439
15603
  init_messages();
15440
15604
  init_exitCodes();
15441
15605
  init_suggestions();
15442
- var COMMANDER_HINT = /\n?\(Did you mean (.+)\?\)\s*$/;
15606
+ var COMMANDER_HINT = /\n?\(Did you mean (?:one of )?(.+)\?\)\s*$/;
15443
15607
  function helpTopicPath(args) {
15444
15608
  const firstOption = args.findIndex((arg) => arg.startsWith("-"));
15445
15609
  return firstOption === -1 ? args : args.slice(0, firstOption);
15446
15610
  }
15447
15611
  function assertKnownHelpTopic(program2, topic) {
15448
15612
  let command = program2;
15449
- const path7 = [];
15613
+ const path8 = [];
15450
15614
  for (const word of topic) {
15451
15615
  if (command.commands.length === 0 || word === "help") return;
15452
15616
  const sub = command.commands.find((c) => c.name() === word || c.aliases().includes(word));
@@ -15456,8 +15620,8 @@ function assertKnownHelpTopic(program2, topic) {
15456
15620
  command.commands.map((c) => c.name())
15457
15621
  );
15458
15622
  const err = unknownHelpTopicError(
15459
- [...path7, word].join(" "),
15460
- closest && [...path7, closest].join(" ")
15623
+ [...path8, word].join(" "),
15624
+ closest && [...path8, closest].join(" ")
15461
15625
  );
15462
15626
  throw new CommandError(
15463
15627
  err.message,
@@ -15465,7 +15629,7 @@ function assertKnownHelpTopic(program2, topic) {
15465
15629
  EXIT_CODES.INVALID_ARGUMENTS
15466
15630
  );
15467
15631
  }
15468
- path7.push(sub.name());
15632
+ path8.push(sub.name());
15469
15633
  command = sub;
15470
15634
  }
15471
15635
  }
@@ -15473,7 +15637,10 @@ function splitCommanderHint(text) {
15473
15637
  const message = text.replace(/^error:\s*/i, "");
15474
15638
  const hint = COMMANDER_HINT.exec(message);
15475
15639
  if (!hint) return { message };
15476
- return { message: message.slice(0, hint.index).trim(), suggestion: `Did you mean: ${hint[1]}?` };
15640
+ return {
15641
+ message: message.slice(0, hint.index).trim(),
15642
+ suggestion: didYouMeanSuggestion((hint[1] ?? "").split(", "))
15643
+ };
15477
15644
  }
15478
15645
  var UNKNOWN_OPTION = /unknown option '([^'=]+)/;
15479
15646
  function closestOption(flag, command) {
@@ -15504,7 +15671,7 @@ function usageErrorDetails(error, command, argv = process.argv) {
15504
15671
 
15505
15672
  // src/commands/start.ts
15506
15673
  import fs11 from "node:fs";
15507
- import os3 from "node:os";
15674
+ import os4 from "node:os";
15508
15675
 
15509
15676
  // src/commands/shared/commonOptions.ts
15510
15677
  function jsonOption() {
@@ -15529,14 +15696,14 @@ init_messages();
15529
15696
 
15530
15697
  // src/utils/version.ts
15531
15698
  import { readFileSync as readFileSync6 } from "fs";
15532
- import { join as join5 } from "path";
15699
+ import { join as join6 } from "path";
15533
15700
  var cachedVersion = "";
15534
15701
  function getVersion() {
15535
15702
  if (cachedVersion) {
15536
15703
  return cachedVersion;
15537
15704
  }
15538
15705
  try {
15539
- const pkgPath = join5(PACKAGE_ROOT, "package.json");
15706
+ const pkgPath = join6(PACKAGE_ROOT, "package.json");
15540
15707
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
15541
15708
  cachedVersion = pkg.version ?? "0.0.0";
15542
15709
  } catch {
@@ -15792,6 +15959,9 @@ function lastSessionEndText(end) {
15792
15959
  function endedSessionText(label, end) {
15793
15960
  return `${label} ended ${sessionEndText(end)}`;
15794
15961
  }
15962
+ function untrustedSessionText(label, why) {
15963
+ return `${label}: ${why}`;
15964
+ }
15795
15965
  function sessionEndText(end) {
15796
15966
  const why = {
15797
15967
  crash: "Chrome crashed or was closed",
@@ -15849,7 +16019,7 @@ var SessionDirError = class extends DaemonError {
15849
16019
  // src/daemon/launcher.ts
15850
16020
  import { spawn } from "child_process";
15851
16021
  import fs10 from "fs";
15852
- import { join as join6 } from "path";
16022
+ import { join as join7 } from "path";
15853
16023
  init_messages();
15854
16024
  init_paths();
15855
16025
  init_logging();
@@ -15860,6 +16030,7 @@ var log8 = createLogger("launcher");
15860
16030
  var DAEMON_READY_TIMEOUT_MS = 5e3;
15861
16031
  var DAEMON_READY_POLL_MS = 20;
15862
16032
  async function launchDaemon() {
16033
+ assertUsableSessionDir();
15863
16034
  if (await isDaemonAlive()) {
15864
16035
  log8.debug("Daemon already running");
15865
16036
  return void 0;
@@ -15870,11 +16041,10 @@ async function launchDaemon() {
15870
16041
  "DAEMON_SCRIPT_NOT_FOUND"
15871
16042
  );
15872
16043
  }
15873
- assertUsableSessionDir();
15874
16044
  await stopUnreachableDaemon();
15875
- const logPath = join6(getSessionDir(), "daemon.log");
16045
+ const logPath = join7(getSessionDir(), "daemon.log");
15876
16046
  rotateLog(logPath);
15877
- const logFd = fs10.openSync(logPath, "a");
16047
+ const logFd = openDaemonLog(logPath);
15878
16048
  log8.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
15879
16049
  const daemon = spawn(process.execPath, [DAEMON_SCRIPT_PATH], {
15880
16050
  detached: true,
@@ -15930,6 +16100,8 @@ function assertUsableSessionDir() {
15930
16100
  problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR
15931
16101
  );
15932
16102
  }
16103
+ const untrusted = secureSessionDir(dir);
16104
+ if (untrusted) fail(untrustedSessionDirError(untrusted));
15933
16105
  try {
15934
16106
  ensureSessionDir();
15935
16107
  fs10.accessSync(dir, fs10.constants.W_OK);
@@ -15942,6 +16114,18 @@ function assertUsableSessionDir() {
15942
16114
  );
15943
16115
  }
15944
16116
  }
16117
+ var DAEMON_LOG_FLAGS = fs10.constants.O_WRONLY | fs10.constants.O_APPEND | fs10.constants.O_CREAT | (fs10.constants.O_NOFOLLOW ?? 0);
16118
+ function openDaemonLog(logPath) {
16119
+ try {
16120
+ return fs10.openSync(logPath, DAEMON_LOG_FLAGS, 384);
16121
+ } catch (error) {
16122
+ const err = daemonLogNotOpenedError(
16123
+ logPath,
16124
+ error.code ?? getErrorMessage(error)
16125
+ );
16126
+ throw new SessionDirError(err.message, err.suggestion);
16127
+ }
16128
+ }
15945
16129
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
15946
16130
  function rotateLog(logPath) {
15947
16131
  try {
@@ -15961,7 +16145,7 @@ async function waitForDaemonReady(hasExited) {
15961
16145
  }
15962
16146
  if (hasExited() && !await isDaemonAlive()) {
15963
16147
  throw new DaemonStartupError(
15964
- `Daemon exited during startup. See ${join6(getSessionDir(), "daemon.log")}`,
16148
+ `Daemon exited during startup. See ${join7(getSessionDir(), "daemon.log")}`,
15965
16149
  "DAEMON_EXITED"
15966
16150
  );
15967
16151
  }
@@ -16568,7 +16752,7 @@ async function probeDevToolsEndpoint(port, logger, options) {
16568
16752
  init_suggestions();
16569
16753
  init_url();
16570
16754
  function expandHome(value) {
16571
- return value.startsWith("~/") ? value.replace(/^~/, os3.homedir()) : value;
16755
+ return value.startsWith("~/") ? value.replace(/^~/, os4.homedir()) : value;
16572
16756
  }
16573
16757
  function extractUserDataDirFromFlags(flags) {
16574
16758
  const rest = [];
@@ -17244,7 +17428,7 @@ function getMethodHint(methodName, result) {
17244
17428
  return void 0;
17245
17429
  }
17246
17430
  function registerCdpCommand(program2) {
17247
- program2.command("cdp").description(
17431
+ program2.command("cdp").summary("CDP protocol introspection and execution").description(
17248
17432
  "CDP protocol introspection and execution\n Discovery: --list, --search, --describe\n Execution: case-insensitive (network.getcookies works)"
17249
17433
  ).argument("[method]", "CDP method name (e.g., Network.getCookies, network.getcookies)").addOption(new Option("--params <json>", "Method parameters as JSON")).addOption(
17250
17434
  new Option("--list", "List all domains or methods in a domain").conflicts([
@@ -18742,6 +18926,15 @@ function headerRepeatedNote(count) {
18742
18926
  function localProxyNote() {
18743
18927
  return "(loopback; likely a local proxy)";
18744
18928
  }
18929
+ function harSanitizedComment() {
18930
+ return "Sanitized by bdg: values of auth, cookie, API key, token and session headers, cookies, credential query parameters in URLs, and password/token fields of request bodies are [redacted] (by name, so some harmless values are too); response bodies and WebSocket messages are not sanitized. Export with --include-sensitive to keep everything";
18931
+ }
18932
+ function harExportedMessage(result) {
18933
+ const filterNote = result.filtered ? " (filtered)" : "";
18934
+ const note = result.sanitized ? "Credentials sanitized (auth/cookie/API key/token headers, cookies, URL tokens, password and token body fields are [redacted]); --include-sensitive keeps them" : "\u26A0 Includes credentials (--include-sensitive): share this file with care";
18935
+ return `\u2713 Exported ${result.entries} requests${filterNote} to ${result.file}
18936
+ ${note}`;
18937
+ }
18745
18938
 
18746
18939
  // src/telemetry/networkRetention.ts
18747
18940
  var SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
@@ -19912,9 +20105,9 @@ init_url();
19912
20105
  var MAX_VALUE_LENGTH2 = 40;
19913
20106
  function fileName(src) {
19914
20107
  if (/^(data|blob):/i.test(src)) return src;
19915
- const path7 = src.split(/[?#]/)[0] ?? src;
19916
- const name = path7.substring(path7.lastIndexOf("/") + 1);
19917
- return name && name !== path7 ? `\u2026/${name}` : src;
20108
+ const path8 = src.split(/[?#]/)[0] ?? src;
20109
+ const name = path8.substring(path8.lastIndexOf("/") + 1);
20110
+ return name && name !== path8 ? `\u2026/${name}` : src;
19918
20111
  }
19919
20112
  function hostOf(src) {
19920
20113
  const parsed = /^https?:/i.test(src) ? safeParseUrl(src) : null;
@@ -20043,7 +20236,7 @@ init_elementInfo();
20043
20236
  init_frameScopedConnection();
20044
20237
  init_logging();
20045
20238
  init_errors();
20046
- var log26 = createLogger("dom");
20239
+ var log27 = createLogger("dom");
20047
20240
  function childFrameIds(tree) {
20048
20241
  return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrameIds(child)]);
20049
20242
  }
@@ -20765,9 +20958,34 @@ async function audit(checks, options) {
20765
20958
  return { success: true, data: response.data };
20766
20959
  }
20767
20960
 
20961
+ // src/commands/dom/helpers/evalResult.ts
20962
+ init_constants();
20963
+ init_strings();
20964
+ function boundEvalResult(value, length) {
20965
+ if (typeof value === "string") {
20966
+ const { text, truncatedFrom } = capLength(value, MAX_VALUE_LENGTH);
20967
+ return { result: text, ...truncatedFrom !== void 0 && { truncatedFrom } };
20968
+ }
20969
+ if (Array.isArray(value)) return boundArray(value, length ?? value.length);
20970
+ if (value === null || typeof value !== "object") return { result: value };
20971
+ const json = JSON.stringify(value);
20972
+ return json.length > MAX_VALUE_LENGTH ? jsonStart(json) : { result: value };
20973
+ }
20974
+ function boundArray(value, count) {
20975
+ const listed = value.slice(0, EVAL_JSON_ARRAY_LIMIT);
20976
+ const listedJson = JSON.stringify(listed);
20977
+ if (listedJson.length > MAX_VALUE_LENGTH) {
20978
+ const json = listed.length === value.length ? listedJson : JSON.stringify(value);
20979
+ return { ...jsonStart(json), count };
20980
+ }
20981
+ return listed.length < count ? { result: listed, count, omitted: count - listed.length } : { result: value };
20982
+ }
20983
+ function jsonStart(json) {
20984
+ return { result: capLength(json, MAX_VALUE_LENGTH).text, truncatedFrom: json.length };
20985
+ }
20986
+
20768
20987
  // src/commands/dom/eval.ts
20769
20988
  init_query();
20770
- init_constants();
20771
20989
  init_messages();
20772
20990
  init_client();
20773
20991
 
@@ -20861,7 +21079,6 @@ function formatDomScreenshot(data) {
20861
21079
  // src/commands/dom/eval.ts
20862
21080
  init_commands();
20863
21081
  init_exitCodes();
20864
- init_strings();
20865
21082
  async function handleDomEval(script, options) {
20866
21083
  await runCommand(
20867
21084
  async () => {
@@ -20874,7 +21091,7 @@ async function handleDomEval(script, options) {
20874
21091
  errorContext: { suggestion: err.suggestion }
20875
21092
  };
20876
21093
  }
20877
- const response = await domEval(script, options.frame);
21094
+ const response = await domEval(script, options.frame, options.full);
20878
21095
  if (response.status === "error" || !response.data) {
20879
21096
  const suggestion = await frameErrorSuggestion(response, options.frame);
20880
21097
  return {
@@ -20884,7 +21101,7 @@ async function handleDomEval(script, options) {
20884
21101
  ...suggestion && { errorContext: { suggestion } }
20885
21102
  };
20886
21103
  }
20887
- const { value, type, subtype, frame, warning } = response.data;
21104
+ const { value, type, subtype, length, frame, warning } = response.data;
20888
21105
  const hint = [
20889
21106
  ...frame !== void 0 ? [evalFrameLine(frame)] : [],
20890
21107
  ...warning ? [warningMessage(warning)] : []
@@ -20892,7 +21109,7 @@ async function handleDomEval(script, options) {
20892
21109
  return {
20893
21110
  success: true,
20894
21111
  data: {
20895
- ...jsonResult(value, options),
21112
+ ...options.json && !options.full ? boundEvalResult(value, length) : { result: value },
20896
21113
  type,
20897
21114
  ...subtype && { subtype },
20898
21115
  ...frame !== void 0 && { frame },
@@ -20905,11 +21122,6 @@ async function handleDomEval(script, options) {
20905
21122
  (data) => formatDomEval(data, { full: options.full })
20906
21123
  );
20907
21124
  }
20908
- function jsonResult(value, options) {
20909
- if (!options.json || options.full || typeof value !== "string") return { result: value };
20910
- const { text, truncatedFrom } = capLength(value, MAX_VALUE_LENGTH);
20911
- return { result: text, ...truncatedFrom !== void 0 && { truncatedFrom } };
20912
- }
20913
21125
  async function frameErrorSuggestion(response, frame) {
20914
21126
  if (frame === void 0 || response.exitCode !== EXIT_CODES.RESOURCE_NOT_FOUND) {
20915
21127
  return response.suggestion;
@@ -21227,7 +21439,7 @@ function formatFormDiscovery(result) {
21227
21439
  init_logging();
21228
21440
  init_errors();
21229
21441
  init_exitCodes();
21230
- var log27 = createLogger("dom");
21442
+ var log28 = createLogger("dom");
21231
21443
  function buildValidation(raw) {
21232
21444
  const hasNativeError = !raw.isValid && !raw.valueMissing && raw.validationMessage;
21233
21445
  const hasAriaError = raw.ariaInvalid;
@@ -21384,7 +21596,7 @@ async function cacheFormElements(forms, document2) {
21384
21596
  const elements = forms.flatMap((form) => [...form.fields, ...form.buttons]);
21385
21597
  const backendNodeIds = await resolveBackendNodeIds(elements.map((el) => el.selector)).catch(
21386
21598
  (error) => {
21387
- log27.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
21599
+ log28.debug(`Form fields not cached by node: ${getErrorMessage(error)}`);
21388
21600
  return [];
21389
21601
  }
21390
21602
  );
@@ -21400,7 +21612,7 @@ async function cacheFormElements(forms, document2) {
21400
21612
  },
21401
21613
  document2
21402
21614
  );
21403
- log27.debug(`Cached ${elements.length} form elements`);
21615
+ log28.debug(`Cached ${elements.length} form elements`);
21404
21616
  }
21405
21617
  async function handleFormCommand(options) {
21406
21618
  await runCommand(
@@ -22386,7 +22598,7 @@ init_logging();
22386
22598
  init_directories();
22387
22599
  init_errors();
22388
22600
  init_exitCodes();
22389
- var log28 = createLogger("dom");
22601
+ var log29 = createLogger("dom");
22390
22602
  var EXTENSION_FORMATS = {
22391
22603
  ".png": "png",
22392
22604
  ".jpg": "jpeg",
@@ -22512,8 +22724,8 @@ async function captureSequenceFrame(outputPath, options) {
22512
22724
  }
22513
22725
  async function handleSequenceCapture(outputDir, options) {
22514
22726
  const fs19 = await import("fs");
22515
- const path7 = await import("path");
22516
- const absoluteDir = path7.resolve(outputDir);
22727
+ const path8 = await import("path");
22728
+ const absoluteDir = path8.resolve(outputDir);
22517
22729
  ensureDirectory(absoluteDir, fs19);
22518
22730
  const intervalRule = positiveIntRule({ name: "--interval", min: 100, max: 6e4, default: 1e3 });
22519
22731
  const limitRule = positiveIntRule({ name: "--limit", min: 1, max: 1e4, required: false });
@@ -22529,7 +22741,7 @@ async function handleSequenceCapture(outputDir, options) {
22529
22741
  while (!stopping) {
22530
22742
  frameCount++;
22531
22743
  const filename = formatFrameFilename(frameCount, format);
22532
- const outputPath = path7.join(absoluteDir, filename);
22744
+ const outputPath = path8.join(absoluteDir, filename);
22533
22745
  try {
22534
22746
  await captureSequenceFrame(outputPath, options);
22535
22747
  } catch (error) {
@@ -22538,7 +22750,7 @@ async function handleSequenceCapture(outputDir, options) {
22538
22750
  if (options.json) {
22539
22751
  console.log(JSON.stringify(buildSuccessResponse({ frame: frameCount, path: outputPath })));
22540
22752
  } else {
22541
- log28.info(`Frame ${frameCount}: ${filename}`);
22753
+ log29.info(`Frame ${frameCount}: ${filename}`);
22542
22754
  }
22543
22755
  if (limit > 0 && frameCount >= limit) break;
22544
22756
  await delay2(interval);
@@ -22721,8 +22933,8 @@ function registerDomCommands(program2) {
22721
22933
  "Image format: png or jpeg/jpg (default: from the file extension, else png)",
22722
22934
  screenshotFormatOption
22723
22935
  ).option("--quality <number>", "JPEG quality 0-100 (default: 90)", integerOption(0, 100)).option("--no-full-page", "Capture viewport only (default: full page)").option("--no-resize", "Disable auto-resize (full resolution)").option("--scroll <selector>", "Scroll element into view before capture").option("-f, --follow", "Continuous capture mode to directory").option("--interval <ms>", "Capture interval for --follow (default: 1000)").option("--limit <count>", "Max frames for --follow").option("-j, --json", "Output as JSON").action(
22724
- async (path7, target, options) => {
22725
- await handleDomScreenshot(path7, target, options);
22936
+ async (path8, target, options) => {
22937
+ await handleDomScreenshot(path8, target, options);
22726
22938
  }
22727
22939
  );
22728
22940
  }
@@ -22730,7 +22942,7 @@ function registerDomCommands(program2) {
22730
22942
  // src/commands/installSkill.ts
22731
22943
  import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync6, writeFileSync as writeFileSync2 } from "fs";
22732
22944
  import { homedir as homedir3 } from "os";
22733
- import { dirname as dirname6, join as join7 } from "path";
22945
+ import { dirname as dirname7, join as join8 } from "path";
22734
22946
  init_errors3();
22735
22947
  init_messages();
22736
22948
 
@@ -22739,9 +22951,9 @@ init_formatting();
22739
22951
  init_commands();
22740
22952
  import { homedir as homedir2 } from "os";
22741
22953
  var PATH_INDENT = " ".repeat(2 + 6 + 2 + 9 + 2);
22742
- function tildePath(path7) {
22954
+ function tildePath(path8) {
22743
22955
  const home = homedir2();
22744
- return path7.startsWith(`${home}/`) ? `~${path7.slice(home.length)}` : path7;
22956
+ return path8.startsWith(`${home}/`) ? `~${path8.slice(home.length)}` : path8;
22745
22957
  }
22746
22958
  function addSkillTargets(fmt, skills) {
22747
22959
  fmt.text("bdg skill:");
@@ -22765,11 +22977,11 @@ function formatInstalledSkills(data) {
22765
22977
  init_logging();
22766
22978
  init_errors();
22767
22979
  init_exitCodes();
22768
- var log29 = createLogger("bdg");
22769
- var SKILL_SOURCE_PATH = join7(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
22980
+ var log30 = createLogger("bdg");
22981
+ var SKILL_SOURCE_PATH = join8(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
22770
22982
  var SKILL_ROOTS = {
22771
- claude: join7(".claude", "skills"),
22772
- agents: join7(".agents", "skills")
22983
+ claude: join8(".claude", "skills"),
22984
+ agents: join8(".agents", "skills")
22773
22985
  };
22774
22986
  function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
22775
22987
  if (!existsSync11(source)) {
@@ -22784,7 +22996,7 @@ function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
22784
22996
  const skills = [];
22785
22997
  const failures = [];
22786
22998
  for (const target of targets) {
22787
- const result = writeSkill(target, join7(home, SKILL_ROOTS[target], "bdg", "SKILL.md"), content);
22999
+ const result = writeSkill(target, join8(home, SKILL_ROOTS[target], "bdg", "SKILL.md"), content);
22788
23000
  if ("error" in result) failures.push(result);
22789
23001
  else skills.push(result);
22790
23002
  }
@@ -22793,10 +23005,10 @@ function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
22793
23005
  }
22794
23006
  function skillFailureError(failures, targets) {
22795
23007
  const other = failures.length === 1 ? targets.find((t) => t !== failures[0]?.target) : void 0;
22796
- const errors = failures.map(({ path: path7, error }) => {
23008
+ const errors = failures.map(({ path: path8, error }) => {
22797
23009
  const code = error.code;
22798
23010
  return skillWriteFailedError(
22799
- path7,
23011
+ path8,
22800
23012
  getErrorMessage(error),
22801
23013
  code === "ENOTDIR" || code === "EEXIST",
22802
23014
  other && `--${other}`
@@ -22808,30 +23020,30 @@ function skillFailureError(failures, targets) {
22808
23020
  EXIT_CODES.PERMISSION_DENIED
22809
23021
  );
22810
23022
  }
22811
- function writeSkill(target, path7, content) {
22812
- const existing = existsSync11(path7) ? readFileSync8(path7, "utf-8") : void 0;
23023
+ function writeSkill(target, path8, content) {
23024
+ const existing = existsSync11(path8) ? readFileSync8(path8, "utf-8") : void 0;
22813
23025
  if (existing === content) {
22814
- return { target, path: path7, status: "unchanged" };
23026
+ return { target, path: path8, status: "unchanged" };
22815
23027
  }
22816
- const backup = existing === void 0 ? void 0 : `${path7}.bak`;
22817
- const temporary = `${path7}.tmp`;
22818
- let writing = path7;
23028
+ const backup = existing === void 0 ? void 0 : `${path8}.bak`;
23029
+ const temporary = `${path8}.tmp`;
23030
+ let writing = path8;
22819
23031
  try {
22820
- mkdirSync2(dirname6(path7), { recursive: true });
23032
+ mkdirSync2(dirname7(path8), { recursive: true });
22821
23033
  writeFileSync2(temporary, content);
22822
23034
  if (backup !== void 0) {
22823
23035
  writing = backup;
22824
23036
  rmSync6(backup, { force: true });
22825
23037
  writeFileSync2(backup, existing ?? "");
22826
23038
  }
22827
- writing = path7;
22828
- renameSync2(temporary, path7);
23039
+ writing = path8;
23040
+ renameSync2(temporary, path8);
22829
23041
  } catch (error) {
22830
- safeRemoveFile(temporary, "temporary skill copy", log29);
23042
+ safeRemoveFile(temporary, "temporary skill copy", log30);
22831
23043
  return { target, path: writing, error };
22832
23044
  }
22833
- if (backup === void 0) return { target, path: path7, status: "installed" };
22834
- return { target, path: path7, status: "updated", backup };
23045
+ if (backup === void 0) return { target, path: path8, status: "installed" };
23046
+ return { target, path: path8, status: "updated", backup };
22835
23047
  }
22836
23048
  function selectedTargets(options) {
22837
23049
  const picked = Object.keys(SKILL_ROOTS).filter((target) => options[target]);
@@ -22862,7 +23074,7 @@ function registerInstallSkillCommand(program2) {
22862
23074
 
22863
23075
  // src/commands/network/har.ts
22864
23076
  import * as fs16 from "fs";
22865
- import * as path6 from "path";
23077
+ import * as path7 from "path";
22866
23078
  init_outputFile();
22867
23079
  init_client();
22868
23080
  init_paths();
@@ -22982,7 +23194,7 @@ function filterByResourceType(requests, types) {
22982
23194
  init_logging();
22983
23195
  init_suggestions();
22984
23196
  init_url();
22985
- var log30 = createLogger("network");
23197
+ var log31 = createLogger("network");
22986
23198
  var SIZE_UNITS = {
22987
23199
  b: 1,
22988
23200
  kb: 1024,
@@ -23092,7 +23304,7 @@ function validateSizeValue(value) {
23092
23304
  parseSize(value);
23093
23305
  return null;
23094
23306
  } catch (error) {
23095
- log30.debug(
23307
+ log31.debug(
23096
23308
  `Size parse failed for "${value}": ${error instanceof Error ? error.message : String(error)}`
23097
23309
  );
23098
23310
  return createError(
@@ -23278,27 +23490,179 @@ Multiple filters:
23278
23490
 
23279
23491
  // src/telemetry/har/builder.ts
23280
23492
  import { createRequire as createRequire2 } from "node:module";
23493
+
23494
+ // src/telemetry/har/sanitizeBody.ts
23495
+ init_elementInfo();
23496
+ init_logging();
23497
+ init_errors();
23498
+ var log32 = createLogger("network");
23499
+ var REDACTED = "[redacted]";
23500
+ var SENSITIVE_FIELD = new RegExp(
23501
+ `${SENSITIVE_NAME_SOURCE}|token|secret|api[-_]?key|authoriz|credential|jwt|private[-_]?key|access[-_]?key|session|signature`,
23502
+ "i"
23503
+ );
23504
+ var FORM_BODY = /^[^\s=&{["]+=[^\s&]*(?:&[^\s=&]+=[^\s&]*)*$/;
23505
+ function isSensitiveField2(name) {
23506
+ return SENSITIVE_FIELD.test(name);
23507
+ }
23508
+ function redactPairs(text, isSensitive, replacement) {
23509
+ return text.split("&").map((pair) => {
23510
+ const eq = pair.indexOf("=");
23511
+ if (eq === -1 || !isSensitive(decodeName(pair.slice(0, eq)))) return pair;
23512
+ return `${pair.slice(0, eq)}=${replacement}`;
23513
+ }).join("&");
23514
+ }
23515
+ function redactRequestBody(text, mimeType) {
23516
+ if (/multipart\/form-data/i.test(mimeType)) return redactMultipart(text, mimeType);
23517
+ if (/x-www-form-urlencoded/i.test(mimeType) || FORM_BODY.test(text)) {
23518
+ return redactPairs(text, isSensitiveField2, REDACTED);
23519
+ }
23520
+ return redactJsonBody(text);
23521
+ }
23522
+ function decodeName(name) {
23523
+ try {
23524
+ return decodeURIComponent(name.replace(/\+/g, " "));
23525
+ } catch (error) {
23526
+ log32.debug(`Field name not decodable: ${getErrorMessage(error)}`);
23527
+ return name;
23528
+ }
23529
+ }
23530
+ function redactMultipart(text, mimeType) {
23531
+ const match = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(mimeType);
23532
+ const boundary = match?.[1] ?? match?.[2];
23533
+ if (!boundary) return text;
23534
+ const delimiter = `--${boundary}`;
23535
+ return text.split(delimiter).map(redactPart).join(delimiter);
23536
+ }
23537
+ function redactPart(part) {
23538
+ const headerEnd = part.indexOf("\r\n\r\n");
23539
+ if (headerEnd === -1) return part;
23540
+ const name = /;\s*name="([^"]*)"/i.exec(part.slice(0, headerEnd))?.[1];
23541
+ if (name === void 0 || !isSensitiveField2(name)) return part;
23542
+ const valueEnd = part.endsWith("\r\n") ? part.length - 2 : part.length;
23543
+ return `${part.slice(0, headerEnd + 4)}${REDACTED}${part.slice(valueEnd)}`;
23544
+ }
23545
+ function redactJsonBody(text) {
23546
+ if (!/^\s*[[{]/.test(text)) return text;
23547
+ let parsed;
23548
+ try {
23549
+ parsed = JSON.parse(text);
23550
+ } catch (error) {
23551
+ log32.debug(`Request body not JSON: ${getErrorMessage(error)}`);
23552
+ return error instanceof SyntaxError ? text : REDACTED;
23553
+ }
23554
+ try {
23555
+ const hits = { count: 0 };
23556
+ const redacted = redactJson(parsed, false, hits);
23557
+ return hits.count > 0 ? JSON.stringify(redacted) : text;
23558
+ } catch (error) {
23559
+ log32.debug(`Request body redacted whole: ${getErrorMessage(error)}`);
23560
+ return REDACTED;
23561
+ }
23562
+ }
23563
+ function redactJson(value, sensitive, hits) {
23564
+ if (Array.isArray(value)) return value.map((item) => redactJson(item, sensitive, hits));
23565
+ if (value !== null && typeof value === "object") {
23566
+ return Object.fromEntries(
23567
+ Object.entries(value).map(([key, field]) => [
23568
+ key,
23569
+ redactJson(field, sensitive || isSensitiveField2(key), hits)
23570
+ ])
23571
+ );
23572
+ }
23573
+ if (!sensitive || !["string", "number", "boolean"].includes(typeof value)) return value;
23574
+ hits.count++;
23575
+ return REDACTED;
23576
+ }
23577
+
23578
+ // src/telemetry/har/sanitize.ts
23579
+ var URL_REDACTED = encodeURIComponent(REDACTED);
23580
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
23581
+ "authorization",
23582
+ "proxy-authorization",
23583
+ "authentication",
23584
+ "cookie",
23585
+ "set-cookie"
23586
+ ]);
23587
+ var SENSITIVE_CUSTOM_HEADER = /^x-.*(key|token|secret|auth)/i;
23588
+ var SENSITIVE_HEADER_SEGMENT = /(^|-)(api-?key|apikey|token|secret|jwt|subscription-key|session(-?id)?)(-|$)/i;
23589
+ var URL_HEADERS = /* @__PURE__ */ new Set(["location", "referer"]);
23590
+ var SENSITIVE_URL_PARAM = /^(code|sig|key)$/i;
23591
+ function sanitizeEntry(entry) {
23592
+ const { request, response } = entry;
23593
+ const postData = request.postData;
23594
+ return {
23595
+ ...entry,
23596
+ request: {
23597
+ ...request,
23598
+ url: redactUrl(request.url),
23599
+ cookies: request.cookies.map(redactCookie),
23600
+ headers: request.headers.map(redactHeader),
23601
+ queryString: request.queryString.map(redactQueryParam),
23602
+ ...postData?.text !== void 0 && {
23603
+ postData: { ...postData, text: redactRequestBody(postData.text, postData.mimeType) }
23604
+ }
23605
+ },
23606
+ response: {
23607
+ ...response,
23608
+ cookies: response.cookies.map(redactCookie),
23609
+ headers: response.headers.map(redactHeader),
23610
+ redirectURL: redactUrl(response.redirectURL)
23611
+ }
23612
+ };
23613
+ }
23614
+ function isSensitiveHeader(name) {
23615
+ return SENSITIVE_HEADERS.has(name.toLowerCase()) || SENSITIVE_CUSTOM_HEADER.test(name) || SENSITIVE_HEADER_SEGMENT.test(name);
23616
+ }
23617
+ function redactHeader(header) {
23618
+ if (isSensitiveHeader(header.name)) return { ...header, value: REDACTED };
23619
+ if (!URL_HEADERS.has(header.name.toLowerCase())) return header;
23620
+ const value = redactUrl(header.value);
23621
+ return value === header.value ? header : { ...header, value };
23622
+ }
23623
+ function redactCookie(cookie) {
23624
+ return { ...cookie, value: REDACTED };
23625
+ }
23626
+ function isSensitiveParam(name) {
23627
+ return isSensitiveField2(name) || SENSITIVE_URL_PARAM.test(name);
23628
+ }
23629
+ function redactQueryParam(param) {
23630
+ return isSensitiveParam(param.name) ? { ...param, value: REDACTED } : param;
23631
+ }
23632
+ function redactUrl(url) {
23633
+ const hash = url.indexOf("#");
23634
+ const beforeHash = hash === -1 ? url : url.slice(0, hash);
23635
+ const fragment = hash === -1 ? "" : `#${redactPairs(url.slice(hash + 1), isSensitiveParam, URL_REDACTED)}`;
23636
+ const question = beforeHash.indexOf("?");
23637
+ if (question === -1) return beforeHash + fragment;
23638
+ const query = redactPairs(beforeHash.slice(question + 1), isSensitiveParam, URL_REDACTED);
23639
+ return `${beforeHash.slice(0, question)}?${query}${fragment}`;
23640
+ }
23641
+
23642
+ // src/telemetry/har/builder.ts
23281
23643
  var requireBuiltin = createRequire2(import.meta.url);
23282
23644
  var UNKNOWN_TIMING = -1;
23283
23645
  var DEFAULT_HTTP_VERSION = "HTTP/1.1";
23284
- function buildHAR(requests, metadata) {
23285
- const entries = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
23286
- const log34 = {
23646
+ function buildHAR(requests, metadata, options = {}) {
23647
+ const built = [...requests].sort((a, b) => a.timestamp - b.timestamp).map(buildEntry);
23648
+ const entries = options.includeSensitive ? built : built.map(sanitizeEntry);
23649
+ const log36 = {
23287
23650
  version: "1.2",
23288
23651
  creator: {
23289
23652
  name: "bdg",
23290
23653
  version: metadata.version,
23291
23654
  comment: "Browser Debugger CLI - https://github.com/szymdzum/browser-debugger-cli"
23292
23655
  },
23293
- entries
23656
+ entries,
23657
+ ...!options.includeSensitive && { comment: harSanitizedComment() }
23294
23658
  };
23295
23659
  if (metadata.chromeVersion) {
23296
- log34.browser = {
23660
+ log36.browser = {
23297
23661
  name: "Chrome",
23298
23662
  version: metadata.chromeVersion
23299
23663
  };
23300
23664
  }
23301
- return { log: log34 };
23665
+ return { log: log36 };
23302
23666
  }
23303
23667
  function buildEntry(req) {
23304
23668
  const timings = buildTimings(req);
@@ -23509,8 +23873,8 @@ function extractRedirectURL(req) {
23509
23873
  }
23510
23874
  function estimateRequestHeadersSize(method, url, headers) {
23511
23875
  const parsedUrl = new URL(url);
23512
- const path7 = parsedUrl.pathname + parsedUrl.search;
23513
- const requestLine = `${method} ${path7} ${DEFAULT_HTTP_VERSION}\r
23876
+ const path8 = parsedUrl.pathname + parsedUrl.search;
23877
+ const requestLine = `${method} ${path8} ${DEFAULT_HTTP_VERSION}\r
23514
23878
  `;
23515
23879
  const headersString = headers ? Object.entries(headers).map(([name, value]) => `${name}: ${value}\r
23516
23880
  `).join("") : "";
@@ -24073,7 +24437,7 @@ function registerDocumentCommand(networkCmd) {
24073
24437
  }
24074
24438
 
24075
24439
  // src/commands/network/har.ts
24076
- var log31 = createLogger("network");
24440
+ var log33 = createLogger("network");
24077
24441
  function generateHARFilename() {
24078
24442
  const now = /* @__PURE__ */ new Date();
24079
24443
  const year = now.getFullYear();
@@ -24083,10 +24447,10 @@ function generateHARFilename() {
24083
24447
  const minutes = String(now.getMinutes()).padStart(2, "0");
24084
24448
  const seconds = String(now.getSeconds()).padStart(2, "0");
24085
24449
  const base = `capture-${year}-${month}-${day}-${hours}${minutes}${seconds}`;
24086
- const sessionDir = path6.dirname(getSessionFilePath("OUTPUT"));
24087
- let candidate = path6.join(sessionDir, `${base}.har`);
24450
+ const sessionDir = path7.dirname(getSessionFilePath("OUTPUT"));
24451
+ let candidate = path7.join(sessionDir, `${base}.har`);
24088
24452
  for (let n = 2; fs16.existsSync(candidate); n++) {
24089
- candidate = path6.join(sessionDir, `${base}-${n}.har`);
24453
+ candidate = path7.join(sessionDir, `${base}-${n}.har`);
24090
24454
  }
24091
24455
  return candidate;
24092
24456
  }
@@ -24096,22 +24460,26 @@ async function getChromeVersion() {
24096
24460
  const product = response.data?.result?.product;
24097
24461
  return product?.split("/")[1];
24098
24462
  } catch (error) {
24099
- log31.debug(`Could not read Chrome version: ${getErrorMessage(error)}`);
24463
+ log33.debug(`Could not read Chrome version: ${getErrorMessage(error)}`);
24100
24464
  return void 0;
24101
24465
  }
24102
24466
  }
24103
24467
  function formatHARExport(data) {
24104
24468
  if ("log" in data) return JSON.stringify(data, null, 2);
24105
- const filterNote = data.filtered ? " (filtered)" : "";
24106
- return `\u2713 Exported ${data.entries} requests${filterNote} to ${data.file}`;
24469
+ return harExportedMessage(data);
24107
24470
  }
24471
+ var HAR_FILE_MODE = 384;
24108
24472
  var STDOUT_PATH = "-";
24109
24473
  var filterDslOption = new Option(
24110
24474
  "--filter <dsl>",
24111
24475
  'Filter requests using DevTools DSL (e.g., "status-code:>=400")'
24112
24476
  );
24477
+ var includeSensitiveOption = new Option(
24478
+ "--include-sensitive",
24479
+ "Keep credentials (auth/cookie/API key headers, cookie values, URL tokens, password and token body fields); redacted by default"
24480
+ );
24113
24481
  function registerHarCommand(networkCmd) {
24114
- networkCmd.command("har [output-file]").addHelpText("after", "\nUse - as the output file to write the HAR to stdout.").description("Export network data as HAR 1.2 format").addOption(jsonOption()).addOption(filterDslOption).action(async (outputFile, options) => {
24482
+ networkCmd.command("har [output-file]").addHelpText("after", "\nUse - as the output file to write the HAR to stdout.").description("Export network data as HAR 1.2 format").addOption(jsonOption()).addOption(filterDslOption).addOption(includeSensitiveOption).action(async (outputFile, options) => {
24115
24483
  await runCommand(
24116
24484
  async () => {
24117
24485
  let requests = await getNetworkRequests();
@@ -24126,20 +24494,26 @@ function registerHarCommand(networkCmd) {
24126
24494
  const outputPath = outputFile ?? generateHARFilename();
24127
24495
  if (outputPath !== STDOUT_PATH) assertFilePath(outputPath, ".har");
24128
24496
  const chromeVersion = await getChromeVersion();
24129
- const har = buildHAR(requests, {
24130
- version: VERSION,
24131
- ...chromeVersion && { chromeVersion }
24132
- });
24497
+ const includeSensitive = options.includeSensitive === true;
24498
+ const har = buildHAR(
24499
+ requests,
24500
+ { version: VERSION, ...chromeVersion && { chromeVersion } },
24501
+ { includeSensitive }
24502
+ );
24133
24503
  if (outputPath === STDOUT_PATH) return { success: true, data: har };
24134
- const file = await writeOutputFile(outputPath, JSON.stringify(har, null, 2), ".har");
24135
- return {
24136
- success: true,
24137
- data: {
24138
- file,
24139
- entries: har.log.entries.length,
24140
- filtered
24141
- }
24504
+ const file = await writeOutputFile(
24505
+ outputPath,
24506
+ JSON.stringify(har, null, 2),
24507
+ ".har",
24508
+ HAR_FILE_MODE
24509
+ );
24510
+ const data = {
24511
+ file,
24512
+ entries: har.log.entries.length,
24513
+ filtered,
24514
+ sanitized: !includeSensitive
24142
24515
  };
24516
+ return { success: true, data };
24143
24517
  },
24144
24518
  options,
24145
24519
  formatHARExport
@@ -24898,13 +25272,13 @@ import * as fs17 from "fs";
24898
25272
  init_paths();
24899
25273
  init_logging();
24900
25274
  init_directories();
24901
- var log32 = createLogger("session");
25275
+ var log34 = createLogger("session");
24902
25276
  function readPortFile(portPath) {
24903
25277
  try {
24904
25278
  const port = parseInt(fs17.readFileSync(portPath, "utf-8").trim(), 10);
24905
25279
  return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
24906
25280
  } catch (error) {
24907
- logDebugError(log32, `read ${portPath}`, error);
25281
+ logDebugError(log34, `read ${portPath}`, error);
24908
25282
  return null;
24909
25283
  }
24910
25284
  }
@@ -24956,7 +25330,7 @@ function selectSession(optionValue) {
24956
25330
  // src/session/sessionList.ts
24957
25331
  init_logging();
24958
25332
  init_sessionCommand();
24959
- var log33 = createLogger("session");
25333
+ var log35 = createLogger("session");
24960
25334
  function toRunningSession(name, data) {
24961
25335
  const meta = data.sessionMetadata;
24962
25336
  const state = meta ? "active" : data.ending ? "ending" : "starting";
@@ -24975,11 +25349,15 @@ async function describeSession(entry) {
24975
25349
  const socketPath = sessionFilePathIn(dir, "DAEMON_SOCKET");
24976
25350
  const probe = await probeDaemonSocket(socketPath);
24977
25351
  if (probe !== "alive") return describeWithoutSocket(entry, probe);
25352
+ const untrusted = secureSessionDir(dir);
25353
+ if (untrusted) {
25354
+ return { name, state: "untrusted", untrusted: `${untrusted.dir}: ${untrusted.reason}` };
25355
+ }
24978
25356
  try {
24979
25357
  const response = await getStatus(socketPath);
24980
25358
  if (response.status === "ok" && response.data) return toRunningSession(name, response.data);
24981
25359
  } catch (error) {
24982
- logDebugError(log33, `get status of ${dir}`, error);
25360
+ logDebugError(log35, `get status of ${dir}`, error);
24983
25361
  }
24984
25362
  return { name, state: "unresponsive" };
24985
25363
  }
@@ -25018,7 +25396,7 @@ function leftoverPort(dir) {
25018
25396
  const meta = JSON.parse(fs18.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
25019
25397
  if (typeof meta.port === "number") return meta.port;
25020
25398
  } catch (error) {
25021
- logDebugError(log33, `read the metadata in ${dir}`, error);
25399
+ logDebugError(log35, `read the metadata in ${dir}`, error);
25022
25400
  }
25023
25401
  return readPortFile(sessionFilePathIn(dir, "PORT"));
25024
25402
  }
@@ -25031,7 +25409,7 @@ function isSameDir(a, b) {
25031
25409
  const second = fs18.statSync(b);
25032
25410
  return first.ino === second.ino && first.dev === second.dev;
25033
25411
  } catch (error) {
25034
- logDebugError(log33, `compare ${a} with ${b}`, error);
25412
+ logDebugError(log35, `compare ${a} with ${b}`, error);
25035
25413
  return false;
25036
25414
  }
25037
25415
  }
@@ -25090,6 +25468,15 @@ function formatSessionList(data) {
25090
25468
  if (ended.length > 0) {
25091
25469
  fmt.blank().section("Ended without bdg stop:", ended);
25092
25470
  }
25471
+ const untrusted = data.sessions.flatMap(
25472
+ ({ name, untrusted: why }) => why ? [untrustedSessionText(name ?? DEFAULT_SESSION_LABEL, why)] : []
25473
+ );
25474
+ if (untrusted.length > 0) {
25475
+ fmt.blank().section(
25476
+ "Directory not safe to use (not asked; bdg status --session <name> says how to fix it):",
25477
+ untrusted
25478
+ );
25479
+ }
25093
25480
  const cleanups = data.sessions.flatMap((session) => session.cleanup ? [session.cleanup] : []);
25094
25481
  if (cleanups.length > 0) {
25095
25482
  fmt.hints("Crashed or stale sessions; clean up with:", cleanups);
@@ -25207,6 +25594,7 @@ function registerStatusCommand(program2) {
25207
25594
  }
25208
25595
  return { success: true, data: withSessionName(jsonOutput) };
25209
25596
  } catch (error) {
25597
+ if (error instanceof CommandError) throw error;
25210
25598
  const errorMessage = getErrorMessage(error);
25211
25599
  if (error instanceof IPCTimeoutError) {
25212
25600
  const err = sessionNotRespondingError(error.timeoutMs / 1e3);
@@ -25255,6 +25643,7 @@ function registerStatusCommand(program2) {
25255
25643
  }
25256
25644
 
25257
25645
  // src/commands/stop.ts
25646
+ init_errors3();
25258
25647
  init_client();
25259
25648
  init_transport();
25260
25649
  init_formatting();
@@ -25325,7 +25714,7 @@ function registerStopCommand(program2) {
25325
25714
  };
25326
25715
  }
25327
25716
  } catch (error) {
25328
- if (error instanceof IPCTimeoutError) throw error;
25717
+ if (error instanceof IPCTimeoutError || error instanceof CommandError) throw error;
25329
25718
  const errorMessage = getErrorMessage(error);
25330
25719
  if (isDaemonNotRunningError(errorMessage)) {
25331
25720
  return {