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/product-sdk-terminal

v0.5.4

Published

QR code login and signing for CLI/terminal apps via Polkadot mobile wallet pairing

Readme

@parity/product-sdk-terminal

Migrated from @polkadot-apps/terminal v0.3.0 (paritytech/polkadot-apps).

QR code login, attestation, and transaction signing for CLI/terminal apps via the Polkadot mobile wallet.

Wraps the @novasamatech/host-papp SDK with Node.js-compatible adapters (file-based storage, WebSocket transport) so the full SSO protocol works outside the browser.

Installation

pnpm add @parity/product-sdk-terminal

Setup

Requires Node ≥21. The package relies on the global WebSocket exposed by Node 21+ (via @polkadot-api/[email protected]). On older Node versions the WebSocket connection fails at runtime with WebSocket is not defined.

Note (≥0.3): The @parity/product-sdk-terminal/register WASM loader hook has been removed. It existed only to patch verifiablejs (a browser-only inline-WASM dependency of @novasamatech/host-papp). host-papp ≥0.7.9 no longer uses verifiablejs — its sr25519 primitives are pure JS — so no loader hook is needed. Drop any --import @parity/product-sdk-terminal/register flags from your node/tsx invocations.

Quick Start

import { createTerminalAdapter, renderQrCode, waitForSessions } from "@parity/product-sdk-terminal";

// 1. Create the adapter
const adapter = createTerminalAdapter({
    appId: "my-terminal-app",
});

// 2. Subscribe to pairing status to show the QR code
adapter.sso.pairingStatus.subscribe(async (status) => {
    if (status.step === "pairing") {
        console.log(await renderQrCode(status.payload));
        console.log("Scan with the Polkadot mobile app...");
    }
});

// 3. Authenticate (QR pairing + on-chain attestation)
const result = await adapter.sso.authenticate();

result.match(
    (session) => console.log("Logged in!", session?.id),
    (error) => console.error("Failed:", error.message),
);

// 4. Wait for sessions to load (they load asynchronously from disk)
const sessions = await waitForSessions(adapter, 2000);

// 5. Sign messages via the paired wallet
if (sessions.length > 0) {
    const session = sessions[0];
    const signer = createSessionSigner(session, adapter);
    // use signer with polkadot-api transactions
}

API

createTerminalAdapter(options): TerminalAdapter

Creates a terminal adapter backed by the host-papp SDK.

Options:

  • appId -- unique app identifier (used as storage namespace)
  • endpoints? -- statement store WebSocket endpoints (defaults to Paseo)
  • hostMetadata? -- optional host environment info
  • storageDir? -- override the on-disk session directory (defaults to ~/.polkadot-apps/). Useful in tests and containerised environments.

Returns a TerminalAdapter with:

  • appId -- the value you passed in (re-exposed so createSessionSigner can pull the productId from the adapter)
  • sso -- auth component (.authenticate(), .abortAuthentication(), status subscriptions)
  • sessions -- session manager (signing, disconnect)
  • destroy() -- disconnect the WebSocket and release resources. Idempotent. Suppresses @novasamatech/statement-store's noisy Statement subscription error log for ~50 ms after the call.

createSessionSigner(session, adapter): PolkadotSigner

Creates a PolkadotSigner backed by a QR-paired mobile wallet session, using the session's default account (derivationIndex: 0) under the adapter's appId. This is the right entry point for ~all CLI flows.

const [session] = adapter.sessions.sessions.read();
const signer = createSessionSigner(session, adapter);
await contract.publish.tx(domain, cid, { signer, origin });

createSessionSignerForAccount(session, ref): PolkadotSigner

Escape hatch for signing as a non-default sub-account of a paired session, or as a productId that differs from the adapter's appId. Most callers don't need this.

ref is { productId: string; derivationIndex: number }:

  • productId -- dotNS-style identifier of the requesting product. In normal usage this equals the adapter's appId; pass a different value only if you have an explicit reason.
  • derivationIndex -- BIP32-style child-key index. 0 is the default account; non-zero indices reach additional sub-accounts derived from the same root.
const subSigner = createSessionSignerForAccount(session, {
    productId: "my-product",
    derivationIndex: 3,
});

Wire format note: @novasamatech/host-papp 0.7 expects productAccountId: [productId, derivationIndex] in SigningRawRequest. Both functions above hide that tuple — pass an adapter for the default case or a named-fields object for the escape hatch.

renderQrCode(data, options?): Promise<string>

Render a string as a QR code using Unicode half-block characters for terminal display.

createNodeStorageAdapter(appId, storageDir?): StorageAdapter

File-based storage adapter for Node.js. Data persists in storageDir (defaults to ~/.polkadot-apps/).

