npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@parity/truapi-host

v0.31.0

Published

WASM-backed TrUAPI host runtime: embeds the Rust core, with web iframe and Web Worker entry points

Downloads

7,811

Readme

@parity/truapi-host

WASM-backed TrUAPI host runtime. It embeds the truapi Rust core (compiled to WASM) behind a Web Worker provider, plus per-environment integration entry points. It is the counterpart to the native Android/iOS host shells.

Entry points

The package exposes tree-shakeable subpath exports — import only what your environment needs:

| Import | Provides | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | @parity/truapi-host | Shared runtime types plus generated typed host callback contracts. | | @parity/truapi-host/markers | CORE_MARKERS, the core's table of which log markers may leave the device and how loud each one is. | | @parity/truapi-host/web | Browser pairing host: createIframeHost (iframe MessageChannel handshake) and createWebWorkerPairingHostRuntime. | | @parity/truapi-host/node | Node pairing host: createNodePairingHostRuntime (core in a worker thread) and createHeadlessHostCallbacks. | | @parity/truapi-host/worker-runtime | Web Worker entrypoint (import with your bundler's ?worker suffix) so the WASM core runs off the page main thread. | | @parity/truapi-host/wasm/web | The raw browser wasm-bindgen glue, if you need to instantiate the core yourself. | | @parity/truapi-host/testing | createMockHost: the in-memory host seam a product is tested against. | | @parity/truapi-host/testing/playwright | Playwright fixture that boots the test host and embeds the product in an iframe. | | @parity/truapi-host/testing/server | Node server that serves the host page and its bundle. | | @parity/truapi-host/testing/client | createMockClient: a product client over the mock with no iframe, for unit tests. | | @parity/truapi-host/testing/dev-accounts | Named dev accounts derived from fixed BIP-39 entropy, which sign for real. | | @parity/truapi-host/testing/host-page | The browser half the fixture drives, for a suite that boots its own page. | | @parity/truapi-host/wasm/testing | The raw glue for the signing-enabled bundle the test host runs on. |

A suite moving from @parity/host-api-test-sdk follows the migration guide.

scripts/build-wasm.mjs builds two WASM bundles, both --no-default-features. wasm/web is the production browser host and excludes WasmSigningHostRuntime; wasm/testing adds the Rust wasm-signing-host and test-host features, which is what lets the test host hold keys and answer resource allocation as granted without allocating anything.

ProductRuntimeConfig configures both host roles. stack is the Trinity stack the host build runs:

  • pointer: the stack's link, polkadotapp://stack?key=0x…&url=….
  • descriptor: the bytes of the signed copy the build bundles. The core checks it against the pointer's key.
  • allowsReservedValues: set only by a Polkadot or Polkadot-fork build. Without it the core refuses Polkadot's genesis hashes and the dot TLD.

The core takes every chain it uses, the network getChainInfo reports and the signing host's TLD from the descriptor. It refuses a pointer without a key and a descriptor without a People or Bulletin chain. Without an Asset Hub no product manifest resolves, so a cross-product grant that is not cached is refused.

The test host has no descriptor, so it passes testStack instead: the network, the TLD and each chain's genesis hash. Only the wasm/testing bundle reads it. mockRuntimeConfig fills it with the MOCK_GENESIS chains, and the Playwright fixture builds it from networks.

Bundler requirements

The worker imports the WASM glue by a literal specifier, so every bundler resolves it statically and emits truapi_server.js as a chunk. Whether the truapi_server_bg.wasm payload comes with it depends on the bundler: emitting it requires treating new URL("truapi_server_bg.wasm", import.meta.url) inside the glue as an asset reference, and not all of them do.

| Bundler | Emits the .wasm? | Host action | | ------------------- | ------------------ | -------------------------------------------------------------- | | Vite | Yes | None — no copy step, and don't reach into dist/wasm/web/. | | webpack 5 | Yes | None. | | Rollup (standalone) | No | Add @web/rollup-plugin-import-meta-assets, or copy manually. | | esbuild | No | Copy manually (see below). | | Bun (bun build) | No | Copy manually (see below). |

esbuild and Bun pass new URL(..., import.meta.url) through verbatim: the build succeeds and the glue chunk is emitted, but no .wasm is written and the worker 404s at runtime. No flag changes this — --loader:.wasm=file only fires on import statements, never on new URL. Hosts on those bundlers must copy truapi_server_bg.wasm out of @parity/truapi-host/dist/wasm/web/ into the same output directory as the emitted truapi_server-*.js chunk, since the glue resolves the payload relative to its own URL.

