feat(host-cli): @dotli/host-cli, a terminal host for the TrUAPI core - #133
Draft
ReinhardHatko wants to merge 1 commit into
Draft
feat(host-cli): @dotli/host-cli, a terminal host for the TrUAPI core#133ReinhardHatko wants to merge 1 commit into
ReinhardHatko wants to merge 1 commit into
Conversation
@dotli/host-cli 0.1.0, dotli's first genuinely published package and the
terminal PEER of the web host: both depend directly on @parity/truapi-host
and implement its 17 platform callbacks; neither depends on the other.
Built on origin/main (the v0.7.0 Rust-core lineage) — feat/host-core is
stranded reference material, deliberately left untouched.
The host is parameterized by the embedding app's metadata (host
{name,icon,version}, pairing deeplink scheme, people/bulletin genesis,
chain endpoints): a CLI app is its own host. The typed callback surface
comes from the package's own generated adapter, reached by file URL
because createWasmRawCallbacks is not in the exports map (candidate
upstream ask; hand-written SCALE callbacks are the drift this avoids).
Beyond the callbacks, the host owns what measurement showed the core
leaves to it:
- pairing QR rendered offline from AuthState.Pairing, and an elapsed
progress line through the silent ~20s Authenticating window;
- product storage cleared on logout AND when a different identity
connects — core product-storage keys carry no account component, so
the next identity would inherit the previous one's data;
- chain connections pooled by genesis hash (the core opens one socket
per need) with per-lease request-id rewriting, subscription-token
routing, order-preserving delivery, and a lease cap per socket below
substrate's per-connection chainHead follow limit;
- theme/preimage streams emit once and park (returning reads as
end-of-stream to the core);
- the untyped 180s SSO timeout translated into phone-facing guidance
(explainProductError), with logLevel defaulting to warn because the
core's own diagnosis is only a tracing warning;
- serializeOperationStarts exported for the product side: papi over the
core hung 3/6 without it and ran 6/6 clean with it.
confirmUserAction prompts are deliberately modest: the host cannot
decode callData or preimage content, so prompts state the review kind
and the typed metadata the host actually knows, and defer content
verification to the paired wallet — the authoritative trust surface.
Non-TTY prompts auto-deny; there is no unattended signing path at all.
Publishing machinery, per the decided policy:
- private:false, tsc build to dist/ (ESM + d.ts), exports at built JS,
files limited to dist+docs, LICENSE included, npm publish dry-run
verified;
- independent semver starting at 0.1.0: scripts/set-version.ts now
skips published (non-private) packages, so app releases keep syncing
the 15 private packages without touching this one;
- release via host-cli-v* tags: .github/workflows/release-host-cli.yml
gates on lint/typecheck/build/test, verifies tag==package version,
publishes with npm provenance (needs the NPM_TOKEN secret).
Tests run the REAL wasm core headless: a product localStorage
round-trip over the loopback wire, the pairing presentation offline
(deeplink emitted with no sockets), logout clearing, plus unit coverage
for the kv store (0600, write races), the chain pool demux, and the
ordering shim. The built dist was additionally smoke-tested under plain
node.
A two-axis review (standards + spec sub-agents) ran before this commit
and its findings are folded in: repository.url names dotli-community
(npm provenance would have hard-failed on the paritytech/dotli
mismatch), loadWasmCore memoizes per dist directory instead of one
process-wide singleton, comments and prompts follow CONTRIBUTING's
documentation rules, and every test is a user story structured
Given/When/Then.
Known seam, documented in the README: product-sdk consumers reach this
host through @parity/product-sdk-host/testing's setTruApiClient — a
test-only entry point, used knowingly until a real consumer shapes the
supported injection point (decided 2026-07-29 not to file that ask yet).
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
Bundle Size ReportChunks over 500 KB:
All files
Commit: e098671 |
Contributor
⚡ Performance Report
|
Contributor
|
❌ E2E Product suite failed on Failed tests:
Logs: https://github.com/paritytech/dotli-community/actions/runs/30564631921 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
dotli's v0.7.0 switch to the Rust core (#70, #77) quietly produced something
bigger than a web refactor: a complete, headless host engine. The core owns
the wire dispatcher, SCALE, accounts, signing, statement store, SSO pairing,
restore, and logout. A platform supplies 17 callbacks and a human surface.
The web app is one such platform. A terminal is another, and real terminal
products exist today with no supported way to be hosted.
This branch adds
packages/host-cli: the terminal peer of the web host,published to npm as dotli's first consumable library. Nothing in the web
app, its build, or its release process changes.
A host is the thin layer around the engine that implements the
RequiredHostCallbackssurface and owns the user-facing presentation. Thatsecond half is not optional:
confirmUserAction(review)returnsPromise<boolean>and can only be answered by asking a human. A host thatcannot prompt can only auto-approve, which is a policy stub, not a host.
The web host answers with modals, localStorage, and a Web Worker. This one
answers with a QR code, readline prompts, owner-only files, and the core
running in-process via
initSync. Same engine, different platform.Why a terminal host, and why here
There is a concrete consumer: d3pot, a git remote for repositories stored
via the TrUAPI triangle (Asset Hub contracts, Bulletin storage, People
identity). Like any terminal product, it needs a host to pair with a wallet
and to sign. And it is not alone: playground-cli and bulletin-deploy are
further terminal products in the same position and are natural next
consumers. The only path any of them had,
@parity/product-sdk-terminal,is built on the deleted
@novasamatechlineage and puts host internals ina product SDK, where they do not belong. Without a supported host library,
every terminal product ends up hand-rolling the 17 callbacks and drifting
against each engine release.
Owning that library here is symmetric with the web: dotli's web host is not
in product-sdk either. It also means terminal products pair with the same
host identity conventions the web uses, so a user sees one coherent "dotli"
pairing story across surfaces.
The host is parameterized by the embedding app's metadata (host name, icon,
version, deeplink scheme, People/Bulletin genesis, served chains). For a
terminal app, the app IS the host. Nothing in the package is network- or
product-specific.
What the branch contains
One package, one workflow, one script change:
packages/host-cli(about 1,100 lines of source): the 17 typed callbacksbuilt on the engine's own generated adapter (no hand-written SCALE), a
swappable terminal presenter, owner-only (0600) JSON file stores, a chain
connection pool, an in-process loopback wire, and two product-side
helpers (
serializeOperationStarts,explainProductError)..github/workflows/release-host-cli.yml: tag-driven release(
host-cli-vX.Y.Z) that gates on lint, typecheck, build, and tests,verifies the tag matches the package version, and publishes with npm
provenance.
scripts/set-version.ts: now skips packages that are notprivate: true. App releases keep stamping the 15 private packages exactly asbefore.
Beyond the callbacks, the host owns behaviors that measurement (not
guesswork) showed the engine leaves to hosts:
AuthState.Pairingbefore any socketopens, so the QR renders instantly and offline.
Authenticatingruns about 20 silent seconds (the People-chain statementround trip) with no callback in between. The presenter shows elapsed
progress so the host does not look hung.
host clears product storage on logout and when a different identity
connects, so no identity inherits another's data.
during one pairing). The pool shares sockets per genesis hash with
per-lease request-id rewriting and strictly order-preserving delivery,
capped at 2 leases per socket to stay under substrate's per-connection
chainHead_v1_followlimit.themeandpreimagecallbacks are emit-once-then-stay-openstreams. Returning early reads as end-of-stream to the engine.
TxErrorafter 180seconds.
explainProductErrorturns it into actionable guidance.Security stance, stated explicitly: confirm prompts are deliberately
modest. The host cannot decode
callDataor preimage content, and thepaired wallet is the authoritative surface that decodes before signing. So
prompts state the action kind and the typed metadata the host actually
knows, then defer to the phone. Prompts auto-deny on non-interactive stdin,
and there is no unattended signing path at all (the engine exposes no local
keypair API, a property treated here as a feature).
Why the web host and this package share no code (yet)
A fair first question for any reviewer:
packages/host-clireuses nothingfrom
packages/uior the other workspace packages. That is deliberate, forthree reasons in decreasing order of weight.
Everything a shared "host core" layer would traditionally hold already
lives in the engine. Dispatch, sessions, signing, account derivation,
storage scoping, and pairing are Rust code behind
@parity/truapi-host. What remains on each side is the platform half:modals versus terminal prompts, localStorage versus 0600 files, a Web
Worker versus in-process wasm. The two halves have no implementable
middle. Both spokes therefore depend directly on the engine, and neither
depends on the other.
A published package cannot depend on private workspace packages.
Every other
@dotlipackage isprivate: truewith raw./src/*.tsexports. The chain pool is the concrete case: reusing the broker in
@dotli/protocolwas evaluated and rejected, because it would pull aprivate package into a published dependency graph. Real sharing would
require publishing a
host-commonpackage too, multiplying thepublishing obligation this PR asks the repo to take on exactly once.
The overlap that does exist is mostly the engine's to own, and a
shared dotli layer would hide that. Both hosts encode the same engine
semantics today: product storage is keyed by product id and not by
account, so both must clear it across identities. The wallet timeout
surfaces as a bare untyped error both must translate. Confirmation
reviews carry payloads no host can decode, so both must summarize
modestly and defer to the wallet. Each of these is a candidate engine
improvement (account-scoped storage, a typed timeout error, renderable
reviews). Filing those upstream deletes the duplication. Centralizing it
in a dotli package would merely relocate it and make the upstream gap
harder to see.
The revisit trigger is concrete: when a second consumer inside this repo
needs the pooling, the network presets, or the presentation layer, extract
then, and let that consumer decide what crosses the published/private
boundary.
Evidence
All of it is re-runnable, and none of it is simulated:
and a real product client round-trips through it.
before any socket, full lifecycle observed live (Pairing, Authenticating,
Connected), session restore in about 600ms without the phone.
entropy.deriveoutput isbyte-identical to the consumer's existing derivation across three
contexts, so previously encrypted data stays readable.
stack through the engine from one terminal process and created real
repositories via an Asset Hub contract, signed by the phone, owned by the
product account that already existed. No ownership moved.
turbo run testlike every otherpackage) boot the real wasm engine: a product localStorage round trip
over the loopback, offline pairing presentation, logout clearing, plus
unit coverage for the stores, the pool demultiplexer, and the ordering
shim. The built dist was additionally smoke-tested under plain node, and
npm publish --dry-runpasses.One measured hazard deserves every reviewer's attention. The chain-head
relay can deliver an operation's events before the start-response that
names its
operationId. polkadot-api drops such events silently and theread never settles (measured: hung 3 runs in 6, with a 155-request retry
storm, and inversion count correlated with the hang 6 out of 6). The
package ships
serializeOperationStartsfor the product side, which gave 6clean runs out of 6. Browsers happen to win this race, which is why the web
app has never seen it. This is candidate upstream-report material for the
engine repo, now with numbers attached.
Costs, honestly
This is the first genuinely published dotli package, and publishing is the
main cost, not the code:
ownership for
@dotli, anNPM_TOKENsecret, release discipline, andchangelog upkeep. The workflow exists, but someone must own the process.
releases. Published packages version independently (
host-clistarts at0.1.0) and release via their own tags. The rule is one line:
app-versioned means private, published means independent. It is still a
second regime where there used to be one.
Consumer contracts, semver discipline, and API review are new muscles
here.
code that only one downstream consumer runs rots silently. The
mitigation ships with the package: its test suite boots the real wasm
engine headless in CI rather than mocking it, so the hosting path is
executed on every run.
package or write their own host. Accepted as symmetric with the web.
qrcode, readline usage) now live in abrowser-focused monorepo. They are isolated to this one package, which
also deviates (documented, deliberately) to a NodeNext tsconfig because
it ships built ESM for node.
does not export its typed-to-raw adapter, so the package reaches it by
file URL (upstream packaging ask). And product-sdk consumers currently
reach any embedded host through a test-only injection point
(
setTruApiClient). The consumer integration is defining what thesupported seam should look like before that ask is filed.
Alternatives considered
SDK must not ship a host, and today's
product-sdk-terminal/hostexportis exactly the inversion being removed.
raw SCALE callback dispatch per app, drifting against every engine
release, with every app re-learning the hazards listed above one outage
at a time.
is frozen. A live, evolving host vendored beside a live web host is
precisely where drift happens, and every vendored copy is one engine
release away from diverging silently.
and the honest fallback if maintainers do not want the publishing
obligation here. Costs: a third place where host conventions live, and
the web/terminal pairing identities drift apart. If the publishing burden
is the blocker, this is the escape hatch to discuss.
What this asks of maintainers
set-version.tscarve-out). It was built against the repo'sCONTRIBUTING rules, including the test-story format.
@dotlinpm scope and provideNPM_TOKENif accepted.published packages independent.
host-clireleases going forward.One repo-level note discovered while wiring provenance: the root
package.jsonstill declaresrepository: paritytech/dotli, which npmprovenance rejects. The new package pins
dotli-communityfor itself, butthe root field deserves its own fix.
🤖 Generated with Claude Code