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 +20 -5
- package/dist/observe.d.ts +9 -0
- package/dist/observe.js +20 -5
- package/package.json +2 -1
- package/src/observe.ts +22 -1
- package/src/wrap-client.ts +28 -8
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
|
-
|
|
121
|
-
|
|
122
|
-
stream.
|
|
123
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
stream.
|
|
123
|
-
|
|
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.
|
|
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
|
|
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
|
},
|
package/src/wrap-client.ts
CHANGED
|
@@ -57,10 +57,16 @@ function defaultPreview(v: unknown): unknown {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
stream.
|
|
77
|
-
|
|
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
|
}
|