Running Vite under Bun (bunx --bun vite build) uses Vite's bundler and is unaffected; only bun build is.

The literal import makes the worker a code-split chunk, so a Vite host must ask for ES workers; the default iife format cannot code-split and fails the build:

export default defineConfig({ worker: { format: "es" } });

Only the .wasm the glue references is emitted, and bundlers content-hash it — Vite writes assets/truapi_server_bg-<hash>.wasm, webpack writes a bare <hash>.wasm. The .wasm.gz / .wasm.br sidecars under dist/wasm/web/ therefore cannot be copied into a host's output: gzip_static / brotli_static serve <request-path>.gz / .br, and the request path now carries the bundler's hash. Hosts that serve precompressed assets should generate them from their own build output, after hashing — either a post-build pass over dist (gzip level 9 and brotli max quality reproduce the sidecars byte for byte) or a bundler plugin:

import { compression } from "vite-plugin-compression2";

export default defineConfig({
  worker: { format: "es" },
  plugins: [
    compression({ include: [/\.(js|css|html|wasm)$/], algorithms: ["gzip"] }),
    compression({
      include: [/\.(js|css|html|wasm)$/],
      algorithms: ["brotliCompress"],
    }),
  ],
});

webpack hosts get the same result from compression-webpack-plugin. Skipping this ships the full 1.4 MB .wasm where about 600 kB (gzip) or 470 kB (brotli) would do — and a server configured with gzip_static but no dynamic gzip on has no fallback.

Optional capabilities

HostCallbacks groups are required except those listed on the Rust OptionalPlatform super-trait, which are emitted as optional members. Omit one and the core answers its product calls with Unsupported; supply it and the whole group must be implemented:

const callbacks: HostCallbacks = {
  navigation,
  notifications,
  // ...required groups...
  chat, // optional: leave it out and chat products get `Unsupported`
  permissionStatus, // optional: reports live OS permission state
  pocket, // optional: serves the host's Pocket card collection
  game, // optional: holds the host's game reminders
  contacts, // optional: leave it out and contacts calls get `Unsupported`
  scanner, // optional: draws the host's QR and barcode viewfinder
};

permissionStatus.devicePermissionStatus must answer from the OS without prompting. Supply it and the core revalidates a stored device grant against it before answering the product, so a capability the OS has since revoked or reset stops reading as usable. Omit it and a stored grant answers on its own.

pocket serves the host's card collection. subscribePocketCards emits the calling product's cards and every later replacement, and removePocketCard takes one out. The host owns the collection: removing an absent card succeeds, and a card the host pins is refused with Privileged.

game holds the host's game reminder. scheduleGameReminder replaces the product's held reminder, and cancelGameReminder drops it. The core asks for no per-product consent, so the host asks the platform for what the reminder needs, and a rejected schedule reaches the product as a host failure. A host keeps one reminder per product. The core serves game to the game product, dim2, alone. The mock test host (@parity/truapi-host/testing) accepts every reminder and cancel without holding them once it runs as that product; its default mock.dot gets Unsupported, so a suite that exercises game passes productId: "dim2.dot".

Under createWebWorkerPairingHostRuntime the presence of each optional group is reported to the worker in its init message, so the core sees the same capability set on both sides of the boundary.

Product-rendered bodies

A host can ask a product to draw one body — a chat message, an input-widget candidate, a Pocket card or its back — and send back what the user does with it. Both entry points live on the product provider and are present only on runtimes holding a live channel to the core:

import type { RenderContext } from "@parity/truapi";

// `payload` here is the product-defined body, hex-encoded.
const context: RenderContext = {
  tag: "ChatMessage",
  value: { roomId, messageId, messageType },
};

const stop = provider.render!(
  { context, payload },
  {
    onUpdate: (node) => setTree(node), // complete replacement tree each time
    onComplete: () => setTree(null),
    onError: (error) => console.warn(error),
  },
);

// A button inside the rendered tree was tapped:
await provider.publishRendererAction!({
  context,
  actionId,
  payload: "0x", // a `Button` press carries no data
});

stop(); // stop rendering; safe to call more than once

A TextField value change instead carries the UTF-8 bytes of the new value, with no length prefix.