waitForSessions(adapter, timeoutMs?): Promise<UserSession[]>

Waits for the session list to emit at least one entry, or resolves with [] after timeoutMs.

Allowance signers — the canonical path

For CLIs that need to write to Bulletin or publish to the Statement Store: ask the paired wallet for an allowance slot, get a PolkadotSigner back, sign extrinsics with it. This is what most consumers want.

import {
    createTerminalAdapter,
    getBulletinSigner,
    getStatementStoreProver,
    waitForSessions,
} from "@parity/product-sdk-terminal";

const adapter = createTerminalAdapter({ appId: "my-cli" });

// ...QR pair the phone, wait for the session...
await waitForSessions(adapter);

// First call prompts the wallet for an allowance slot; subsequent calls
// return the cached slot key — no wire round-trip.
const bulletinSigner = await getBulletinSigner(adapter, "my-cli.dot");
await bulletinClient.tx.TransactionStorage.store({ data }).signAndSubmit(bulletinSigner);

// Same idea, returns a StatementProver for `@novasamatech/statement-store` writes.
const prover = await getStatementStoreProver(adapter, "my-cli.dot");

getBulletinSigner(adapter, productId, sessionId?): Promise<PolkadotSigner>

getStatementStoreProver(adapter, productId, sessionId?): Promise<StatementProver>

Both default sessionId to the only paired session. With zero or more than one paired sessions and no explicit id, both throw AllowanceError with reason: 'NoSession'. Pass an explicit sessionId to disambiguate.

Failures from the host (Rejected, NotAvailable, codec drift) surface as AllowanceError:

import { AllowanceError } from "@parity/product-sdk-terminal";

try {
    const signer = await getBulletinSigner(adapter, "my-cli.dot");
} catch (err) {
    if (err instanceof AllowanceError && err.reason === "Rejected") {
        // user denied the allowance request on the phone
    }
    throw err;
}

