The debugger-side consumer for TrUAPI wire frames. Private, in-repo, not published.
The host taps every product↔host wire frame in its Rust core (truapi's
DebugSink) and streams each one outward as a { channelId, dir, frame: bytes }
envelope. This package is the other end: it owns all decoding — the wire
envelope (requestId and frame id, via decodeWireMessage), the grouping into
per-operation traces, and the per-frame payload decode. The host core treats
frames as opaque bytes and never decodes.
This keeps @parity/truapi (the product package) genuinely untouched: the tap is
in the Rust host, and the debugger's decode/trace logic lives here instead of in
the product transport.
Scope note. This package holds both the debugger library (the trace, envelope-decode, and value-decode engines plus the ingest that turns a wire envelope into a decoded frame) and its two mounts — the standalone app (
server.ts) and the in-app embed (in-app.ts). It lives in-repo because the debugger is coupled to the protocol this repo owns: it decodes wire frames with@parity/truapi, tracking the generated wire surface. Where the app ultimately lives (stays a truapi tool / own repo / a desktop app) is an open decision for the host-protocol owner; in-repo is the low-regret default and moving it later is cheap.
createDebugSession()— the trace engine wired to the ingest. Feed it envelopes withhandleEnvelope(...); read grouped traces fromtraceEngine, per-frame values fromframeDetail(...)/decodedFrames(...).createDebugIngest(sink)— decodes aDebugFrameEnvelopeinto anObservedFrameand forwards it. The layer that turns raw wire bytes into something the trace engine can group.createWireDebugger(...)— accumulates observed frames into per-requestIdtraces (correlates with product-sdk telemetry spans on the same id).createFrameDecoder(...)— the level-2 value decoder (see below): a per-frame decode of a payload to a plain JS value, reusing@parity/truapi's generatedWIRE_DECODE_TABLE. Every frame it can decode, it does, with no sensitive special-casing. The bare factory takesenabled: trueto opt in; a session turns it on for you.buildTraceView/wireTraceToView,renderOperationRow,renderTraceDetail,renderFrameValueDetail— the one view model and the one set of renderers both mounts share, so the two cannot drift apart.startDebugServer(...)(server.ts) — the standalone mount, below.createInAppDebugger(...)(in-app.ts) — the in-app mount, below.
Both render the same view model with the same renderers and the same stylesheet. They differ in where the debugger sits relative to the host:
standalone: host process ──ws://127.0.0.1:9231──▶ debugger server ──HTTP──▶ browser
(host dials out; frames leave the app; one server, many channels)
in-app: host in the page ──handleFrame()──▶ InAppDebugger.mount(el)
(same page as the host; no server, no dial; frames never leave the app)
- Standalone (
startDebugServer): a Bun WS+HTTP server bound to127.0.0.1only. Hosts dial in and send one text message per frame,{ channelId, dir, frame }withframebase64-encoded, plus the wire-identity fields a versioned host stamps (v,codec,schema) and an optionaldroppedcount. The browser view is a thin client over server-rendered fragments. - In-app (
createInAppDebugger): the second mount, for a host that runs in the page. It takes the same raw SCALE frame bytes with the same product-vantagedir, holds the session in-process, and renders the fragments directly with no polling. Browser-only (usesdocument); each browser tab is its own tenant, so there is nothing to host or scope.
This is a dev-only tool that decodes everything. The list views stay payload-blind — they group frames and sum byte lengths, never their contents — and the drill-down decodes a frame's payload to a plain JS value, for every frame, with no "sensitive" special-casing. Its contract:
- On by default. The standalone server decodes unless
TRUAPI_DEBUGGER_DECODE_VALUESis set to a falsy value (0/false/no/off), orstartDebugServer({ decodeValues: false })/createInAppDebugger({ decodeValues: false })in code — useful for a demo. With decode off, every frame reports byte length only and no bytes are even retained. - Reuses the generated table. Decoding is
WIRE_DECODE_TABLE[frameId]?.(bytes)from@parity/truapi/wire-decode— the same dev-only codecs the client uses. The debugger writes none of its own. - No redaction, no reveal toggle. Every frame the table can decode is
decoded, including signing, login, and payment. A developer inspecting their
own session's traffic sees the real values; there is no denylist, no reveal
escape hatch, and no
redactedstate. A frame the codec cannot type still shows its raw payload as<n>B · 0x…hex — a dev-only tool hides nothing it has the bytes for. Only a frame with no retained bytes (decode off) readspayload not shown. - Refused on contract drift. Decode is allowed only for a channel whose
declared
schemafingerprint (TRUAPI_WIRE_SCHEMA_HASH) andcodecmatch this debugger's; a mismatched or absent identity is refused (/frameanswers 409) and banners in the view. Payload-blind grouping is unaffected. - Never over the wire, never in the list endpoints. The host emits opaque bytes only; nothing about decode changes what it sends. Decode happens in the debugger, in the drill-down paths only.
| Endpoint | Serves |
|---|---|
GET / |
The inspector page: polls the fragments below. |
GET /op-list?channel=&sort= |
One server-rendered row per op. sort is recent, duration, frames, or method; absent keeps arrival order. Payload-blind. |
GET /op?id=&channel=&gen= |
The selected op's drill-down, each frame's value inline. |
GET /view |
The drill-down as a standalone fragment, values inline. |
GET /channels |
Connected hosts/channels, liveness, codec-mismatch flag. |
GET /stats?channel= |
Aggregate roll-up: counts, bytes, durations, health, busiest methods. Payload-blind. |
GET /traces |
The grouped traces as JSON. Payload-blind — never serializes bytes or values. |
GET /frame?id=&i=&channel= |
One frame's decode as JSON (the programmatic drill-down). |
Loopback is enforced on more than the bind: a request whose Host header is not
a loopback name gets a 403 (DNS-rebinding guard), and a WebSocket upgrade from a
foreign browser Origin is refused (CSWSH).
npm install # links @parity/truapi via the workspace
npm run build # tsc -b
npm run serve # bun run src/server.ts — listens on 127.0.0.1:9231, decodes by default
# a different port, or decode off for a demo
TRUAPI_DEBUGGER_PORT=9300 npm run serve
TRUAPI_DEBUGGER_DECODE_VALUES=0 npm run servePoint a host's debugger URL at ws://127.0.0.1:9231 (the host dials out) and
open http://127.0.0.1:9231/; click an op for its drill-down detail.
Use the literal 127.0.0.1, not localhost. Both dial gates accept a ws://
URL on a loopback host only — wss://, certificates, and any non-loopback
target are rejected — and localhost passes that check but resolves ::1 first
on macOS, while the server binds 127.0.0.1 alone. A native host then dials an
address nothing is listening on and logs nothing.
For the in-app mount, feed frames straight to the session:
import { createInAppDebugger } from "@parity/truapi-debugger";
const inspector = createInAppDebugger();
const dispose = inspector.mount(document.getElementById("wire-panel")!);
// from the host's tap, per frame:
inspector.handleFrame(channelId, "out", frameBytes);The exact host↔debugger framing is provisional (envelope spec, track T3); base64-in-JSON is what the server accepts today.