hrpc-inspector 0.1.1 → 0.1.2

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.js CHANGED
@@ -117,10 +117,21 @@ function monitorStream(report, endpoint, corrId, stream, t0, preview, tag) {
117
117
  report({ type: "request.end", method: endpoint, corrId, status, count: n, dur: Date.now() - t0, t: Date.now(), ...tag, ...extra || {} });
118
118
  };
119
119
  try {
120
- stream.on("data", (item) => report({ type: "stream.data", method: endpoint, corrId, seq: ++n, item: preview(item), t: Date.now(), ...tag }));
121
- stream.on("error", (err) => settle("error", { error: String(err) }));
122
- stream.on("end", () => settle("closed"));
123
- stream.on("close", () => settle("closed"));
120
+ const original = stream.emit;
121
+ if (typeof original !== "function") return;
122
+ stream.emit = function patched(name, ...args) {
123
+ try {
124
+ if (name === "data") {
125
+ report({ type: "stream.data", method: endpoint, corrId, seq: ++n, item: preview(args[0]), t: Date.now(), ...tag });
126
+ } else if (name === "error") {
127
+ settle("error", { error: String(args[0]) });
128
+ } else if (name === "end" || name === "close") {
129
+ settle("closed");
130
+ }
131
+ } catch {
132
+ }
133
+ return original.apply(this, [name, ...args]);
134
+ };
124
135
  } catch (e) {
125
136
  report({ type: "request.end", method: endpoint, corrId, status: "error", error: "monitor-failed: " + String(e), dur: 0, t: Date.now(), ...tag });
126
137
  }