render reports failure through onError rather than throwing, so one dead render cannot take the surrounding surface with it. Exactly one terminal fires per render: onComplete means the last tree delivered stands, onError means it is partial and must not be shown as final. A product that declines the render, a tree that fails to decode, a closed connection, and a throwing renderer all arrive as onError. An open render holds one worker reference for the provider's product, released when the stream ends or the disposer runs, so the product's worker stays up for as long as something is being drawn.

publishChatAction is the path for posted messages, commands and host-drawn Actions buttons. Each action entry point sits behind its own service's access policy: the renderer refuses a connection that is not a Worker execution, and chat additionally requires a live session.

Three rules the core cannot check are the host's to keep: send a render context only for a surface the product's manifest includes, send PocketCardBack only for a card whose definition declares back, and publish a renderer action only from the current tree of an open render stream.

Product account addresses

A host that stores the core's ProductSubtree slot can name the account a review will sign with, so the review can carry an address and a fee rather than a bare derivation path. Both calls are pure and need no runtime or session, so default() alone is enough:

import init, {
  deriveProductAccountPublicKey,
  productAccountAddress,
} from "@parity/truapi-host/wasm/web";
import { DerivationIndex } from "@parity/truapi";

await init();

// `subtreePublicKey` is the 32 bytes read from the host's own
// `ProductSubtree { sessionId, productId }` slot.
const publicKey = deriveProductAccountPublicKey(
  subtreePublicKey,
  DerivationIndex.enc(account.derivationIndex),
);
const address = productAccountAddress(publicKey);

The index crosses as a SCALE-encoded DerivationIndex, the same value a review already carries, so the 32-byte chain code behind it stays core-owned and a host never reconstructs it. productAccountAddress applies the prefix host-spec C.6 fixes, rather than leaving each host to choose one.

The same name in a test suite

@parity/truapi-host/testing/playwright exports a second productAccountAddress. It is asynchronous, it takes the account and product to derive rather than a public key, and it loads the testing WASM bundle itself:

import { asProductId } from "@parity/truapi";
import { productAccountAddress } from "@parity/truapi-host/testing/playwright";

const address = await productAccountAddress({
  account: "bob", // a dev account name, or a `DevAccount`
  productId: asProductId("tx-demo.dot"),
  index: 0, // optional, defaults to 0
});

Which one to reach for:

  • productAccountAddress(publicKey) from @parity/truapi-host/wasm/web is synchronous and formats a subtree-derived public key the host already holds. This is the one a host ships.
  • productAccountAddress(query) from @parity/truapi-host/testing/playwright is asynchronous and runs the whole derivation from a dev account's session root. It needs the built testing bundle, so it is for suites only. Because the address depends on nothing but the root, the product id and the index, a suite can work it out in a globalSetup and fund it once rather than per test.

A running fixture answers the same address through testHost.getProductAccountAddress(productId?, index?), which reads the session the host actually holds and so returns undefined while it is signed out.

contacts needs both callbacks, or the group counts as absent. pickContact draws the picker and returns the chosen account, or NoContacts when there is nobody to show. contacts({ handleKey, handles }) resolves the handles a transaction names: one entry per handle, in order, the account or undefined. A contact's handle is BLAKE2b-256 keyed with handleKey over its 32-byte account (blake2b(account, { key: handleKey, dkLen: 32 }) in @noble/hashes). The core re-checks every account returned. It caches what it resolves, so call notifyContactsChanged() whenever a contact is removed or blocked. Omit blocked contacts from both. See the contacts RFC (docs/rfcs/contacts-api.md).

scanner.scanCode(product, request) opens the host's viewfinder. The rules it follows are on ScannerPlatform in the generated callbacks. The mock test host serves a scanner only when created with createMockHost({ scanner: answer }), and setScanAnswer(answer) changes the next answer.

Generated WASM artefacts

The ignored bundle under dist/wasm/web/ is built with host-owned chain access. Hosts wire their JSON-RPC provider through chainConnect; if they omit it, chain calls fail with the core's standard unavailable error. Release builds use the workspace size-optimized Rust profile plus wasm-opt -Oz, validate that debug/name/producers custom sections were stripped, and emit .wasm.gz and .wasm.br sidecars for hosts that serve precompressed assets.

Build them after editing rust/crates/truapi and before packaging, publishing, or running tests that load the raw WASM bundle (requires wasm-pack on PATH):

npm run build:wasm   # or `make wasm` from the repo root

Example — browser (Web Worker)

import HostWorker from "@parity/truapi-host/worker-runtime?worker";
import { createWebWorkerPairingHostRuntime } from "@parity/truapi-host/web";