Behind the scenes both helpers wrap adapter.allowance (inherited from host-papp's PappAdapter) and unwrap its neverthrow ResultAsync into a throwing Promise. If you want to keep neverthrow's .match() / .mapErr() ergonomics or need explicit multi-session handling, call adapter.allowance.getBulletinSigner(sessionId, productId) / getStatementStoreProver(...) directly.

Cache-only probes — hasBulletinAllowance / hasStatementStoreAllowance

For paths that must run silently — login health checks, "would calling getBulletinSigner prompt the phone?" decisions, readiness probes — use the cache-only variants. They read host-papp's on-disk allowance file directly and never trigger a wallet prompt.

import {
    getBulletinSigner,
    hasBulletinAllowance,
} from "@parity/product-sdk-terminal";

if (await hasBulletinAllowance(adapter, "my-cli.dot")) {
    // happy path — fetch the signer without risking a wallet prompt
    const signer = await getBulletinSigner(adapter, "my-cli.dot");
} else {
    // tell the user a wallet prompt will fire, then call getBulletinSigner
    console.log("Approve the allowance request on your phone…");
    const signer = await getBulletinSigner(adapter, "my-cli.dot");
}

hasBulletinAllowance(adapter, productId, sessionId?): Promise<boolean>

hasStatementStoreAllowance(adapter, productId, sessionId?): Promise<boolean>

Resolves true when a slot key for (sessionId, productId, resource) is already cached on disk; false when it is not (file absent, or present with no matching entry). Same sessionId defaulting as the fetching helpers: defaults to the only paired session, throws AllowanceError('NoSession') for zero or multiple paired sessions without an explicit id.

Rejects only on decrypt or decode failures — a corrupted cache file is a real failure, not a "no allowance" signal.

The cache-only check uses an internal mirror of host-papp's AllowanceRepository codec because host-papp doesn't expose a public probe today. We'll switch to the upstream API when one ships; the public surface (hasBulletinAllowance / hasStatementStoreAllowance) won't change.

Allowance management — manual cache + AP control (./host subpath)

For CLIs that need finer control — pre-allocating multiple resources in one wallet prompt, inspecting the cache before round-tripping, or building a signer over a cached key without consulting the wallet — the @parity/product-sdk-terminal/host subpath exposes the lower-level Host-runner surface (the RFC-10 Accounts Protocol companion, an on-disk allowance-key cache, and a local sr25519 signer over the cached keys). Most CLIs don't need this — start with getBulletinSigner / getStatementStoreProver above.

import {
    ensureSlotAccountSigner,
    requestResourceAllocation,
    getCachedAllocation,
    createSlotAccountSigner,
} from "@parity/product-sdk-terminal/host";

// First call prompts the wallet; subsequent calls are wire-free.
const signer = await ensureSlotAccountSigner(session, adapter, {
    tag: "BulletInAllowance",
    value: undefined,
});
await bulletinTx.submitAndWatch(signer);

ensureSlotAccountSigner(session, adapter, resource): Promise<PolkadotSigner>

The one call most CLIs want. Cache hit → returns signer; cache miss → allocates via the wallet, then returns signer. Throws on Rejected / NotAvailable. Slot-table resources only (BulletInAllowance, StatementStoreAllowance).

requestResourceAllocation(session, adapter, resources, options?): Promise<ApAllocationOutcome[]>

Lower-level: pre-allocate any combination of resources. Granted keys cached at {storageDir}/{appId}_AllowanceKeys.json (0o600, atomic write). onExisting auto-picks Ignore/Increase based on cache state; override via options.onExisting.

getCachedAllocation(adapter, resource): Promise<CachedAllocation | null>

Read-only cache lookup; no wire round-trip.

createSlotAccountSigner(adapter, resource): Promise<PolkadotSigner | null>

Build a PolkadotSigner from the cached slot key without round-tripping. Returns null if nothing's cached (use ensureSlotAccountSigner for the round-trip-if-missing flow).

Caveats. AutoSigning currently returns NotAvailable on both Android and iOS wallets. Slot renewal at expiry depends on paritytech/individuality#931 (open) — first-time allocation works end-to-end today.

Migration from @polkadot-apps/terminal

For consumers moving from @polkadot-apps/terminal v0.2.0 / v0.3.0. Existing sessions on disk (~/.polkadot-apps/) carry over — same appId, same path. No re-pairing required for the migration itself.

| Concern | @polkadot-apps/terminal | @parity/product-sdk-terminal | | --- | --- | --- | | Package name | @polkadot-apps/terminal | @parity/product-sdk-terminal | | WASM loader hook | --import @polkadot-apps/terminal/register (required) | removed — no --import needed (host-papp ≥0.7.9 dropped verifiablejs) | | createTerminalAdapter | async — returned Promise<TerminalAdapter> | sync — returns TerminalAdapter directly. Drop the await. | | Default account signer | createSessionSigner(session) | createSessionSigner(session, adapter) — pass the adapter as second arg | | Non-default sub-account signer | not exposed | createSessionSignerForAccount(session, { productId, derivationIndex }) | | Override session storage dir | not supported (hard-coded ~/.polkadot-apps/) | createTerminalAdapter({ ..., storageDir }) option | | E2E test helper for sessions | none | createTestSession from @parity/product-sdk-terminal/testing | | Node version | any (bundled ws) | ≥21 (uses global WebSocket) | | destroy() shutdown noise | emitted Statement subscription error to stderr | suppressed; console.error muted for ~50 ms |

Why the signer API changed

@novasamatech/host-papp 0.7 replaced SigningRawRequest.address with productAccountId: [productId, derivationIndex]. The wire format requires both fields, so a session-only argument is no longer enough — the signer needs to know which sub-account of which product is asking. We split that into two functions to keep the common case ergonomic:

  • createSessionSigner(session, adapter) for the default account (uses [adapter.appId, 0])
  • createSessionSignerForAccount(session, { productId, derivationIndex }) for everything else

The single-argument createSessionSigner(session) from @polkadot-apps/terminal no longer works against host-papp 0.7 regardless of which package you use.

Migration steps

  1. Replace the dep: pnpm remove @polkadot-apps/terminal && pnpm add @parity/product-sdk-terminal
  2. Remove the --import @.../register flag from your node / tsx invocations or package.json scripts — the WASM loader hook no longer exists (and is no longer needed).
  3. Drop await in front of createTerminalAdapter(...) calls.
  4. Update each createSessionSigner call site: change createSessionSigner(session)createSessionSigner(session, adapter).
  5. Verify Node version is ≥21 (node --version).

If your existing sessions don't appear after migrating, double-check that the appId is identical to what you used in @polkadot-apps/terminal — the on-disk file names depend on it.

Testing

The @parity/product-sdk-terminal/testing subpath exports createTestSession, a helper that synthesizes a valid persisted session on disk. E2E tests can inject a known-good session without going through QR pairing + attestation:

import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createTerminalAdapter, waitForSessions } from "@parity/product-sdk-terminal";
import { createTestSession } from "@parity/product-sdk-terminal/testing";

const storageDir = mkdtempSync(join(tmpdir(), "e2e-"));
const { sessionId } = await createTestSession({
    appId: "my-terminal-app",
    storageDir,
});

const adapter = createTerminalAdapter({
    appId: "my-terminal-app",
    storageDir,
});
const sessions = await waitForSessions(adapter);
// sessions[0].id === sessionId

