diff --git a/Makefile b/Makefile index 880f1ff2a..d3d586c11 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 @@ -396,6 +398,27 @@ 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 \ + 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 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' + @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 diff --git a/README.md b/README.md index c7b032e4f..125f5574d 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,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. 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"