const runtime = await createWebWorkerPairingHostRuntime(
  new HostWorker(),
  callbacks,
  {
    hostConfig,
  },
);

const firstProvider = await runtime.createProvider({ productId: await runtime.parseProductId("first.dot") });
const secondProvider = await runtime.createProvider({
  productId: await runtime.parseProductId("second.dot"),
});

@parity/truapi-host/web also exports createIframeHost for the protocol-iframe MessageChannel handshake. A product that navigates its whole document connects again: onPort is called with a fresh port once the new document sends a frame, and the function onPort returned for the previous port runs then. Each port is a new product session, so give it a core connection of its own. With an opaque sandbox only the first document is connected. Host code creates one worker runtime and then opens one provider per product ID. runtime.parseProductId(input) validates and normalizes through the already loaded Rust worker. It returns the same ProductId branded string exported by @parity/truapi, so IDs remain ordinary strings in memory and across worker messages.

Example — Node (worker thread)

For a host with no browser, such as a CLI that pairs with the user's wallet. createNodePairingHostRuntime takes the same callbacks and options as createWebWorkerPairingHostRuntime and returns the same runtime; the core runs in a node:worker_threads thread and its WASM is read from disk.

import { createClient, createTransport } from "@parity/truapi";
import { createNodePairingHostRuntime, createHeadlessHostCallbacks } from "@parity/truapi-host/node";

const runtime = await createNodePairingHostRuntime(
  createHeadlessHostCallbacks({
    coreStorage, // persists the paired session; files on disk for a CLI
    chain, // opens a JSON-RPC WebSocket per genesis hash
    auth: { authStateChanged: (state) => state.tag === "Pairing" && showQr(state.value.deeplink) },
    productStorage,
    servedChains, // genesis hashes `chain` can reach
    userConfirmation, // the user's answer to reviews, e.g. a CLI prompt
    permissions,
  }),
  { hostConfig },
);
const client = createClient(createTransport(await runtime.createProvider({ productId: "my-cli.dot" })));
await client.account.requestLogin({});

createHeadlessHostCallbacks fills in what a host without a screen cannot do: navigation and push notifications reject, and theme, locale and preimage lookup answer with fixed values. Every chain builds V4 transactions unless the host passes extrinsicVersion with the chains it builds as V5. The host still passes userConfirmation and permissions, because they are the user's answers: getAccount asks to disclose the product subtree, and signRaw asks for the ChainSubmit permission and a signing review. A CLI asks at its prompt. A host with a UI passes the full callback set instead, exactly as on the web.

./node loads the production bundle, which has no signing host, so it runs pairing hosts only: role: "signing" is refused.

Call runtime.dispose() when done: the core's thread keeps the process alive until then. Install the package rather than bundling ./node into a single file, since the worker thread and the WASM are loaded from files beside it.

Session lifecycle

The core owns the session; the host owns persistence. At boot the core restores the AuthSession slot on its own and reports the outcome through the auth callback, Disconnected included, so a host waits for the first authStateChanged instead of treating silence as "signed out". Every transition below reports the resulting AuthState the same way.

| Runtime method | Use it to | | ------------------------------- | ---------------------------------------------------------------------------- | | activateStoredSession() | Await the restore of the AuthSession slot before opening providers. | | activateExternalSession(blob) | Install a session the host holds itself, without writing it to core storage. | | notifySessionStoreChanged() | Tell the core the persisted blob may have changed; it re-reads it. | | notifyContactsChanged() | Tell the core a contact was removed or blocked; it drops cached handles. | | disconnectSession() | Log out: clears the session and notifies the peer. | | resetSessionState() | Drop the local session without notifying the peer. |

The boot order is create the runtime, restore, then open providers:

const runtime = await createWebWorkerPairingHostRuntime(
  new HostWorker(),
  callbacks,
  { hostConfig },
);

// Resolves once product frames may use the restored session; rejects when
// there was nothing to restore.
await runtime.activateStoredSession().catch(() => {});

const provider = await runtime.createProvider({ productId: await runtime.parseProductId("first.dot") });

Statement-store traffic of the host's own

A host that runs its own statement-store traffic — P2P chat, device sync — signs with the account its device advertises in the pairing QR. The paired wallet registers that account's statement-store allowance while answering the handshake, so it is the only account whose statements the network accepts from this host. A locally minted key gets no allowance, and every submission fails with no allowance set for account.