Limits and usage notes.

  • Signing does not round-trip. Both session.signRaw and session.signPayload go out over the statement store and expect a real phone to respond. Use this helper for flows that test session discovery, persistence, and logout — not happy-path signing.
  • Expiry tests still work. The synthesized local account was never registered on the People chain, so any statement-store write from this session fails with NoAllowanceError. That's the same error the CLI sees when a previously valid session's on-chain attestation has expired.
  • No expiresAt option. The on-disk codec has no expiry field; validity lives on chain.
  • Corrupted-session cases don't need a helper — fs.writeFile("<storageDir>/<appId>_SsoSessions.json", "not-hex") from the test is enough.

Signing

After login and attestation, the paired wallet can sign both transactions and raw messages via the statement store. The PolkadotSigner returned by createSessionSigner routes each path to the right host-papp method:

  • Transactions (signTx from polkadot-api's perspective — what submitAndWatch, signSubmitAndWatch, contract method calls, etc. invoke) go through session.signRaw with the Payload tag. polkadot-api assembles the full SCALE-encoded signing payload from runtime metadata — callData ‖ extras ‖ additionalSigneds for every signed extension the chain declares — and hands the bytes to the wallet as an opaque hex blob. The wallet signs the payload as-is, with no envelope wrapping. Any signed extension declared by the runtime (including extensions polkadot-api doesn't know about, e.g. AsPgas on Paseo Next v2) survives end-to-end because the wallet doesn't inspect the bytes — it just signs them.
  • Raw bytes (signBytes) go through session.signRaw with the Bytes tag. Mobile applies the standard <Bytes>...</Bytes> anti-phishing wrap before signing — appropriate for arbitrary data, the same behavior signRaw has across all Polkadot wallets.

Note on previous bugs. Versions prior to 0.1.1 routed all signing through signRaw with no tag distinction, producing BadProof-rejected signatures. Versions 0.1.1 through 0.2.x then used polkadot-api/pjs-signer, which threw PJS does not support this signed-extension: <name> on any extension outside its eight built-in mappers (notably AsPgas). The current path bypasses both pitfalls.

Notes

WebSocket transport

The adapter uses @polkadot-api/[email protected], which relies on the global WebSocket exposed by Node ≥21. Older Node versions (18, 20) will fail at connect time with WebSocket is not defined — upgrade Node, or pass an explicit websocketClass from the ws package.

The default WebSocket is constructed without followRedirects: true, so endpoints behind an HTTP redirect will fail to connect. If you must point at an endpoint that does, supply the resolved URL directly via the endpoints option.

How It Works

  1. QR Pairing -- generates Sr25519 + P256 keypairs, encodes a polkadotapp://pair?handshake=0x... deep link, subscribes to the statement store
  2. Attestation -- registers the local account on the People chain so it can publish statements
  3. Signing -- sends encrypted signing requests to the wallet via the statement store, receives signed responses

Sessions are persisted to ~/.polkadot-apps/ and survive across restarts. The SDK loads them asynchronously on startup — subscribe to adapter.sessions.sessions and wait for the first emission.

Dependencies

  • @novasamatech/host-papp -- Polkadot host-product SDK (auth, attestation, signing)
  • @novasamatech/statement-store -- statement store client and session management
  • @novasamatech/storage-adapter -- storage interface
  • @polkadot-api/ws-provider -- WebSocket JSON-RPC provider
  • neverthrow -- Result type for error handling
  • qrcode -- QR code generation

Future Work

  • KvStoreStorageAdapter bridge. This package implements its own file-backed StorageAdapter for Node.js (createNodeStorageAdapter). Once @parity/product-sdk-local-storage grows a file backend with the same read/write/clear/subscribe ResultAsync shape, replace node-storage.ts with a thin adapter over it.
  • Codec re-exports from @parity/product-sdk-statement-store. testing.ts imports session-account codec helpers (AccountIdCodec, LocalSessionAccountCodec, etc.) directly from @novasamatech/statement-store. Re-exporting them through the in-monorepo wrapper would let this package depend only on workspace siblings.
  • @noble/* major version drift. This package pins @noble/{ciphers,curves,hashes}: ^2.x because upstream @polkadot-apps/terminal did, and the testing.ts codec helpers use the v2 import paths (@noble/hashes/blake2.js, @noble/curves/nist.js). The rest of the monorepo is on ^1.x. Both majors coexist in the lockfile; not a runtime problem today but worth a coordinated bump. Either move the whole monorepo to v2, or rewrite testing.ts against v1 paths (@noble/hashes/blake2b.js etc.).