From b236b7e17502b7e29985bde55bdb6608069ea1fa Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 4 Sep 2026 18:11:40 +0530 Subject: [PATCH 1/4] chore(make): add a debugger target that builds the host in dev mode --- Makefile | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9d8acc2d5..7fdd86f2c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Run `make help` for the list of targets. .DEFAULT_GOAL := help -.PHONY: help setup build codegen test check check-generated clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check provider-android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-cli dev-bootstrap dev-link-check e2e-dotli e2e-cli-diagnosis e2e-signing-cli e2e-pairing-cli e2e-chat-cli e2e-cli-update headless install cli-runner cli-dist matrix explorer xcframework +.PHONY: help setup build codegen test check check-generated clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check provider-android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-cli dev-bootstrap debugger dev-link-check e2e-dotli e2e-cli-diagnosis e2e-signing-cli e2e-pairing-cli e2e-chat-cli e2e-cli-update headless install cli-runner cli-dist matrix explorer xcframework CARGO ?= cargo TRUAPI_PKG := js/packages/truapi @@ -25,6 +25,8 @@ DOTLI_TRUAPI_LINK := $(DOTLI_NODE_MODULES)/@parity/truapi DOTLI_HOST_WASM_LINK := $(DOTLI_NODE_MODULES)/@parity/truapi-host DOTLI_UI_TRUAPI_SHADOW := $(DOTLI_UI)/node_modules/@parity/truapi DOTLI_UI_HOST_WASM_SHADOW := $(DOTLI_UI)/node_modules/@parity/truapi-host +DEBUGGER_PKG := $(JS_PACKAGES)/truapi-debugger +DEBUGGER_PORT ?= 9231 VITE_NETWORKS ?= paseo-next-v2,previewnet export VITE_NETWORKS @@ -403,6 +405,24 @@ dev: dev-bootstrap ## Start dotli host (:5173) + playground (:3000) together; op ( until curl -fsS http://localhost:3000/ >/dev/null 2>&1; do sleep 1; done; curl -fsS http://localhost:3000/diagnostics >/dev/null 2>&1 || true ) & \ wait +debugger: dev-bootstrap ## Wire debugger (:9231) + a DEV-MODE dotli host (:5173) + playground (:3000). Open http://127.0.0.1:9231 + # `make dev` cannot drive the debugger: dotli ships only `build` and `preview`, + # both production builds, and the dial sits behind `import.meta.env.DEV`, which + # a production bundle replaces with `false`. The host then never dials and the + # board stays empty with no error - so build the host in dev mode here. + cd $(DOTLI)/apps/host && NODE_ENV=development VITE_APP_DEBUG=true bunx --bun vite build + @printf '\n Debugger: http://127.0.0.1:$(DEBUGGER_PORT)\n' + @printf ' Host: http://localhost:5173/localhost:3000\n\n' + @printf ' One-time, in the browser console on http://localhost:5173 (the realm that\n' + @printf ' creates the host runtime - localStorage is per-origin AND per browser profile):\n\n' + @printf ' localStorage.setItem("truapi:debugger", "ws://127.0.0.1:$(DEBUGGER_PORT)"); location.reload()\n\n' + @printf ' The host logs `wire debugger: dialling ...` once it connects.\n\n' + @trap 'kill 0' EXIT; \ + ( cd $(DEBUGGER_PKG) && TRUAPI_DEBUGGER_PORT=$(DEBUGGER_PORT) bun run src/server.ts ) & \ + ( cd $(DOTLI) && bun scripts/preview-server.ts ) & \ + ( cd $(PLAYGROUND) && yarn dev ) & \ + wait + e2e-dotli: ## Fully automated dotli + playground diagnosis e2e using the local signing-host CLI. @$(MAKE) dev-bootstrap cargo build -p truapi-host-cli From 7c3690b4116a050061c1e4fb9242f9af226cdf1f Mon Sep 17 00:00:00 2001 From: Nidish Date: Mon, 7 Sep 2026 14:28:31 +0530 Subject: [PATCH 2/4] docs(readme): describe the wire debugger package and the debugger target --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index bc731216e..83d45434c 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,28 @@ host owns where the bytes live. The crate stores nothing itself: a host implemen `StorageClient` over storage it already owns, on web and native alike, so it keeps control of quota and of whether the bytes are backed up or encrypted. +### Wire debugger + +[`@parity/truapi-debugger`](js/packages/truapi-debugger) is the consumer for the +payload-blind frame tap in `truapi-server`. The core streams raw SCALE frames out +of two choke points; the debugger correlates them into per-operation traces, +decodes envelopes and values behind a `TRUAPI_WIRE_SCHEMA_HASH` match, and renders +them through one of two mounts: + +- `startDebugServer(...)` is a standalone Bun WS+HTTP server on `127.0.0.1:9231` + that hosts dial into, so frames from any host reach one inspector. +- `createInAppDebugger(...)` mounts the same engine inside the host page, with no + server and no dial. + +All decoding lives in this package; `@parity/truapi` has no debug seam. Its +[README](js/packages/truapi-debugger/README.md) carries the endpoint list and the +per-host enablement recipe. + +`make debugger` brings up the inspector on `:9231` alongside a dot.li host and the +playground. It builds the host with `NODE_ENV=development` on purpose: the dial +sits behind `import.meta.env.DEV`, which a production bundle replaces with `false`, +so `make dev` leaves the board empty with no error. + ## How it works 1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each method tagged `#[wire(id = N)]` for a stable byte-level dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. From ab3bd5ccff0548381d254fd2057a903ff24bbc36 Mon Sep 17 00:00:00 2001 From: Nidish Date: Mon, 7 Sep 2026 17:15:56 +0530 Subject: [PATCH 3/4] chore(make): pass the debugger dial URL to the host build --- Makefile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7fdd86f2c..0c64b80cd 100644 --- a/Makefile +++ b/Makefile @@ -410,11 +410,15 @@ debugger: dev-bootstrap ## Wire debugger (:9231) + a DEV-MODE dotli host (:5173) # both production builds, and the dial sits behind `import.meta.env.DEV`, which # a production bundle replaces with `false`. The host then never dials and the # board stays empty with no error - so build the host in dev mode here. - cd $(DOTLI)/apps/host && NODE_ENV=development VITE_APP_DEBUG=true bunx --bun vite build + cd $(DOTLI)/apps/host && NODE_ENV=development VITE_APP_DEBUG=true \ + VITE_TRUAPI_DEBUGGER=ws://127.0.0.1:$(DEBUGGER_PORT) bunx --bun vite build @printf '\n Debugger: http://127.0.0.1:$(DEBUGGER_PORT)\n' @printf ' Host: http://localhost:5173/localhost:3000\n\n' - @printf ' One-time, in the browser console on http://localhost:5173 (the realm that\n' - @printf ' creates the host runtime - localStorage is per-origin AND per browser profile):\n\n' + @printf ' The dial URL is passed to the host build. A dotli revision that reads\n' + @printf ' VITE_TRUAPI_DEBUGGER seeds it into localStorage for every browser profile;\n' + @printf ' the revision this repo pins does not, so until that pin moves, set it once\n' + @printf ' in the browser console on http://localhost:5173 (the realm that creates the\n' + @printf ' host runtime - localStorage is per-origin AND per browser profile):\n\n' @printf ' localStorage.setItem("truapi:debugger", "ws://127.0.0.1:$(DEBUGGER_PORT)"); location.reload()\n\n' @printf ' The host logs `wire debugger: dialling ...` once it connects.\n\n' @trap 'kill 0' EXIT; \ From e20df8d3e9a41f364f3d43a538173b84b9c341f1 Mon Sep 17 00:00:00 2001 From: Nidish Date: Wed, 9 Sep 2026 16:37:08 +0530 Subject: [PATCH 4/4] feat(truapi-host): let a dev build carry the wire-debugger dial URL --- Makefile | 11 ++--- docs/design/wire-observability-debug-host.md | 6 ++- js/packages/truapi-host/README.md | 7 +++ .../src/web/create-worker-host-runtime.ts | 48 +++++++++++++++++-- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 0c64b80cd..8269d35d1 100644 --- a/Makefile +++ b/Makefile @@ -414,13 +414,12 @@ debugger: dev-bootstrap ## Wire debugger (:9231) + a DEV-MODE dotli host (:5173) VITE_TRUAPI_DEBUGGER=ws://127.0.0.1:$(DEBUGGER_PORT) bunx --bun vite build @printf '\n Debugger: http://127.0.0.1:$(DEBUGGER_PORT)\n' @printf ' Host: http://localhost:5173/localhost:3000\n\n' - @printf ' The dial URL is passed to the host build. A dotli revision that reads\n' - @printf ' VITE_TRUAPI_DEBUGGER seeds it into localStorage for every browser profile;\n' - @printf ' the revision this repo pins does not, so until that pin moves, set it once\n' - @printf ' in the browser console on http://localhost:5173 (the realm that creates the\n' - @printf ' host runtime - localStorage is per-origin AND per browser profile):\n\n' + @printf ' The dial URL is built into the host, so there is nothing to enable and it\n' + @printf ' works in any browser profile. The host logs `wire debugger: dialling ...\n' + @printf ' from the build` once it connects.\n\n' + @printf ' To aim it somewhere else instead, set the key on http://localhost:5173\n' + @printf ' (the realm that creates the host runtime); a key set by hand always wins:\n\n' @printf ' localStorage.setItem("truapi:debugger", "ws://127.0.0.1:$(DEBUGGER_PORT)"); location.reload()\n\n' - @printf ' The host logs `wire debugger: dialling ...` once it connects.\n\n' @trap 'kill 0' EXIT; \ ( cd $(DEBUGGER_PKG) && TRUAPI_DEBUGGER_PORT=$(DEBUGGER_PORT) bun run src/server.ts ) & \ ( cd $(DOTLI) && bun scripts/preview-server.ts ) & \ diff --git a/docs/design/wire-observability-debug-host.md b/docs/design/wire-observability-debug-host.md index 2670e2b87..24eb4af20 100644 --- a/docs/design/wire-observability-debug-host.md +++ b/docs/design/wire-observability-debug-host.md @@ -238,7 +238,11 @@ installed, which §9's enablement rules govern. - The web host reads its debugger URL behind a build-time DEV condition, so a production bundle returns no URL and a stray `localStorage` key cannot turn the - tap on. With no URL the host installs no emit callback, so the core installs no + tap on. The URL has two sources, both inside that condition: a `localStorage` + key, and a value the dev build carries. A key set by hand **MUST** win, so a + build's value is a default rather than an override, and the host **MUST** report + which source it used - an ignored key is otherwise indistinguishable from a key + that was never read. With no URL the host installs no emit callback, so the core installs no sink. The condition **MUST** be the bare token the bundler substitutes (`import.meta.env.DEV`) — no alias, no optional chaining. A bundler replaces that exact token and nothing else; an aliased read survives into the bundle, diff --git a/js/packages/truapi-host/README.md b/js/packages/truapi-host/README.md index 7cbd923ca..39aaf27c6 100644 --- a/js/packages/truapi-host/README.md +++ b/js/packages/truapi-host/README.md @@ -273,6 +273,13 @@ no tap: localStorage.setItem("truapi:debugger", "ws://127.0.0.1:9231"); ``` + A dev build can carry the URL instead, which is what a local stack does: + build the host with `VITE_TRUAPI_DEBUGGER=ws://127.0.0.1:9231` and every + browser profile that opens it dials without a key. A key set by hand always + wins over the build's value, so it stays an override. `localStorage` is + per-origin and per-profile, which is the reason a build-time default exists + at all: a key cannot be arranged from outside the browser. + Run the debugger at the other end (`@parity/truapi-debugger`, `npm run serve`, `127.0.0.1:9231`). On the next runtime boot the worker dials that URL and (via the Rust core's `DebugSink` tap) sends each frame as `{ channelId, dir, frame }`. diff --git a/js/packages/truapi-host/src/web/create-worker-host-runtime.ts b/js/packages/truapi-host/src/web/create-worker-host-runtime.ts index 451f0e541..a3785410c 100644 --- a/js/packages/truapi-host/src/web/create-worker-host-runtime.ts +++ b/js/packages/truapi-host/src/web/create-worker-host-runtime.ts @@ -233,10 +233,38 @@ type DebuggerEnablement = { | "enabled" | "production-build" | "production-build-switch-set" + | "enabled-from-build" | "no-key" | "no-storage"; }; +/** + * Dial URL a dev build was given, when it was given one. + * + * Lets a local stack point the tap at its own debugger with no per-browser setup: + * `localStorage` is per-origin AND per-profile, so a key is the one piece of this + * that cannot be arranged from outside the browser. A build-time value can, and it + * then holds for every profile that opens that build. + * + * Same literal-token rule as the `DEV` read below: a bundler replaces the exact + * `import.meta.env.VITE_TRUAPI_DEBUGGER` expression, so it must not be aliased or + * optionally chained. The try/catch covers realms with no `import.meta.env` at all + * (tsc output under Node, unit tests), where the access throws. + */ +function buildTimeDebuggerUrl(): string | null { + let raw: unknown; + try { + raw = ( + import.meta as unknown as { env: { VITE_TRUAPI_DEBUGGER?: unknown } } + ).env.VITE_TRUAPI_DEBUGGER; + } catch { + return null; + } + if (typeof raw !== "string") return null; + const url = raw.trim(); + return url === "" ? null : url; +} + function readPersistedDebuggerUrl(): DebuggerEnablement { // Hard dev-only gate, not a convention: bundlers (Vite) replace // `import.meta.env.DEV` with a boolean literal, so in a PRODUCTION build this @@ -284,10 +312,14 @@ function readPersistedDebuggerUrl(): DebuggerEnablement { }; } const storage = globalThis.localStorage; + const url = storage?.getItem(DEV_DEBUGGER_URL_KEY) ?? null; + // A key set by hand wins, so a developer can always aim a build somewhere else + // without rebuilding it. The build-time value is the default, not an override. + if (url !== null && url !== "") return { url, reason: "enabled" }; + const fromBuild = buildTimeDebuggerUrl(); + if (fromBuild !== null) return { url: fromBuild, reason: "enabled-from-build" }; if (storage === undefined) return { url: null, reason: "no-storage" }; - const url = storage.getItem(DEV_DEBUGGER_URL_KEY); - if (url === null || url === "") return { url: null, reason: "no-key" }; - return { url, reason: "enabled" }; + return { url: null, reason: "no-key" }; } /** @@ -323,6 +355,16 @@ function reportDebuggerEnablement(e: DebuggerEnablement): void { console.info(`[truapi] wire debugger: dialling ${e.url} (origin ${origin})`); return; } + if (e.reason === "enabled-from-build") { + // Say where the URL came from. A developer who never set a key needs to know + // the build chose one, and a developer whose key was ignored needs to know it + // was not: a key always wins, so seeing this line means no key was set. + console.info( + `[truapi] wire debugger: dialling ${e.url} from the build (origin ${origin}); ` + + `set "${DEV_DEBUGGER_URL_KEY}" here to override`, + ); + return; + } const why = e.reason === "no-storage" ? "no localStorage in this realm"