@@ -257,6 +268,8 @@ function observe(opts = {}) {
257
268
  const source = resolveSource(opts.source, info.runtime);
258
269
  const redactOn = opts.redact ?? (opts.websocket ? false : true);
259
270
  const redactor = opts.redactor ?? (redactOn ? new Redactor({ bodyAllowlist: opts.bodyAllowlist }) : null);
271
+ const redactMethods = new Set(opts.redactMethods ?? []);
272
+ const methodRedactor = !redactor && redactMethods.size > 0 ? new Redactor() : null;
260
273
  const wireSource = redactor ? redactSource(source, redactor) : source;
261
274
  const invokeEnabled = opts.allowInvoke === true && !!opts.websocket;
262
275
  const replayLogMax = opts.replayLogMax ?? 2e3;
@@ -296,7 +309,9 @@ function observe(opts = {}) {
296
309
  const flusher = new BatchFlusher({
297
310
  intervalMs: opts.intervalMs ?? 200,
298
311
  onFlush: (batch) => {
299
- const out = redactor ? redactor.redactBatch(batch) : batch;
312
+ const out = redactor ? redactor.redactBatch(batch) : methodRedactor ? batch.map(
313
+ (e) => redactMethods.has(String(e.method)) ? methodRedactor.redact(e) : e
314
+ ) : batch;
300
315
  exporter.export(out.map((e) => ({ ...e, src: wireSource.id })));
301
316
  }
302
317
  });
package/dist/observe.d.ts CHANGED
@@ -42,6 +42,15 @@ export interface ObserveOptions {
42
42
  * Set false ONLY for a purely-local view that never leaves the machine (F3/F4/F9).
43
43
  */
44
44
  redact?: boolean;
45
+ /**
46
+ * Per-`method` FORCED redaction, for when global `redact` is off (the local viewer default).
47
+ * A listed method still appears in the GUI — you see it fired, when, how long it took, whether
48
+ * it errored, and the SHAPE of its payload (`{phrase: 'string'}`) — but its values are replaced
49
+ * by a content summary. Use it for methods that carry secrets you never want rendered, e.g.
50
+ * a recovery phrase. This is a denylist: a newly added secret-bearing method must be listed.
51
+ * Ignored when `redact` is on, because everything is redacted already.
52
+ */
53
+ redactMethods?: Iterable<string>;
45
54
  /** Supply a shared Redactor (e.g. to reuse its local peer reverse-map). Default: a fresh one. */
46
55
  redactor?: Redactor;
47
56
  /** Per-`method` opt-in allowlist for full payload bodies (default: none — bodies summarised). */
package/dist/observe.js CHANGED
@@ -117,10 +117,21 @@ function monitorStream(report, endpoint, corrId, stream, t0, preview, tag) {
117
117
  report({ type: "request.end", method: endpoint, corrId, status, count: n, dur: Date.now() - t0, t: Date.now(), ...tag, ...extra || {} });
118
118
  };
119
119
  try {
120
- stream.on("data", (item) => report({ type: "stream.data", method: endpoint, corrId, seq: ++n, item: preview(item), t: Date.now(), ...tag }));
121
- stream.on("error", (err) => settle("error", { error: String(err) }));
122
- stream.on("end", () => settle("closed"));
123
- stream.on("close", () => settle("closed"));
120
+ const original = stream.emit;
121
+ if (typeof original !== "function") return;
122
+ stream.emit = function patched(name, ...args) {
123
+ try {
124
+ if (name === "data") {
125
+ report({ type: "stream.data", method: endpoint, corrId, seq: ++n, item: preview(args[0]), t: Date.now(), ...tag });
126
+ } else if (name === "error") {
127
+ settle("error", { error: String(args[0]) });
128
+ } else if (name === "end" || name === "close") {
129
+ settle("closed");
130
+ }
131
+ } catch {
132
+ }
133
+ return original.apply(this, [name, ...args]);
134
+ };
124
135
  } catch (e) {
125
136
  report({ type: "request.end", method: endpoint, corrId, status: "error", error: "monitor-failed: " + String(e), dur: 0, t: Date.now(), ...tag });
126
137
  }
@@ -257,6 +268,8 @@ function observe(opts = {}) {
257
268
  const source = resolveSource(opts.source, info.runtime);
258
269
  const redactOn = opts.redact ?? (opts.websocket ? false : true);
259
270
  const redactor = opts.redactor ?? (redactOn ? new Redactor({ bodyAllowlist: opts.bodyAllowlist }) : null);
271
+ const redactMethods = new Set(opts.redactMethods ?? []);
272
+ const methodRedactor = !redactor && redactMethods.size > 0 ? new Redactor() : null;
260
273
  const wireSource = redactor ? redactSource(source, redactor) : source;
261
274
  const invokeEnabled = opts.allowInvoke === true && !!opts.websocket;
262
275
  const replayLogMax = opts.replayLogMax ?? 2e3;
@@ -296,7 +309,9 @@ function observe(opts = {}) {
296
309
  const flusher = new BatchFlusher({
297
310
  intervalMs: opts.intervalMs ?? 200,
298
311
  onFlush: (batch) => {
299
- const out = redactor ? redactor.redactBatch(batch) : batch;
312
+ const out = redactor ? redactor.redactBatch(batch) : methodRedactor ? batch.map(
313
+ (e) => redactMethods.has(String(e.method)) ? methodRedactor.redact(e) : e
314
+ ) : batch;
300
315
  exporter.export(out.map((e) => ({ ...e, src: wireSource.id })));
301
316
  }
302
317
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hrpc-inspector",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "hrpc based debugging and observability for Bare. Tap an RPC client or a raw transport and read endpoint, request, response, latency and live subscription streams in a local GUI, or merge multi-peer timelines over a Hyperswarm hub.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "esbuild": "^0.28.2",
66
+ "streamx": "^2.28.1",
66
67
  "typescript": "^7.0.2"
67
68
  }
68
69
  }
package/src/observe.ts CHANGED
@@ -128,6 +128,15 @@ export interface ObserveOptions {
128
128
  * Set false ONLY for a purely-local view that never leaves the machine (F3/F4/F9).
129
129
  */
130
130
  redact?: boolean;
131
+ /**
132
+ * Per-`method` FORCED redaction, for when global `redact` is off (the local viewer default).
133
+ * A listed method still appears in the GUI — you see it fired, when, how long it took, whether
134
+ * it errored, and the SHAPE of its payload (`{phrase: 'string'}`) — but its values are replaced
135
+ * by a content summary. Use it for methods that carry secrets you never want rendered, e.g.
136
+ * a recovery phrase. This is a denylist: a newly added secret-bearing method must be listed.
137
+ * Ignored when `redact` is on, because everything is redacted already.
138
+ */
139
+ redactMethods?: Iterable<string>;
131
140
  /** Supply a shared Redactor (e.g. to reuse its local peer reverse-map). Default: a fresh one. */
132
141
  redactor?: Redactor;
133
142
  /** Per-`method` opt-in allowlist for full payload bodies (default: none — bodies summarised). */
@@ -249,6 +258,10 @@ export function observe(opts: ObserveOptions = {}): ObserveHandle {
249
258
  const redactOn = opts.redact ?? (opts.websocket ? false : true);
250
259
  const redactor: Redactor | null =
251
260
  opts.redactor ?? (redactOn ? new Redactor({ bodyAllowlist: opts.bodyAllowlist }) : null);
261
+ // Only meaningful while global redaction is off; when it is on these methods are covered anyway.
262
+ const redactMethods = new Set(opts.redactMethods ?? []);
263
+ const methodRedactor: Redactor | null =
264
+ !redactor && redactMethods.size > 0 ? new Redactor() : null;
252
265
  // Identity ON THE WIRE — hashed ids / no human labels when redaction is on. `handle.source`
253
266
  // keeps the original for the app's own use.
254
267
  const wireSource = redactor ? redactSource(source, redactor) : source;
@@ -306,7 +319,15 @@ export function observe(opts: ObserveOptions = {}): ObserveHandle {
306
319
  const flusher = new BatchFlusher<L2Event>({
307
320
  intervalMs: opts.intervalMs ?? 200,
308
321
  onFlush: (batch) => {
309
- const out = redactor ? redactor.redactBatch(batch) : batch;
322
+ const out = redactor
323
+ ? redactor.redactBatch(batch)
324
+ : methodRedactor
325
+ ? batch.map((e) =>
326
+ redactMethods.has(String((e as Record<string, unknown>).method))
327
+ ? (methodRedactor.redact(e as Record<string, unknown>) as L2Event)
328
+ : e,
329
+ )
330
+ : batch;
310
331
  // Stamp the (wire) source id so the GUI can group/select by device.
311
332
  exporter.export(out.map((e) => ({ ...(e as object), src: wireSource.id })));
312
333
  },
@@ -57,10 +57,16 @@ function defaultPreview(v: unknown): unknown {
57
57
  }
58
58
  }
59
59
 
60
- // Attach PASSIVE listeners to a subscription stream. Non-destructive when the app also uses
61
- // `.on('data')` (every listener receives each item). If a consumer instead uses `.read()` /
62
- // async-iteration, adding a data listener flips it to flowing mode — opt out with
63
- // monitorStreams:false for those.
60
+ // Observe a subscription stream WITHOUT changing its behaviour.
61
+ //
62
+ // This wraps `emit` instead of calling `.on('data', ...)`. Registering a data listener is not
63
+ // passive: it puts the stream into flowing mode immediately, so anything emitted before the app
64
+ // attaches its own listener is delivered to us and LOST to the app. That is a debug-only data
65
+ // loss — the app silently misses items, with no error — and it is real: keet's RPC subscriptions
66
+ // return their stream synchronously, while the consumer attaches a tick later.
67
+ //
68
+ // Wrapping `emit` cannot cause that: we only see events the stream was already delivering. If
69
+ // nothing consumes the stream, nothing is emitted and we report nothing, which is the truth.
64
70
  function monitorStream(report: Report, endpoint: string, corrId: string, stream: any, t0: number, preview: (v: unknown) => unknown, tag: Record<string, unknown>): void {
65
71
  report({ type: 'stream.open', method: endpoint, corrId, t: Date.now(), ...tag });
66
72
  let n = 0;
@@ -71,10 +77,24 @@ function monitorStream(report: Report, endpoint: string, corrId: string, stream:
71
77
  report({ type: 'request.end', method: endpoint, corrId, status, count: n, dur: Date.now() - t0, t: Date.now(), ...tag, ...(extra || {}) });
72
78
  };
73
79
  try {
74
- stream.on('data', (item: unknown) => report({ type: 'stream.data', method: endpoint, corrId, seq: ++n, item: preview(item), t: Date.now(), ...tag }));
75
- stream.on('error', (err: unknown) => settle('error', { error: String(err) }));
76
- stream.on('end', () => settle('closed'));
77
- stream.on('close', () => settle('closed'));
80
+ const original = stream.emit;
81
+ if (typeof original !== 'function') return;
82
+ stream.emit = function patched(this: unknown, name: string, ...args: unknown[]): unknown {
83
+ // Observation must never affect delivery: record, then hand off unchanged. A throw in the
84
+ // reporting path would surface as a stream error in the app, so it is swallowed.
85
+ try {
86
+ if (name === 'data') {
87
+ report({ type: 'stream.data', method: endpoint, corrId, seq: ++n, item: preview(args[0]), t: Date.now(), ...tag });
88
+ } else if (name === 'error') {
89
+ settle('error', { error: String(args[0]) });
90
+ } else if (name === 'end' || name === 'close') {
91
+ settle('closed');
92
+ }
93
+ } catch {
94
+ /* never break the stream */
95
+ }
96
+ return original.apply(this, [name, ...args] as never);
97
+ };
78
98
  } catch (e) {
79
99
  report({ type: 'request.end', method: endpoint, corrId, status: 'error', error: 'monitor-failed: ' + String(e), dur: 0, t: Date.now(), ...tag });
80
100
  }