@saptools/cf-inspector 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Built so an AI agent (or a CI job) can drive a debugger from a single shell comm
22
22
  - ✅ **Conditional breakpoints** — `--condition 'req.userId === "abc"'` only pauses when the predicate is truthy
23
23
  - 🎭 **Multi-breakpoint** — repeat `--bp` to race several locations; first hit wins
24
24
  - 📡 **Non-pausing logpoints** — `cf-inspector log --at file:line --expr 'JSON.stringify({…})'` streams JSON Lines as the line executes, **without ever pausing the inspectee** (safe for production traffic)
25
- - 🧠 **Agent-friendly** — JSON-by-default I/O, deterministic shape, sensitive-name redaction (`password`, `credentials`, `token`, `secret`, `cookie`, …) baked in
25
+ - 🧠 **Agent-friendly** — JSON-by-default I/O, deterministic shape, bounded value previews for large debugger payloads
26
26
  - 🧭 **Path mapping** — local `src/handler.ts:42` is matched against the remote URL via a `urlRegex`, with optional `--remote-root` literal or regex (same DSL as `cds-debug`)
27
27
  - 🔁 **Composes with `cf-debugger`** — pass `--app/--region/--org/--space` and the tunnel is opened automatically; pass `--port` to attach to anything CDP-speaking
28
28
  - 🪶 **Tiny dependency footprint** — `commander` + `ws` only, no heavy CDP framework
@@ -111,6 +111,7 @@ cf-inspector snapshot --port 9229 \
111
111
  | `--condition <expr>` | Only pause when this JS expression evaluates truthy in the paused frame. Errors in the condition are silently treated as `false` by V8 |
112
112
  | `--capture <expr,…>` | Top-level comma-separated expressions to evaluate in the paused frame; nested commas inside objects, arrays, calls, or strings are preserved. Object results are materialized to JSON strings when serializable, with fallback to CDP descriptions for non-serializable values |
113
113
  | `--timeout <seconds>` | How long to wait for the breakpoint to hit (default: `30`) |
114
+ | `--max-value-length <chars>` | Maximum characters per captured value before truncation (default: `4096`) |
114
115
  | `--remote-root <value>` | Optional path-mapping anchor: literal path or `regex:<pattern>` / `/pattern/flags` |
115
116
  | `--include-scopes` | Include expanded paused-frame scopes under `topFrame.scopes`. Omitted by default to keep targeted captures concise |
116
117
  | `--no-json` | Print a human-readable summary instead of JSON |
@@ -120,7 +121,9 @@ cf-inspector snapshot --port 9229 \
120
121
  JSON output includes frame metadata and `captures` by default. `topFrame.scopes`
121
122
  is only present with `--include-scopes`, because Cloud Foundry Node apps often
122
123
  carry large local/closure/module objects that drown out targeted captures. The
123
- output also includes `pausedDurationMs`, the client-observed time from receiving
124
+ output contains raw debugger values; use it only against trusted targets and be
125
+ careful when sharing logs. The output also includes `pausedDurationMs`, the
126
+ client-observed time from receiving
124
127
  the matching pause event until `Debugger.resume` completes. It does not include
125
128
  the time spent waiting for the breakpoint to hit. When `--keep-paused` is used,
126
129
  `pausedDurationMs` is `null` because `cf-inspector` intentionally skips
