@saptools/cf-inspector 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DebuggerHandle } from '@saptools/cf-debugger';
2
2
 
3
- type CfInspectorErrorCode = "INVALID_ARGUMENT" | "INVALID_BREAKPOINT" | "INVALID_REMOTE_ROOT" | "INVALID_EXPRESSION" | "BREAKPOINT_DID_NOT_BIND" | "INSPECTOR_DISCOVERY_FAILED" | "INSPECTOR_CONNECTION_FAILED" | "CDP_REQUEST_FAILED" | "BREAKPOINT_NOT_HIT" | "EVALUATION_FAILED" | "MISSING_TARGET" | "ABORTED";
3
+ type CfInspectorErrorCode = "INVALID_ARGUMENT" | "INVALID_BREAKPOINT" | "INVALID_REMOTE_ROOT" | "INVALID_EXPRESSION" | "BREAKPOINT_DID_NOT_BIND" | "INSPECTOR_DISCOVERY_FAILED" | "INSPECTOR_CONNECTION_FAILED" | "CDP_REQUEST_FAILED" | "BREAKPOINT_NOT_HIT" | "UNRELATED_PAUSE" | "UNRELATED_PAUSE_TIMEOUT" | "EVALUATION_FAILED" | "MISSING_TARGET" | "ABORTED";
4
4
  declare class CfInspectorError extends Error {
5
5
  readonly code: CfInspectorErrorCode;
6
6
  readonly detail?: string;
@@ -54,6 +54,7 @@ interface PauseEvent {
54
54
  readonly reason: string;
55
55
  readonly hitBreakpoints: readonly string[];
56
56
  readonly callFrames: readonly CallFrameInfo[];
57
+ readonly receivedAtMs?: number;
57
58
  }
58
59
  interface VariableSnapshot {
59
60
  readonly name: string;
@@ -172,12 +173,16 @@ interface InspectorVersion {
172
173
  interface PauseWaitGate {
173
174
  active: boolean;
174
175
  }
176
+ interface DebuggerState {
177
+ lastResumedAtMs?: number;
178
+ }
175
179
  interface InspectorSession {
176
180
  readonly client: CdpClient;
177
181
  readonly target: InspectorTarget;
178
182
  readonly scripts: ReadonlyMap<string, ScriptInfo>;
179
183
  readonly pauseBuffer: PauseEvent[];
180
184
  readonly pauseWaitGate: PauseWaitGate;
185
+ readonly debuggerState: DebuggerState;
181
186
  dispose(): Promise<void>;
182
187
  }
183
188
  interface CdpEvalResult {
@@ -206,6 +211,8 @@ declare function removeBreakpoint(session: InspectorSession, breakpointId: strin
206
211
  interface WaitForPauseOptions {
207
212
  readonly timeoutMs: number;
208
213
  readonly breakpointIds?: readonly string[];
214
+ readonly unmatchedPausePolicy?: "wait-for-resume" | "fail";
215
+ readonly onUnmatchedPause?: (pause: PauseEvent) => void;
209
216
  }
210
217
  declare function waitForPause(session: InspectorSession, options: WaitForPauseOptions): Promise<PauseEvent>;
211
218
  declare function resume(session: InspectorSession): Promise<void>;
@@ -298,4 +305,4 @@ interface OpenedTunnel {
298
305
  }
299
306
  declare function openCfTunnel(target: TunnelTarget): Promise<OpenedTunnel>;
300
307
 
301
- export { type BreakpointHandle, type BreakpointLocation, type CallFrameInfo, type CaptureSnapshotOptions, type CapturedExpression, CfInspectorError, type CfInspectorErrorCode, type FrameSnapshot, type InspectorConnectOptions, type InspectorSession, type InspectorTarget, type LogpointEvent, type LogpointStreamOptions, type LogpointStreamResult, type OpenedTunnel, type PauseEvent, type RemoteRootSetting, type ResolvedLocation, type ScopeInfo, type ScopeSnapshot, type ScriptInfo, type SetBreakpointInput, type SnapshotCaptureResult, type SnapshotResult, type TunnelTarget, type VariableSnapshot, type WaitForPauseOptions, buildBreakpointUrlRegex, buildLogpointCondition, captureSnapshot, connectInspector, discoverInspectorTargets, evaluateGlobal, evaluateOnFrame, fetchInspectorVersion, getProperties, listScripts, openCfTunnel, parseBreakpointSpec, parseRemoteRoot, removeBreakpoint, resume, setBreakpoint, streamLogpoint, validateExpression, waitForPause };
308
+ export { type BreakpointHandle, type BreakpointLocation, type CallFrameInfo, type CaptureSnapshotOptions, type CapturedExpression, CfInspectorError, type CfInspectorErrorCode, type DebuggerState, type FrameSnapshot, type InspectorConnectOptions, type InspectorSession, type InspectorTarget, type LogpointEvent, type LogpointStreamOptions, type LogpointStreamResult, type OpenedTunnel, type PauseEvent, type RemoteRootSetting, type ResolvedLocation, type ScopeInfo, type ScopeSnapshot, type ScriptInfo, type SetBreakpointInput, type SnapshotCaptureResult, type SnapshotResult, type TunnelTarget, type VariableSnapshot, type WaitForPauseOptions, buildBreakpointUrlRegex, buildLogpointCondition, captureSnapshot, connectInspector, discoverInspectorTargets, evaluateGlobal, evaluateOnFrame, fetchInspectorVersion, getProperties, listScripts, openCfTunnel, parseBreakpointSpec, parseRemoteRoot, removeBreakpoint, resume, setBreakpoint, streamLogpoint, validateExpression, waitForPause };
package/dist/index.js CHANGED
@@ -215,6 +215,15 @@ function stripTrailingSlash(value) {
215
215
  }
216
216
  return value;
217
217
  }
218
+ function normalizeRegexRootPattern(pattern) {
219
+ const withoutStartAnchor = pattern.startsWith("^") ? pattern.slice(1) : pattern;
220
+ const withoutEndAnchor = withoutStartAnchor.endsWith("$") && !isEscaped(withoutStartAnchor, withoutStartAnchor.length - 1) ? withoutStartAnchor.slice(0, -1) : withoutStartAnchor;
221
+ return stripTrailingSlash(withoutEndAnchor);
222
+ }
223
+ function buildFileUrlRegex(rootPattern, tail) {
224
+ const separator = rootPattern.endsWith("/") ? "" : "/";
225
+ return `^file://${rootPattern}${separator}${tail}$`;
226
+ }
218
227
  function escapeRegExp(value) {
219
228
  return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
220
229
  }
@@ -241,16 +250,18 @@ function buildBreakpointUrlRegex(input) {
241
250
  }
242
251
  case "literal": {
243
252
  const escapedRoot = escapeRegExp(input.remoteRoot.value);
244
- return `^file://${escapedRoot}/${tail}$`;
253
+ return buildFileUrlRegex(escapedRoot, tail);
245
254
  }
246
255
  case "regex": {
247
- return `^file://${input.remoteRoot.pattern}/${tail}$`;
256
+ const rootPattern = normalizeRegexRootPattern(input.remoteRoot.pattern);
257
+ return buildFileUrlRegex(rootPattern, tail);
248
258
  }
249
259
  }
250
260
  }
251
261
 
252
262
  // src/inspector.ts
253
263
  import { request } from "http";
264
+ import { performance } from "perf_hooks";
254
265
 
255
266
  // src/cdp.ts
256
267
  init_types();
@@ -606,21 +617,21 @@ async function connectInspector(options) {
606
617
  const PAUSE_BUFFER_LIMIT = 32;
607
618
  const pauseBuffer = [];
608
619
  const pauseWaitGate = { active: false };
620
+ const debuggerState = {};
609
621
  client.on("Debugger.paused", (raw) => {
610
622
  if (pauseWaitGate.active) {
611
623
  return;
612
624
  }
613
625
  const params = raw;
614
- const event = {
615
- reason: asString(params.reason),
616
- hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
617
- callFrames: toCallFrames(params.callFrames)
618
- };
626
+ const event = toPauseEvent(params, performance.now());
619
627
  if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
620
628
  pauseBuffer.shift();
621
629
  }
622
630
  pauseBuffer.push(event);
623
631
  });
632
+ client.on("Debugger.resumed", () => {
633
+ debuggerState.lastResumedAtMs = performance.now();
634
+ });
624
635
  await client.send("Runtime.enable");
625
636
  await client.send("Debugger.enable");
626
637
  return {
@@ -629,6 +640,7 @@ async function connectInspector(options) {
629
640
  scripts,
630
641
  pauseBuffer,
631
642
  pauseWaitGate,
643
+ debuggerState,
632
644
  dispose: async () => {
633
645
  try {
634
646
  await client.send("Debugger.disable");
@@ -746,36 +758,121 @@ function pauseMatches(pause, breakpointIds) {
746
758
  }
747
759
  return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
748
760
  }
761
+ function toPauseEvent(params, receivedAtMs) {
762
+ return {
763
+ reason: asString(params.reason),
764
+ hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
765
+ callFrames: toCallFrames(params.callFrames),
766
+ receivedAtMs
767
+ };
768
+ }
769
+ function remainingUntil(deadlineMs) {
770
+ return Math.max(0, deadlineMs - performance.now());
771
+ }
772
+ function topFrameLocation(pause) {
773
+ const top = pause.callFrames[0];
774
+ if (top === void 0) {
775
+ return "(no call frame)";
776
+ }
777
+ const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
778
+ return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
779
+ }
780
+ function pauseDetail(pause) {
781
+ return JSON.stringify({
782
+ reason: pause.reason,
783
+ hitBreakpoints: pause.hitBreakpoints,
784
+ topFrame: topFrameLocation(pause)
785
+ });
786
+ }
787
+ function hasResumedSincePause(session, pause) {
788
+ const pauseAt = pause.receivedAtMs;
789
+ const resumedAt = session.debuggerState.lastResumedAtMs;
790
+ return pauseAt !== void 0 && resumedAt !== void 0 && resumedAt >= pauseAt;
791
+ }
792
+ function throwBreakpointTimeout(timeoutMs) {
793
+ throw new CfInspectorError(
794
+ "BREAKPOINT_NOT_HIT",
795
+ `Timed out waiting for matching Debugger.paused after ${timeoutMs.toString()}ms`
796
+ );
797
+ }
798
+ function throwUnrelatedPauseTimeout(pause, timeoutMs) {
799
+ throw new CfInspectorError(
800
+ "UNRELATED_PAUSE_TIMEOUT",
801
+ `Target stayed paused by another debugger event before this command's breakpoint could hit within ${timeoutMs.toString()}ms`,
802
+ pauseDetail(pause)
803
+ );
804
+ }
805
+ async function waitForUnmatchedPauseToResume(session, pause, deadlineMs, timeoutMs) {
806
+ if (hasResumedSincePause(session, pause)) {
807
+ return;
808
+ }
809
+ const remainingMs = remainingUntil(deadlineMs);
810
+ if (remainingMs <= 0) {
811
+ throwUnrelatedPauseTimeout(pause, timeoutMs);
812
+ }
813
+ try {
814
+ await session.client.waitFor("Debugger.resumed", { timeoutMs: remainingMs });
815
+ session.debuggerState.lastResumedAtMs = performance.now();
816
+ } catch (err) {
817
+ if (err instanceof CfInspectorError && err.code === "BREAKPOINT_NOT_HIT") {
818
+ throwUnrelatedPauseTimeout(pause, timeoutMs);
819
+ }
820
+ throw err;
821
+ }
822
+ }
823
+ async function handleUnmatchedPause(session, pause, options, deadlineMs) {
824
+ if (options.unmatchedPausePolicy === "fail") {
825
+ throw new CfInspectorError(
826
+ "UNRELATED_PAUSE",
827
+ "Target paused before this command's breakpoint was reached",
828
+ pauseDetail(pause)
829
+ );
830
+ }
831
+ if (hasResumedSincePause(session, pause)) {
832
+ return;
833
+ }
834
+ options.onUnmatchedPause?.(pause);
835
+ await waitForUnmatchedPauseToResume(session, pause, deadlineMs, options.timeoutMs);
836
+ }
749
837
  async function waitForPause(session, options) {
838
+ const deadlineMs = performance.now() + options.timeoutMs;
750
839
  const buffer = session.pauseBuffer;
751
- while (buffer.length > 0) {
752
- const head = buffer.shift();
753
- if (head !== void 0 && pauseMatches(head, options.breakpointIds)) {
754
- return head;
840
+ while (buffer.length > 0 || remainingUntil(deadlineMs) > 0) {
841
+ while (buffer.length > 0) {
842
+ const buffered = buffer.shift();
843
+ if (buffered === void 0) {
844
+ continue;
845
+ }
846
+ if (pauseMatches(buffered, options.breakpointIds)) {
847
+ return buffered;
848
+ }
849
+ await handleUnmatchedPause(session, buffered, options, deadlineMs);
755
850
  }
756
- }
757
- session.pauseWaitGate.active = true;
758
- let params;
759
- try {
760
- const expected = options.breakpointIds;
761
- params = await session.client.waitFor("Debugger.paused", {
762
- timeoutMs: options.timeoutMs,
763
- predicate: (raw) => {
764
- if (expected === void 0 || expected.length === 0) {
851
+ const remainingMs = remainingUntil(deadlineMs);
852
+ if (remainingMs <= 0) {
853
+ throwBreakpointTimeout(options.timeoutMs);
854
+ }
855
+ session.pauseWaitGate.active = true;
856
+ let receivedAtMs;
857
+ let params;
858
+ try {
859
+ params = await session.client.waitFor("Debugger.paused", {
860
+ timeoutMs: remainingMs,
861
+ predicate: () => {
862
+ receivedAtMs = performance.now();
765
863
  return true;
766
864
  }
767
- const ids = Array.isArray(raw.hitBreakpoints) ? raw.hitBreakpoints.filter((id) => typeof id === "string") : [];
768
- return ids.some((id) => expected.includes(id));
769
- }
770
- });
771
- } finally {
772
- session.pauseWaitGate.active = false;
865
+ });
866
+ } finally {
867
+ session.pauseWaitGate.active = false;
868
+ }
869
+ const pause = toPauseEvent(params, receivedAtMs ?? performance.now());
870
+ if (pauseMatches(pause, options.breakpointIds)) {
871
+ return pause;
872
+ }
873
+ await handleUnmatchedPause(session, pause, options, deadlineMs);
773
874
  }
774
- return {
775
- reason: asString(params.reason),
776
- hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
777
- callFrames: toCallFrames(params.callFrames)
778
- };
875
+ throwBreakpointTimeout(options.timeoutMs);
779
876
  }
780
877
  async function resume(session) {
781
878
  await session.client.send("Debugger.resume");
@@ -831,7 +928,7 @@ var MAX_SCOPE_VARIABLES = 20;
831
928
  var MAX_CHILD_VARIABLES = 8;
832
929
  var MAX_VARIABLE_DEPTH = 2;
833
930
  var MAX_VALUE_LENGTH = 240;
834
- var SENSITIVE_NAME_REGEX = /(pass(?:word)?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
931
+ var SENSITIVE_NAME_REGEX = /(pass(?:word)?|credentials?|creds?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
835
932
  var PRIORITY_BY_TYPE = {
836
933
  local: 0,
837
934
  arguments: 1,
@@ -892,8 +989,11 @@ function formatPrimitive(value) {
892
989
  }
893
990
  return String(value);
894
991
  }
992
+ function isSensitiveName(name) {
993
+ return SENSITIVE_NAME_REGEX.test(name);
994
+ }
895
995
  function sanitizeValue(name, raw) {
896
- if (SENSITIVE_NAME_REGEX.test(name)) {
996
+ if (isSensitiveName(name)) {
897
997
  return "[REDACTED]";
898
998
  }
899
999
  if (raw.length <= MAX_VALUE_LENGTH) {
@@ -911,8 +1011,9 @@ async function captureProperties(session, objectId, limit, depth) {
911
1011
  limited.map(async (prop) => {
912
1012
  const name = typeof prop.name === "string" ? prop.name : "?";
913
1013
  const described = describeProperty(prop);
1014
+ const sensitive = isSensitiveName(name);
914
1015
  let children;
915
- if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1016
+ if (!sensitive && depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
916
1017
  try {
917
1018
  const nested = await captureProperties(
918
1019
  session,
@@ -926,7 +1027,7 @@ async function captureProperties(session, objectId, limit, depth) {
926
1027
  } catch {
927
1028
  }
928
1029
  }
929
- const sanitizedValue = sanitizeValue(name, described.value);
1030
+ const sanitizedValue = sensitive ? "[REDACTED]" : sanitizeValue(name, described.value);
930
1031
  const base = { name, value: sanitizedValue };
931
1032
  const withType = described.type === void 0 ? base : { ...base, type: described.type };
932
1033
  return children === void 0 ? withType : { ...withType, children };
@@ -949,15 +1050,19 @@ async function captureScopes(session, frame) {
949
1050
  if (objectId === void 0) {
950
1051
  return { type: scope.type, variables: [] };
951
1052
  }
952
- const variables = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
953
- return { type: scope.type, variables };
1053
+ try {
1054
+ const variables = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
1055
+ return { type: scope.type, variables };
1056
+ } catch {
1057
+ return { type: scope.type, variables: [] };
1058
+ }
954
1059
  })
955
1060
  );
956
1061
  }
957
1062
  function evalResultToCaptured(expression, result) {
958
1063
  if (result.exceptionDetails !== void 0) {
959
1064
  const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
960
- return { expression, error: text };
1065
+ return { expression, error: sanitizeValue(expression, text) };
961
1066
  }
962
1067
  const inner = result.result;
963
1068
  if (!inner) {