| Value | Where | | ---------------------------------------- | -------------------------------------- | | SessionUiInfo.deviceStatementAccountId | On every AuthState.Connected | | getDeviceStatementKey() | Runtime method, 64-byte sr25519 secret |

Peers are told to address that same account, so it is also what a host derives its own statement topics from. Both are undefined without an active session, and the account is rotated per login: read it from the current session rather than caching it across sign-ins.

Worker lifecycle

A product has one worker, and the core keeps one reference count per worker. The host takes a reference while a modality holder is on screen or in flight, such as a chat room the product serves, and releases it when the holder leaves. The host runs and stops the worker executable itself; the runtime only tells it when demand crosses zero.

| Runtime method | Use it to | | --------------------------------- | ---------------------------------------------------------------- | | acquireWorker(productId) | Take one reference while a holder is on screen or in flight. | | releaseWorker(productId) | Release one reference; with none held it is a no-op. | | subscribeWorkerDemand(listener) | Learn which workers to run: current set first, then each change. |

const stop = runtime.subscribeWorkerDemand(({ productId, wanted }) => {
  if (wanted) startProductWorker(productId);
  else stopProductWorker(productId);
});

runtime.acquireWorker("chat-bot.dot"); // a room it serves came on screen
runtime.releaseWorker("chat-bot.dot"); // the room left the screen

Two holders of one product hold one worker: the listener hears wanted: true once, on the first, and wanted: false once, after the last. A wanted: false is permission to stop, not an order: a host may keep the worker warm.

Debugging (dev-only)

The worker can stream every product↔core wire frame to the wire debugger. It is off by default and the embedding host decides; the product needs no changes. Two conditions must both hold or nothing dials and the core installs no tap:

  1. The host page is a dev build. The dial sits behind a hard import.meta.env.DEV gate, which bundlers replace with a boolean literal: in a production bundle that gate is false, so no option can turn the tap on. A production build that shows no frames is this gate, not a broken debugger. NODE_ENV=development is what opens the gate under Vite.

  2. A ws:// loopback URL reaches the runtime, from one of two places. The host's own value wins over the build's, so the build's is a default and never an override:

    // 1. the host passes it, the normal path, where the host stays in control
    await createWebWorkerPairingHostRuntime(worker, callbacks, {
      hostConfig,
      debugger: "ws://127.0.0.1:9231", // null or "" refuses the dial outright
    });
    # 2. or compile a default in, which is what a local stack does: every
    #    browser profile that opens the build dials, with nothing to switch on
    VITE_TRUAPI_DEBUGGER_URL=ws://127.0.0.1:9231 vite build

    Passing null or "" is how a host refuses the dial even when the build carries one; omitting the field takes the build's value.

    While a dial is live the host shows a small fixed-position badge naming every endpoint frames are going to, so a tap left on from an earlier session is visible rather than buried in a console line. Pass debuggerIndicator: false to suppress it, and only when the host renders its own signal, since the point is that a host streaming frames is never silent about it. One runtime suppressing the badge leaves another runtime's badge alone.

    The dial is resolved once, when the runtime is created, and cannot be changed from the page afterwards, so whether this session is observed is a property of the build and the host, not of anything typed into a console later.

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 }.

The URL must be ws:// on a loopback host. Anything else — wss://, http://, a LAN or public address, a non-loopback hostname — yields an inert link and a wire debugger URL rejected console warning; there is no certificate or wss path. Prefer the literal 127.0.0.1 over localhost: localhost passes the gate, but it resolves ::1 first on macOS while the debugger binds 127.0.0.1 alone, so the same URL handed to a native host (truapi's WsDebugSink dials the first resolved address) silently never connects.

The debugger owns all decoding and decodes every frame it can, including signing and payment payloads; its safety is the dev-build gate above, not redaction. See js/packages/truapi-debugger/README.md for the tap, the envelope, and the host-dials-debugger topology.

Publishing

This package is published by the root Release workflow through paritytech/npm_publish_automation. Do not run npm publish locally. Cut a release: PR with a changeset for @parity/truapi-host; the workflow builds the generated host bindings, the browser WASM bundle, packs the tarball, and publishes it when the @parity/truapi-host@<version> tag does not already exist.

Architecture

JS host code
  protocol handlers / typed callbacks
  (types from @parity/truapi-host)
       |
       v
createWebWorkerPairingHostRuntime
  shared worker runtime: pairing session, chain runtime, WASM instance
       |
       +-- createProvider({ productId }) -> product core / WireProvider
       |
       +-- createProvider({ productId }) -> product core / WireProvider