@@ -219,6 +222,7 @@ const bp = await setBreakpoint(session, {
219
222
  const pause = await waitForPause(session, { timeoutMs: 30_000 });
220
223
  const snapshot = await captureSnapshot(session, pause, {
221
224
  captures: ["this.user"],
225
+ maxValueLength: 4096,
222
226
  });
223
227
  const topFrame = pause.callFrames[0];
224
228
  if (topFrame === undefined) {
@@ -240,7 +244,7 @@ console.log({ bp, snapshot, customValue });
240
244
  | `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root |
241
245
  | `removeBreakpoint(session, id)` | Remove a breakpoint by id |
242
246
  | `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires |
243
- | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes |
247
+ | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes or `maxValueLength` to override the default captured value limit |
244
248
  | `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
245
249
  | `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
246
250
  | `listScripts(session)` | Return the scripts the V8 instance knows about |
package/dist/cli.js CHANGED
@@ -1067,12 +1067,12 @@ async function waitForStop(session, options) {
1067
1067
  }
1068
1068
 
1069
1069
  // src/snapshot.ts
1070
+ init_types();
1070
1071
  var MAX_SCOPES = 3;
1071
1072
  var MAX_SCOPE_VARIABLES = 20;
1072
1073
  var MAX_CHILD_VARIABLES = 8;
1073
1074
  var MAX_VARIABLE_DEPTH = 2;
1074
- var MAX_VALUE_LENGTH = 240;
1075
- var SENSITIVE_NAME_REGEX = /(pass(?:word)?|credentials?|creds?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
1075
+ var DEFAULT_MAX_VALUE_LENGTH = 4096;
1076
1076
  var PRIORITY_BY_TYPE = {
1077
1077
  local: 0,
1078
1078
  arguments: 1,
@@ -1133,37 +1133,43 @@ function formatPrimitive(value) {
1133
1133
  }
1134
1134
  return String(value);
1135
1135
  }
1136
- function isSensitiveName(name) {
1137
- return SENSITIVE_NAME_REGEX.test(name);
1138
- }
1139
- function sanitizeValue(name, raw) {
1140
- if (isSensitiveName(name)) {
1141
- return "[REDACTED]";
1136
+ function resolveMaxValueLength(value) {
1137
+ if (value === void 0) {
1138
+ return DEFAULT_MAX_VALUE_LENGTH;
1139
+ }
1140
+ if (!Number.isInteger(value) || value <= 0) {
1141
+ throw new CfInspectorError(
1142
+ "INVALID_ARGUMENT",
1143
+ `Invalid maxValueLength: ${value.toString()} \u2014 expected a positive integer`
1144
+ );
1142
1145
  }
1143
- if (raw.length <= MAX_VALUE_LENGTH) {
1146
+ return value;
1147
+ }
1148
+ function limitValueLength(raw, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1149
+ if (raw.length <= maxValueLength) {
1144
1150
  return raw;
1145
1151
  }
1146
- return `${raw.slice(0, MAX_VALUE_LENGTH)}...`;
1152
+ return `${raw.slice(0, maxValueLength)}...`;
1147
1153
  }
1148
1154
  function isExpandable(type) {
1149
1155
  return type === "object" || type === "function";
1150
1156
  }
1151
- async function captureProperties(session, objectId, limit, depth) {
1157
+ async function captureProperties(session, objectId, limit, depth, maxValueLength) {
1152
1158
  const properties = await getProperties(session, objectId);
1153
1159
  const limited = properties.slice(0, limit);
1154
1160
  const variables = await Promise.all(
1155
1161
  limited.map(async (prop) => {
1156
1162
  const name = typeof prop.name === "string" ? prop.name : "?";
1157
1163
  const described = describeProperty(prop);
1158
- const sensitive = isSensitiveName(name);
1159
1164
  let children;
1160
- if (!sensitive && depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1165
+ if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1161
1166
  try {
1162
1167
  const nested = await captureProperties(
1163
1168
  session,
1164
1169
  described.objectId,
1165
1170
  MAX_CHILD_VARIABLES,
1166
- depth - 1
1171
+ depth - 1,
1172
+ maxValueLength
1167
1173
  );
1168
1174
  if (nested.length > 0) {
1169
1175
  children = nested;
@@ -1171,7 +1177,7 @@ async function captureProperties(session, objectId, limit, depth) {
1171
1177
  } catch {
1172
1178
  }
1173
1179
  }
1174
- const sanitizedValue = sensitive ? "[REDACTED]" : sanitizeValue(name, described.value);
1180
+ const sanitizedValue = limitValueLength(described.value, maxValueLength);
1175
1181
  const base = { name, value: sanitizedValue };
1176
1182
  const withType = described.type === void 0 ? base : { ...base, type: described.type };
1177
1183
  return children === void 0 ? withType : { ...withType, children };
@@ -1186,7 +1192,7 @@ function selectScopes(scopeChain) {
1186
1192
  function priorityOf(type) {
1187
1193
  return PRIORITY_BY_TYPE[type] ?? Number.MAX_SAFE_INTEGER;
1188
1194
  }
1189
- async function captureScopes(session, frame) {
1195
+ async function captureScopes(session, frame, maxValueLength) {
1190
1196
  const scopes = selectScopes(frame.scopeChain);
1191
1197
  return await Promise.all(
1192
1198
  scopes.map(async (scope) => {
@@ -1195,7 +1201,13 @@ async function captureScopes(session, frame) {
1195
1201
  return { type: scope.type, variables: [] };
1196
1202
  }
1197
1203
  try {
1198
- const variables = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
1204
+ const variables = await captureProperties(
1205
+ session,
1206
+ objectId,
1207
+ MAX_SCOPE_VARIABLES,
1208
+ MAX_VARIABLE_DEPTH,
1209
+ maxValueLength
1210
+ );
1199
1211
  return { type: scope.type, variables };
1200
1212
  } catch {
1201
1213
  return { type: scope.type, variables: [] };
@@ -1203,10 +1215,10 @@ async function captureScopes(session, frame) {
1203
1215
  })
1204
1216
  );
1205
1217
  }
1206
- function evalResultToCaptured(expression, result) {
1218
+ function evalResultToCaptured(expression, result, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1207
1219
  if (result.exceptionDetails !== void 0) {
1208
1220
  const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1209
- return { expression, error: sanitizeValue(expression, text) };
1221
+ return { expression, error: limitValueLength(text, maxValueLength) };
1210
1222
  }
1211
1223
  const inner = result.result;
1212
1224
  if (!inner) {
@@ -1214,7 +1226,7 @@ function evalResultToCaptured(expression, result) {
1214
1226
  }
1215
1227
  const type = typeof inner.type === "string" ? inner.type : void 0;
1216
1228
  const buildCaptured = (rendered) => {
1217
- const sanitized = sanitizeValue(expression, rendered);
1229
+ const sanitized = limitValueLength(rendered, maxValueLength);
1218
1230
  const base = { expression, value: sanitized };
1219
1231
  return type === void 0 ? base : { ...base, type };
1220
1232
  };
@@ -1309,9 +1321,15 @@ function toStructuredValue(variable) {
1309
1321
  }
1310
1322
  return out;
1311
1323
  }
1312
- async function renderObjectCapture(session, objectId) {
1324
+ async function renderObjectCapture(session, objectId, maxValueLength) {
1313
1325
  try {
1314
- const properties = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
1326
+ const properties = await captureProperties(
1327
+ session,
1328
+ objectId,
1329
+ MAX_SCOPE_VARIABLES,
1330
+ MAX_VARIABLE_DEPTH,
1331
+ maxValueLength
1332
+ );
1315
1333
  const structured = {};
1316
1334
  for (const variable of properties) {
1317
1335
  structured[variable.name] = toStructuredValue(variable);
@@ -1330,7 +1348,7 @@ function normalizeRenderedObjectCapture(rendered, original) {
1330
1348
  }
1331
1349
  return rendered;
1332
1350
  }
1333
- async function withSerializedObjectCapture(session, expression, evalResult, captured) {
1351
+ async function withSerializedObjectCapture(session, expression, evalResult, captured, maxValueLength) {
1334
1352
  if (captured.error !== void 0 || captured.value === void 0) {
1335
1353
  return captured;
1336
1354
  }
@@ -1338,7 +1356,7 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1338
1356
  if (objectId === void 0) {
1339
1357
  return captured;
1340
1358
  }
1341
- const rendered = await renderObjectCapture(session, objectId);
1359
+ const rendered = await renderObjectCapture(session, objectId, maxValueLength);
1342
1360
  if (rendered === void 0) {
1343
1361
  return captured;
1344
1362
  }
@@ -1346,10 +1364,11 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1346
1364
  if (normalized === void 0) {
1347
1365
  return captured;
1348
1366
  }
1349
- const value = sanitizeValue(expression, normalized);
1367
+ const value = limitValueLength(normalized, maxValueLength);
1350
1368
  return captured.type === void 0 ? { expression, value } : { expression, value, type: captured.type };
1351
1369
  }
1352
1370
  async function captureSnapshot(session, pause, options = {}) {
1371
+ const maxValueLength = resolveMaxValueLength(options.maxValueLength);
1353
1372
  const top = pause.callFrames[0];
1354
1373
  let topFrame;
1355
1374
  let captures = [];
@@ -1361,7 +1380,7 @@ async function captureSnapshot(session, pause, options = {}) {
1361
1380
  column: top.columnNumber + 1
1362
1381
  };
1363
1382
  if (options.includeScopes === true) {
1364
- const scopes = await captureScopes(session, top);
1383
+ const scopes = await captureScopes(session, top, maxValueLength);
1365
1384
  topFrame = { ...topFrame, scopes };
1366
1385
  }
1367
1386
  if (options.captures !== void 0 && options.captures.length > 0) {
@@ -1369,11 +1388,17 @@ async function captureSnapshot(session, pause, options = {}) {
1369
1388
  options.captures.map(async (expression) => {
1370
1389
  try {
1371
1390
  const result = await evaluateOnFrame(session, top.callFrameId, expression);
1372
- const captured = evalResultToCaptured(expression, result);
1373
- return await withSerializedObjectCapture(session, expression, result, captured);
1391
+ const captured = evalResultToCaptured(expression, result, maxValueLength);
1392
+ return await withSerializedObjectCapture(
1393
+ session,
1394
+ expression,
1395
+ result,
1396
+ captured,
1397
+ maxValueLength
1398
+ );
1374
1399
  } catch (err) {
1375
1400
  const message = err instanceof Error ? err.message : String(err);
1376
- return { expression, error: message };
1401
+ return { expression, error: limitValueLength(message, maxValueLength) };
1377
1402
  }
1378
1403
  })
1379
1404
  );
@@ -1651,6 +1676,7 @@ async function handleSnapshot(opts) {
1651
1676
  const remoteRoot = parseRemoteRoot(opts.remoteRoot);
1652
1677
  const captures = parseCaptureList(opts.capture);
1653
1678
  const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
1679
+ const maxValueLength = parsePositiveInt(opts.maxValueLength, "--max-value-length");
1654
1680
  const timeoutMs = timeoutSec * 1e3;
1655
1681
  const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
1656
1682
  const result = await withSession(target, async (session) => {
@@ -1685,7 +1711,8 @@ async function handleSnapshot(opts) {
1685
1711
  const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
1686
1712
  const snapshot = await captureSnapshot(session, pause, {
1687
1713
  captures,
1688
- includeScopes: opts.includeScopes === true
1714
+ includeScopes: opts.includeScopes === true,
1715
+ ...maxValueLength === void 0 ? {} : { maxValueLength }
1689
1716
  });
1690
1717
  if (opts.keepPaused === true) {
1691
1718
  return withPausedDuration(snapshot, null);
@@ -1856,7 +1883,7 @@ async function main(argv) {
1856
1883
  "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
1857
1884
  collectStrings,
1858
1885
  []
1859
- ).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
1886
+ ).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
1860
1887
  "--condition <expr>",
1861
1888
  "Only pause when this JS expression evaluates truthy in the paused frame"
1862
1889
  ).option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--fail-on-unmatched-pause", "Fail immediately if the target pauses somewhere else").action(async (opts) => {