@saptools/cf-inspector 0.3.1 → 0.3.3

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;
@@ -78,14 +79,16 @@ interface CapturedExpression {
78
79
  readonly type?: string;
79
80
  readonly error?: string;
80
81
  }
81
- interface SnapshotResult {
82
+ interface SnapshotCaptureResult {
82
83
  readonly reason: string;
83
84
  readonly hitBreakpoints: readonly string[];
84
85
  readonly capturedAt: string;
85
- readonly captureDurationMs: number;
86
86
  readonly topFrame?: FrameSnapshot;
87
87
  readonly captures: readonly CapturedExpression[];
88
88
  }
89
+ interface SnapshotResult extends SnapshotCaptureResult {
90
+ readonly pausedDurationMs: number | null;
91
+ }
89
92
  interface ScriptInfo {
90
93
  readonly scriptId: string;
91
94
  readonly url: string;
@@ -170,12 +173,16 @@ interface InspectorVersion {
170
173
  interface PauseWaitGate {
171
174
  active: boolean;
172
175
  }
176
+ interface DebuggerState {
177
+ lastResumedAtMs?: number;
178
+ }
173
179
  interface InspectorSession {
174
180
  readonly client: CdpClient;
175
181
  readonly target: InspectorTarget;
176
182
  readonly scripts: ReadonlyMap<string, ScriptInfo>;
177
183
  readonly pauseBuffer: PauseEvent[];
178
184
  readonly pauseWaitGate: PauseWaitGate;
185
+ readonly debuggerState: DebuggerState;
179
186
  dispose(): Promise<void>;
180
187
  }
181
188
  interface CdpEvalResult {
@@ -204,6 +211,8 @@ declare function removeBreakpoint(session: InspectorSession, breakpointId: strin
204
211
  interface WaitForPauseOptions {
205
212
  readonly timeoutMs: number;
206
213
  readonly breakpointIds?: readonly string[];
214
+ readonly unmatchedPausePolicy?: "wait-for-resume" | "fail";
215
+ readonly onUnmatchedPause?: (pause: PauseEvent) => void;
207
216
  }
208
217
  declare function waitForPause(session: InspectorSession, options: WaitForPauseOptions): Promise<PauseEvent>;
209
218
  declare function resume(session: InspectorSession): Promise<void>;
@@ -236,7 +245,7 @@ declare function getProperties(session: InspectorSession, objectId: string): Pro
236
245
  interface CaptureSnapshotOptions {
237
246
  readonly captures?: readonly string[];
238
247
  }
239
- declare function captureSnapshot(session: InspectorSession, pause: PauseEvent, options?: CaptureSnapshotOptions): Promise<SnapshotResult>;
248
+ declare function captureSnapshot(session: InspectorSession, pause: PauseEvent, options?: CaptureSnapshotOptions): Promise<SnapshotCaptureResult>;
240
249
 
241
250
  interface LogpointEvent {
242
251
  readonly ts: string;
@@ -296,4 +305,4 @@ interface OpenedTunnel {
296
305
  }
297
306
  declare function openCfTunnel(target: TunnelTarget): Promise<OpenedTunnel>;
298
307
 
299
- 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 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");
@@ -826,13 +923,12 @@ async function getProperties(session, objectId) {
826
923
  }
827
924
 
828
925
  // src/snapshot.ts
829
- import { performance } from "perf_hooks";
830
926
  var MAX_SCOPES = 3;
831
927
  var MAX_SCOPE_VARIABLES = 20;
832
928
  var MAX_CHILD_VARIABLES = 8;
833
929
  var MAX_VARIABLE_DEPTH = 2;
834
930
  var MAX_VALUE_LENGTH = 240;
835
- 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;
836
932
  var PRIORITY_BY_TYPE = {
837
933
  local: 0,
838
934
  arguments: 1,
@@ -893,8 +989,11 @@ function formatPrimitive(value) {
893
989
  }
894
990
  return String(value);
895
991
  }
992
+ function isSensitiveName(name) {
993
+ return SENSITIVE_NAME_REGEX.test(name);
994
+ }
896
995
  function sanitizeValue(name, raw) {
897
- if (SENSITIVE_NAME_REGEX.test(name)) {
996
+ if (isSensitiveName(name)) {
898
997
  return "[REDACTED]";
899
998
  }
900
999
  if (raw.length <= MAX_VALUE_LENGTH) {
@@ -912,8 +1011,9 @@ async function captureProperties(session, objectId, limit, depth) {
912
1011
  limited.map(async (prop) => {
913
1012
  const name = typeof prop.name === "string" ? prop.name : "?";
914
1013
  const described = describeProperty(prop);
1014
+ const sensitive = isSensitiveName(name);
915
1015
  let children;
916
- if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1016
+ if (!sensitive && depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
917
1017
  try {
918
1018
  const nested = await captureProperties(
919
1019
  session,
@@ -927,7 +1027,7 @@ async function captureProperties(session, objectId, limit, depth) {
927
1027
  } catch {
928
1028
  }
929
1029
  }
930
- const sanitizedValue = sanitizeValue(name, described.value);
1030
+ const sanitizedValue = sensitive ? "[REDACTED]" : sanitizeValue(name, described.value);
931
1031
  const base = { name, value: sanitizedValue };
932
1032
  const withType = described.type === void 0 ? base : { ...base, type: described.type };
933
1033
  return children === void 0 ? withType : { ...withType, children };
@@ -985,7 +1085,6 @@ function evalResultToCaptured(expression, result) {
985
1085
  return buildCaptured("undefined");
986
1086
  }
987
1087
  async function captureSnapshot(session, pause, options = {}) {
988
- const startedAt = performance.now();
989
1088
  const top = pause.callFrames[0];
990
1089
  let topFrame;
991
1090
  let captures = [];
@@ -1012,12 +1111,10 @@ async function captureSnapshot(session, pause, options = {}) {
1012
1111
  );
1013
1112
  }
1014
1113
  }
1015
- const captureDurationMs = Math.round((performance.now() - startedAt) * 1e3) / 1e3;
1016
1114
  return {
1017
1115
  reason: pause.reason,
1018
1116
  hitBreakpoints: pause.hitBreakpoints,
1019
1117
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1020
- captureDurationMs,
1021
1118
  ...topFrame === void 0 ? {} : { topFrame },
1022
1119
  captures
1023
1120
  };