From f3a326c99d7b4f35d6b81e132ca40aff549a289a Mon Sep 17 00:00:00 2001 From: UtkarshBhardwaj007 Date: Mon, 18 May 2026 14:27:44 +0100 Subject: [PATCH 1/4] docs: add product-account unification design spec Captures the design for consolidating sr25519 product-account derivation in @parity/product-sdk-keys, locking it against frozen vitest vectors, surfacing username + product account in `dot init`, renaming the misleading blake2b util in product-sdk/identity, and deferring the "CLI as host" option as a roadmap note. --- ...5-18-product-account-unification-design.md | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-18-product-account-unification-design.md diff --git a/docs/superpowers/specs/2026-05-18-product-account-unification-design.md b/docs/superpowers/specs/2026-05-18-product-account-unification-design.md new file mode 100644 index 0000000..93a2736 --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-product-account-unification-design.md @@ -0,0 +1,221 @@ +# Product-account unification across playground-cli, playground-app, and product-sdk + +**Status:** Draft, pending user approval +**Author:** utkarsh.bhardwaj@parity.io +**Created:** 2026-05-18 + +## Problem + +The `playground-cli`, the `playground-app` web UI, and the underlying Polkadot hosts (mobile, desktop, dotli) each derive a "product account": the per-product public key the user signs with when interacting with `playground.dot`. Today this derivation is implemented in three places (CLI, desktop, mobile), and a fourth, *misleadingly named* utility lives in `product-sdk` using a completely different algorithm. There is no single source of truth, no fixture proving the implementations agree, and no on-screen confirmation in `dot init` that lets a user verify their CLI is acting as the same account that `playground-app` sees. + +This spec consolidates derivation in `@parity/product-sdk-keys`, locks it against a canonical fixture, surfaces the result in `dot init`, deprecates the misleading SDK util, and captures the deferred "CLI as host" option as a roadmap note. + +## Goals + +1. **One canonical implementation** of `deriveProductAccountPublicKey(parentPublicKey, productId, derivationIndex)`, living in `@parity/product-sdk-keys`. CLI imports it; no other consumer reimplements it. +2. **Algorithmic equivalence with the desktop host** locked by frozen vitest fixtures in product-sdk. Drift fails CI before it reaches consumers. +3. **`dot init` shows the user's root username (on-chain) and product account address (SS58 + H160)**, so the user can eyeball-confirm their CLI account matches what `playground-app` displays under "My apps." +4. **Rename the misleading `deriveProductAccount` in `@parity/product-sdk/identity`** (a blake2b util that is NOT the canonical product-account derivation) to a name reflecting its actual purpose. Hard rename, consistent across the product-sdk repo, no deprecation alias. +5. **Capture the "CLI as a host" option as a deferred roadmap note** with a written reason and a trigger for revisiting. + +## Non-goals + +- Making the CLI a host in the host-api sense (`host-container`-based). Deferred; rationale in the roadmap note (Section 5). +- Generating a CLI-only root mnemonic. Would defeat the shared-identity goal and is explicitly out of scope. +- Adding sr25519 *keypair* derivation (private + public) to `product-sdk-keys`. Only the public-key path is in scope. Private keys never leave the user's mobile under the current trust model. +- Refactoring `sessionSigner.ts` to use `@parity/product-sdk-signer`'s `SignerManager`. Half-measure; not needed without becoming a host. +- Touching `deriveAnonymousAlias`, `createRingProof`, `verifyRingProof` in `product-sdk/identity`. Unrelated to the misleading-name issue. + +## Background + +### How the four hosts derive product accounts today + +| Source | File | Algorithm | Input | +|---|---|---|---| +| Mobile (Android) | `polkadot-app-android-v2/feature/products/impl/.../ProductAccountDerivationUseCase.kt:56-58` | sr25519 soft derivation, path `"/product/{productId}/{derivationIndex}"` | bare-mnemonic seed | +| Desktop (Electron) | `polkadot-desktop/src/domains/product/account/service.ts:8-27` | sr25519 soft derivation, junctions `['product', productId, String(derivationIndex)]`, u64 numeric encoding, blake2b fallback for >32-byte chain codes | root public key | +| Dotli (web) | `dotli/packages/auth/src/account.ts` (per agent research) | sr25519 soft derivation | root public key (paired with mobile via QR, same trust model as CLI) | +| CLI | `playground-cli/src/utils/productAccountDerivation.ts:76-86` | sr25519 soft derivation, junctions `['product', productId, String(derivationIndex)]`, u32 numeric encoding, throws on >32-byte chain codes | root public key from `session.rootAccountId` | + +### Algorithmic equivalence (today) + +By code reading, the CLI and desktop produce **byte-identical** chain codes for every junction in our production input range: + +- Junction `'product'`: SCALE-encoded as 9 bytes (compact-length 1 byte + 7 utf8 bytes), zero-padded to 32. **Identical** between CLI and desktop. +- Junction `'playground.dot'`: SCALE-encoded as 15 bytes, zero-padded to 32. **Identical**. +- Junction `'0'`: CLI emits `u32.enc(0)` (4 zero bytes) padded to 32 zero bytes. Desktop emits `u64.enc(0n)` (8 zero bytes) padded to 32 zero bytes. **Identical at the chain-code level.** For any index `< 2^32`, the LE encoding agrees on the prefix bytes and the trailing bytes are zeros in both. + +Therefore: **CLI and desktop produce the same 32-byte product-account public key for `(rootPubKey, "playground.dot", 0)` today.** The same conclusion holds against mobile by virtue of sr25519 soft derivation being a standard with identical junction parsing. Cross-verification of mobile by Kotlin testing is out of scope here but flagged as an optional follow-up. + +The CLI's `u32` choice for the numeric branch is functionally equivalent to desktop's `u64` for any index < 2^32. We adopt `u64` in `product-sdk-keys` to match desktop byte-for-byte, eliminating any future ambiguity. + +### Why we cannot call the host-api from the CLI + +The `host-api` is a wire protocol over a `postMessage` channel between a product (running inside a webview/iframe) and a host (the mobile/desktop wallet). The CLI is a standalone Node/Bun process with no parent webview or `MessagePort`, so it is on neither end of the channel. The only existing CLI-to-mobile transport is `@parity/product-sdk-terminal`'s SSO/QR flow (statement-store messages, a different protocol). For derivation specifically, no host call is necessary: it is a pure function of `(rootPubKey, productId, index)`, and the CLI already has the root pubkey via `session.rootAccountId`. + +### Why we are not making the CLI a host (yet) + +`@novasamatech/host-container` is environment-agnostic at its core and could run inside a Node/Bun CLI process. The closest reference is `dotli`, which proxies the user's session to mobile via QR pairing (the same trust model the CLI already uses). However, becoming a host means implementing ~40 handler callbacks, a Provider transport, file-backed storage, and Ink consent prompts (roughly 2-3 weeks of work) **without any product to actually host inside the CLI process today.** The CLI is currently the product side of the host-product split. Until a `dot run X` workflow exists or a second tool wants host-api shaped abstractions in the CLI, becoming a host is speculative scaffolding. Captured as a roadmap note (Section 5). + +## Design + +### Section 1: add canonical derivation to `@parity/product-sdk-keys` + +**New file:** `product-sdk/packages/keys/src/productAccount.ts`. + +**Exports:** + +```ts +export function createChainCode(code: string): Uint8Array; + +export function deriveProductAccountPublicKey( + parentPublicKey: Uint8Array, + productId: string, + derivationIndex: number, +): Uint8Array; +``` + +**Algorithm.** Sr25519 soft derivation with junctions `['product', productId, String(derivationIndex)]`, applied left-to-right to `parentPublicKey`. For each junction: + +- if `code` matches `/^\d+$/`: encode as SCALE `u64` (BigInt), matching desktop byte-for-byte. +- else: encode as SCALE `str` (compact-length + UTF-8 bytes). +- if the encoded form is ≤ 32 bytes: zero-pad to 32 bytes. +- if the encoded form > 32 bytes: `blake2b(encoded, { dkLen: 32 })`. + +**Dependencies.** `@scure/sr25519` (`HDKD.publicSoft`), `@noble/hashes/blake2.js` (`blake2b`), `scale-ts` (`u64`, `str`). All already present in product-sdk; no new third-party deps. + +**Index re-export.** `product-sdk/packages/keys/src/index.ts` adds: + +```ts +export { deriveProductAccountPublicKey, createChainCode } from "./productAccount.js"; +``` + +**JSDoc header.** Block-comment establishing this as the canonical implementation, with citations to the desktop and mobile mirrors. Notes that the function works on the parent *public* key alone (no secret key required for derivation) because sr25519 soft derivation is composable on public keys. + +### Section 2: CLI imports from product-sdk-keys + +**Deletions in playground-cli:** + +- `src/utils/productAccountDerivation.ts`: entire file removed. +- Any local `*.test.ts` adjacent to the deleted file: removed (canonical tests live in product-sdk). + +**Import swaps:** + +- `src/utils/sessionSigner.ts:115-119`: change to `import { deriveProductAccountPublicKey } from "@parity/product-sdk-keys";`. Function body unchanged. +- Any other callsite of `deriveProductAccountPublicKey` in `src/`: swept and updated. + +**Package.json bump.** Bump `@parity/product-sdk-keys` to the version that ships the new export. Caret-range respected per existing CLAUDE.md guidance. + +**Rollout sequence.** + +1. PR 1 (product-sdk repo): ship `deriveProductAccountPublicKey` + `createChainCode` in `@parity/product-sdk-keys`. Publish a new minor version. +2. PR 2 (playground-cli repo): bump dep to that version, delete `productAccountDerivation.ts`, swap imports. +3. Both PRs verify `pnpm test && pnpm build && pnpm format:check && pnpm lint:license` per CLAUDE.md. + +**Risk: derivation drift on non-zero indices.** The CLI uses `u32` today; product-sdk-keys ships `u64`. Outputs agree byte-for-byte for any index < 2^32, and we always use index 0. Confirmed during implementation by sweeping for non-zero `derivationIndex` callsites before merging. + +### Section 3: verification fixtures (four layers) + +**Layer A: frozen vectors in `product-sdk-keys`.** New file `product-sdk/packages/keys/src/productAccount.test.ts` with four `(rootPublicKey, productId, derivationIndex) → expected publicKey hex` cases: + +1. `root = 0x00…` (32 zero bytes), `productId = "playground.dot"`, `index = 0`: production case. +2. `root = 0x01…` (32 0x01 bytes), `productId = "playground.dot"`, `index = 1`: exercises non-zero u64 numeric branch. +3. `root = 0x…`, `productId = "a-very-long-product.dot"`, `index = 0`: near-32-byte-boundary, no fallback. +4. `root = 0x…`, `productId = "this-name-is-deliberately-long-enough-to-trip-the-fallback.dot"`, `index = 0`: exercises blake2b fallback. + +The expected hex values are computed once by running desktop's `productAccountService.deriveProductPublicKey` against the same inputs, then frozen in the test file. If desktop's algorithm changes, these tests fail in product-sdk before any consumer drifts. + +**Layer B: desktop parity script.** `product-sdk/packages/keys/scripts/regenerate-fixtures.ts`: a maintenance tool (not run in CI) that replicates desktop's algorithm against the same input table and prints the expected vectors. Run manually if desktop's implementation is ever updated. Makes the "match desktop" contract auditable. + +**Layer C: integration test in playground-cli.** Lightweight: one vitest case that calls `deriveProductAccountPublicKey(parentPubKey, "playground.dot", 0)` against a fixed input and asserts the SS58-encoded result. Validates the import wiring and the `@parity/product-sdk-address::ss58Encode` integration; the algorithm itself is already covered by Layer A. + +**Layer D: playground-app console log.** Separate PR on `playground-app` repo. After `getProductAccount("playground.dot")` returns in `src/utils/contracts.ts:98`, add `console.info("product account:", account.address, account.h160Address)` (exact format TBD with playground-app maintainers, structured-log preferred). Gives the user an in-DevTools cross-check against the `dot init` display. + +### Section 4: `dot init` identity display + +**Where.** The success branch of `src/commands/init/InitScreen.tsx`. Two new lines after the existing "you're signed in" confirmation: + +``` +Logged in. + Username: alice.dot + Product account: 5GrwvaEF…rXKDi (0x1a2b…ef34) +``` + +**Address line.** Computed from `ss58Encode(deriveProductAccountPublicKey(session.rootAccountId, PLAYGROUND_PRODUCT_ID, 0))` (SS58) and `deriveH160(...)` (H160). Both formats shown: SS58 is substrate-native; H160 is what the registry's owner index uses (so users can match it against `playground-app`'s "My apps" filter which keys off `account.h160Address`). + +**Username line.** Queried from the on-chain identity registry (People parachain, RFC-0014 DotNS). Investigation step during implementation: confirm `@novasamatech/host-papp`'s `IdentityAdapter.readIdentities` is a chain query (not an IPC wrapper). If chain-query: import + use it directly. If IPC-wrapped: drop a layer and query People parachain via PAPI using the existing `peopleEndpoints` from `src/config.ts`. + +**Privacy.** DotNS identities are public on-chain registry entries. We are only displaying the user's own username back to them, sourced from public data. No host-api `getUserId` consent prompt is needed (and would not be available from a CLI anyway). The username is not stored locally; it is re-queried each `dot init` run. + +**Failure modes.** + +- People endpoint unreachable: `Username: (lookup failed)`. +- Account has no on-chain identity: `Username: (no username set on chain)`. +- Account has only `liteUsername` (no `fullUsername`): display `liteUsername` (per the `Identity` type from `@novasamatech/host-papp`). +- Identity lookup hangs: time-bounded (a few seconds); on timeout, show `(lookup failed)` and surface the address line regardless. + +The address line never fails; it is computed from data the CLI already holds. + +### Section 5: CLI-as-host roadmap note + +A markdown note at `docs/superpowers/specs/2026-05-18-cli-as-host-deferred.md`, committed alongside this spec. Captures: + +1. **What we found.** `host-container` is environment-agnostic; a Node/Bun CLI Provider over Worker MessagePorts, Unix sockets, stdio, or an in-memory event bus is feasible. dotli is the reference architecture (web host pairing to mobile via QR, same trust model as our CLI). +2. **What blocks it today.** ~40 handler callbacks, a Provider transport, file-backed storage, and Ink consent prompts (~2-3 weeks) with no product to host. The CLI is currently the product side; becoming a host without an in-process product is speculative scaffolding. +3. **What triggers revisiting.** Any of: a `dot run ` command running products inside the CLI; a second tool wanting `SignerManager`/`HostProvider` abstractions in the CLI; product-sdk publishing a Node-runnable `HostProvider` impl we can plug into. +4. **References.** File:line pointers to `host-container/src/createContainer.ts`, `dotli/packages/ui/src/container.ts`, `dotli/packages/auth/src/auth.ts`. +5. **Non-goals.** Generating a CLI-only root mnemonic; networked host-api; refactoring `sessionSigner.ts` to use `SignerManager` without a host. + +### Section 6: rename blake2b util in product-sdk + +**Target.** `product-sdk/packages/sdk/src/identity/product-account.ts:34-63`, `deriveProductAccount(parentAddress, productName, ss58Prefix)`. Algorithm: `blake2b-256(parentPublicKey || productName_bytes)`. This is **not** the canonical product-account derivation; it is a context-bound alias derivation whose name misleads. + +**Approach.** Hard rename, no deprecation alias, consistent across the product-sdk repo. + +**Investigation step (during implementation).** Sweep callsites: + +```bash +grep -rn "deriveProductAccount\b" product-sdk/ +grep -rn "@parity/product-sdk/identity" product-sdk/ playground-cli/ playground-app/ dotli/ polkadot-desktop/ polkadot-app-android-v2/ +``` + +Use the discovered callsites to pick a name that matches the function's actual usage. Best guesses pending investigation: `deriveContextAlias`, `deriveDomainSeparatedAccount`, `deriveContextAccount`, `deriveBoundedAlias`. Final choice deferred to the implementation plan. + +**Changes.** + +- Rename the function in `product-sdk/packages/sdk/src/identity/product-account.ts`. +- Update every callsite within `product-sdk/` (the consistency requirement). +- Update the index re-export (`@parity/product-sdk/identity`) to the new name only. Old name is removed in the same PR. +- Type renames (`ProductAccountInfo` to the corresponding new name, if appropriate). +- Changeset entry flagging the breaking change. + +**External consumers.** Any external repo importing the old name will get a build error after upgrading. This is the explicit choice; we are not maintaining a deprecation alias. The changeset note plus a release notes line make the migration discoverable. + +## Implementation order + +Three repos, four PRs in approximately this sequence: + +1. **product-sdk PR (Section 1 + Section 3 Layer A + Layer B):** add `deriveProductAccountPublicKey` and `createChainCode` to `@parity/product-sdk-keys`. Add the frozen vector test file. Add the desktop parity script. Publish a new minor version. +2. **product-sdk PR (Section 6):** rename `deriveProductAccount` (blake2b util) consistently across the repo. Publish. +3. **playground-cli PR (Sections 2, 3 Layer C, 4, 5):** bump SDK deps, swap imports, delete `productAccountDerivation.ts`. Add the integration vitest case. Add the `dot init` username + product account display lines. Commit the CLI-as-host roadmap note. Verify per CLAUDE.md. +4. **playground-app PR (Section 3 Layer D):** add the `console.info` line after `getProductAccount`. Trivial. + +PR 1 and 2 can land in either order in product-sdk; PR 3 depends on PR 1. PR 4 is independent and can land anytime. + +## Verification + +The success criteria for this work: + +- `pnpm test` in product-sdk passes the four-vector fixture for `deriveProductAccountPublicKey`. +- `pnpm test && pnpm build && pnpm format:check && pnpm lint:license` in playground-cli pass. +- `grep -rn "deriveProductAccountPublicKey" playground-cli/src/` shows imports only from `@parity/product-sdk-keys`, no local file. +- A `dot init` run shows the username and product account address lines (with appropriate fallback strings when applicable). +- The same user, logging into `playground-app`, sees the same SS58 and H160 in the browser console (Layer D log). +- `grep -rn "deriveProductAccount\b" product-sdk/ playground-cli/` shows the old blake2b util name nowhere; only the renamed version is referenced. + +## Open questions deferred to implementation + +- Final name of the renamed blake2b util (Section 6): picked after callsite investigation. +- Whether `IdentityAdapter.readIdentities` (Section 4) is on-chain or IPC-wrapped: decides whether we import host-papp's adapter or query People parachain directly. +- Exact UX phrasing for the two `dot init` lines: refined during implementation against the existing Ink screen styling. +- Mobile (Android) cross-verification of the derivation algorithm: currently relies on code-reading; an optional Kotlin-side fixture is out of scope here. From 0e8bf0f2a0c985e97693b1fc68906b3d2ab0c85a Mon Sep 17 00:00:00 2001 From: UtkarshBhardwaj007 Date: Tue, 19 May 2026 15:02:13 +0100 Subject: [PATCH 2/4] feat(init): show username and product account on success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders two new lines in the `dot init` logged-in confirmation: the user's root username (queried on-chain from People parachain's Resources.Consumers storage with a 5s timeout, falling back to a "(no username set on chain)" / "(lookup failed)" placeholder) and their product account address (SS58 + truncated H160 derived locally via sr25519 soft derivation). The product account line lets users eyeball that their CLI account matches the one playground-app uses for "My apps" — same root account + same product id + same index = same SS58/H160 across hosts. The on-chain lookup mirrors `@novasamatech/host-papp`'s `createIdentityRpcAdapter` shape directly because the factory is not re-exported at the package root and the package blocks deep imports; the mapping is documented in `src/utils/username.ts` with a pointer back to the upstream source. --- src/commands/init/IdentityLines.tsx | 64 +++++++++++++ src/commands/init/InitScreen.tsx | 3 + src/commands/init/identityLine.test.ts | 47 ++++++++++ src/commands/init/identityLine.ts | 49 ++++++++++ src/utils/username.test.ts | 55 +++++++++++ src/utils/username.ts | 123 +++++++++++++++++++++++++ 6 files changed, 341 insertions(+) create mode 100644 src/commands/init/IdentityLines.tsx create mode 100644 src/commands/init/identityLine.test.ts create mode 100644 src/commands/init/identityLine.ts create mode 100644 src/utils/username.test.ts create mode 100644 src/utils/username.ts diff --git a/src/commands/init/IdentityLines.tsx b/src/commands/init/IdentityLines.tsx new file mode 100644 index 0000000..4a43b86 --- /dev/null +++ b/src/commands/init/IdentityLines.tsx @@ -0,0 +1,64 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { useEffect, useState } from "react"; +import { Row, Section } from "../../utils/ui/theme/index.js"; +import { formatUsernameLine, lookupUsername, type UsernameLookup } from "../../utils/username.js"; +import { productAccountDisplay } from "./identityLine.js"; + +/** + * Two-line identity block shown after a successful login: + * + * username alice.dot + * product account 5Grwva...utQY (0x1a2b...ef34) + * + * The username lookup is async (queries People parachain) and has a 5s + * timeout inside `lookupUsername`; the product account is synchronous (pure + * sr25519 soft derivation). A `(looking up...)` placeholder renders while + * the lookup is in flight; failures and missing identities fall through to + * the relevant fallback strings from `formatUsernameLine`. + */ +export function IdentityLines({ address }: { address: string }) { + const [username, setUsername] = useState({ kind: "loading" }); + + useEffect(() => { + let cancelled = false; + lookupUsername(address).then((result) => { + if (!cancelled) setUsername(result); + }); + return () => { + cancelled = true; + }; + }, [address]); + + const usernameTone = username.kind === "found" ? "default" : "muted"; + + return ( +
+ + +
+ ); +} diff --git a/src/commands/init/InitScreen.tsx b/src/commands/init/InitScreen.tsx index 5dcf527..1a037f2 100644 --- a/src/commands/init/InitScreen.tsx +++ b/src/commands/init/InitScreen.tsx @@ -17,6 +17,7 @@ import { useState, useEffect } from "react"; import { Box } from "ink"; import { Header, Row, Section } from "../../utils/ui/theme/index.js"; import { DependencyList } from "./DependencyList.js"; +import { IdentityLines } from "./IdentityLines.js"; import { QrLogin } from "./QrLogin.js"; import { AccountSetup } from "./AccountSetup.js"; import { computeAllDone } from "./completion.js"; @@ -82,6 +83,8 @@ export function InitScreen({ )} + {loggedInAddress && } + {loggedInAddress && depsComplete && ( diff --git a/src/commands/init/identityLine.test.ts b/src/commands/init/identityLine.test.ts new file mode 100644 index 0000000..384122f --- /dev/null +++ b/src/commands/init/identityLine.test.ts @@ -0,0 +1,47 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, it } from "vitest"; +import { ss58Encode } from "@parity/product-sdk-address"; +import { productAccountAddresses, productAccountDisplay } from "./identityLine.js"; + +// A deterministic, all-zero root public key gives a stable derived product +// account. The exact bytes don't matter; we only assert that the helper +// produces a non-empty SS58 + valid 0x-prefixed H160 and that the display +// helper renders both in the expected "ss58 (h160)" shape. +const ZERO_ROOT_SS58 = ss58Encode(new Uint8Array(32)); + +describe("productAccountAddresses", () => { + it("derives a non-empty SS58 + a 42-char H160 from a root SS58", () => { + const { ss58, h160 } = productAccountAddresses(ZERO_ROOT_SS58); + expect(typeof ss58).toBe("string"); + expect(ss58.length).toBeGreaterThan(40); + expect(h160).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + + it("is deterministic for the same root SS58", () => { + const a = productAccountAddresses(ZERO_ROOT_SS58); + const b = productAccountAddresses(ZERO_ROOT_SS58); + expect(a.ss58).toBe(b.ss58); + expect(a.h160).toBe(b.h160); + }); +}); + +describe("productAccountDisplay", () => { + it("renders 'ss58 (h160)' with both addresses truncated", () => { + const display = productAccountDisplay(ZERO_ROOT_SS58); + expect(display).toMatch(/^.+\.\.\..+ \(0x.+\.\.\..+\)$/); + }); +}); diff --git a/src/commands/init/identityLine.ts b/src/commands/init/identityLine.ts new file mode 100644 index 0000000..dc83fa8 --- /dev/null +++ b/src/commands/init/identityLine.ts @@ -0,0 +1,49 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Pure helpers for the identity block in InitScreen — lifted out of the + * `.tsx` per the repo convention "pure logic that lives inside a `.tsx` + * component should be lifted into a sibling `.ts` file" (see + * `completion.ts` next to `InitScreen.tsx` for the same pattern). + * + * The product-account derivation here uses the local + * `src/utils/productAccountDerivation.ts` mirror; Phase 3 of the + * product-account unification migration will swap that for the canonical + * `@parity/product-sdk-keys` export. Output is byte-identical either way. + */ + +import { deriveH160, ss58Decode, ss58Encode, truncateAddress } from "@parity/product-sdk-address"; +import { PLAYGROUND_PRODUCT_ID } from "../../config.js"; +import { deriveProductAccountPublicKey } from "../../utils/productAccountDerivation.js"; + +export interface ProductAccountAddresses { + ss58: string; + h160: `0x${string}`; +} + +export function productAccountAddresses(rootAccountSs58: string): ProductAccountAddresses { + const { publicKey } = ss58Decode(rootAccountSs58); + const productPubkey = deriveProductAccountPublicKey(publicKey, PLAYGROUND_PRODUCT_ID, 0); + return { + ss58: ss58Encode(productPubkey), + h160: deriveH160(productPubkey), + }; +} + +export function productAccountDisplay(rootAccountSs58: string): string { + const { ss58, h160 } = productAccountAddresses(rootAccountSs58); + return `${truncateAddress(ss58, 6, 4)} (${truncateAddress(h160, 6, 4)})`; +} diff --git a/src/utils/username.test.ts b/src/utils/username.test.ts new file mode 100644 index 0000000..4fe5e98 --- /dev/null +++ b/src/utils/username.test.ts @@ -0,0 +1,55 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, it } from "vitest"; +import { formatUsernameLine, type UsernameLookup } from "./username.js"; + +describe("formatUsernameLine", () => { + it("returns the full username when present", () => { + const lookup: UsernameLookup = { + kind: "found", + fullUsername: "alice.dot", + liteUsername: "alice", + }; + expect(formatUsernameLine(lookup)).toBe("alice.dot"); + }); + + it("falls back to the lite username when full is null", () => { + const lookup: UsernameLookup = { + kind: "found", + fullUsername: null, + liteUsername: "alice", + }; + expect(formatUsernameLine(lookup)).toBe("alice"); + }); + + it("returns '(no username set on chain)' when the account has no identity", () => { + const lookup: UsernameLookup = { kind: "none" }; + expect(formatUsernameLine(lookup)).toBe("(no username set on chain)"); + }); + + it("returns '(lookup failed)' on any lookup error", () => { + const lookup: UsernameLookup = { + kind: "error", + reason: "endpoint unreachable", + }; + expect(formatUsernameLine(lookup)).toBe("(lookup failed)"); + }); + + it("returns '(looking up...)' while the lookup is pending", () => { + const lookup: UsernameLookup = { kind: "loading" }; + expect(formatUsernameLine(lookup)).toBe("(looking up...)"); + }); +}); diff --git a/src/utils/username.ts b/src/utils/username.ts new file mode 100644 index 0000000..c5bac1e --- /dev/null +++ b/src/utils/username.ts @@ -0,0 +1,123 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * On-chain username lookup for the user's root account. + * + * The data lives on the People parachain in `Resources.Consumers` (the + * statement-store storage map). `@novasamatech/host-papp` exposes the same + * query inside `createIdentityRpcAdapter`, but the factory is not re-exported + * at the package root and host-papp's `exports` field blocks deep imports + * (see `node_modules/@novasamatech/host-papp/package.json`). Adding host-papp + * as a direct dep just for this one-shot call would also pull in the full + * SSO/sessions/identity-cache pipeline we don't need. So we mirror the small + * piece we do need: the storage query + the byte mapping. Same precedent as + * `src/utils/allowances/host.ts` (which mirrors host-papp's RFC-0010 call). + * + * The upstream source we're mirroring lives in + * `@novasamatech/host-papp@0.7.9-4/dist/identity/rpcAdapter.js`; keep this in + * sync if that ever changes pallet/storage names. + */ + +import { AccountId, createClient } from "polkadot-api"; +import { getWsProvider } from "polkadot-api/ws"; +import { getChainConfig } from "../config.js"; + +const LOOKUP_TIMEOUT_MS = 5_000; + +export type UsernameLookup = + | { kind: "loading" } + | { kind: "found"; fullUsername: string | null; liteUsername: string } + | { kind: "none" } + | { kind: "error"; reason: string }; + +export function formatUsernameLine(lookup: UsernameLookup): string { + switch (lookup.kind) { + case "loading": + return "(looking up...)"; + case "found": + return lookup.fullUsername ?? lookup.liteUsername; + case "none": + return "(no username set on chain)"; + case "error": + return "(lookup failed)"; + } +} + +/** + * Raw shape of the `Resources.Consumers` storage value, mirrored from + * `@novasamatech/host-papp/dist/identity/rpcAdapter.js`. Typed as `unknown` + * fields where we only need a few keys; `getUnsafeApi()` returns `any`-ish + * values so we narrow defensively at the read site. + */ +type ConsumerRecord = { + full_username: Uint8Array | null; + lite_username: Uint8Array; + credibility: unknown; +}; + +/** + * Look up the on-chain identity for `rootAccountSs58` with a hard timeout. + * + * Returns within ~5 seconds regardless of network conditions. Slow paths + * return `{ kind: "error", reason: "lookup timed out" }`. The lookup uses + * the People parachain endpoints from `getChainConfig()`. + */ +export async function lookupUsername(rootAccountSs58: string): Promise { + const { peopleEndpoints } = getChainConfig(); + const client = createClient(getWsProvider(peopleEndpoints)); + try { + const accCodec = AccountId(); + const accountKey = accCodec.dec(rootAccountSs58); + const unsafeApi = client.getUnsafeApi(); + const query = unsafeApi.query.Resources?.Consumers; + if (!query) { + return { + kind: "error", + reason: "Resources.Consumers storage not found on chain", + }; + } + + const result = await Promise.race([ + query.getValues([[accountKey]]) as Promise>, + new Promise<"timeout">((resolve) => + setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS), + ), + ]); + + if (result === "timeout") { + return { kind: "error", reason: "lookup timed out" }; + } + + const raw = result[0]; + if (!raw) return { kind: "none" }; + + const textDecoder = new TextDecoder(); + return { + kind: "found", + fullUsername: raw.full_username ? textDecoder.decode(raw.full_username) : null, + liteUsername: textDecoder.decode(raw.lite_username), + }; + } catch (err) { + return { + kind: "error", + reason: err instanceof Error ? err.message : String(err), + }; + } finally { + // Fire-and-forget. The CLI's process-guard catches benign + // post-destroy artifacts from polkadot-api's chainHead unfollow race. + client.destroy(); + } +} From f618038a19a7d621a1b9ba02e2798ebefaea02df Mon Sep 17 00:00:00 2001 From: UtkarshBhardwaj007 Date: Tue, 19 May 2026 15:04:10 +0100 Subject: [PATCH 3/4] chore: changeset for dot init identity display --- .changeset/dot-init-identity-display.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/dot-init-identity-display.md diff --git a/.changeset/dot-init-identity-display.md b/.changeset/dot-init-identity-display.md new file mode 100644 index 0000000..9ce4454 --- /dev/null +++ b/.changeset/dot-init-identity-display.md @@ -0,0 +1,8 @@ +--- +"playground-cli": minor +--- + +`dot init` now shows your username and your product account address alongside the existing "logged in" confirmation. + +- **Username** comes from your on-chain identity on People parachain (`Resources.Consumers` storage). If you haven't registered a username yet you'll see `(no username set on chain)`; if the lookup fails or times out (5s) it falls back to `(lookup failed)`. +- **Product account** is the SS58 + truncated H160 derived locally from your root account via the same sr25519 soft-derivation path that the mobile wallet uses privately. The address you see here is the SAME one `playground-app` resolves for "My apps" and the SAME one your CLI signs as on-chain — so a quick eyeball is enough to confirm both clients agree on your identity. From 86f027fa4f50c05a862961b7e1c075ba083965ab Mon Sep 17 00:00:00 2001 From: UtkarshBhardwaj007 Date: Tue, 19 May 2026 15:55:33 +0100 Subject: [PATCH 4/4] chore: drop design spec, gitignore docs/superpowers/ playground-cli doesn't commit plans, specs, or other design / brainstorming docs (see the existing precedent of `docs-internal/` being gitignored). Removes the design spec for the product-account unification work that was committed in error, and gitignores the whole `docs/superpowers/` subtree so plans + specs stay local-only going forward. --- .gitignore | 1 + ...5-18-product-account-unification-design.md | 221 ------------------ 2 files changed, 1 insertion(+), 221 deletions(-) delete mode 100644 docs/superpowers/specs/2026-05-18-product-account-unification-design.md diff --git a/.gitignore b/.gitignore index c67490f..dd5b99e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ .cdm/ docs-internal/ +docs/superpowers/ .worktrees/ reference-repos/ .tokensave diff --git a/docs/superpowers/specs/2026-05-18-product-account-unification-design.md b/docs/superpowers/specs/2026-05-18-product-account-unification-design.md deleted file mode 100644 index 93a2736..0000000 --- a/docs/superpowers/specs/2026-05-18-product-account-unification-design.md +++ /dev/null @@ -1,221 +0,0 @@ -# Product-account unification across playground-cli, playground-app, and product-sdk - -**Status:** Draft, pending user approval -**Author:** utkarsh.bhardwaj@parity.io -**Created:** 2026-05-18 - -## Problem - -The `playground-cli`, the `playground-app` web UI, and the underlying Polkadot hosts (mobile, desktop, dotli) each derive a "product account": the per-product public key the user signs with when interacting with `playground.dot`. Today this derivation is implemented in three places (CLI, desktop, mobile), and a fourth, *misleadingly named* utility lives in `product-sdk` using a completely different algorithm. There is no single source of truth, no fixture proving the implementations agree, and no on-screen confirmation in `dot init` that lets a user verify their CLI is acting as the same account that `playground-app` sees. - -This spec consolidates derivation in `@parity/product-sdk-keys`, locks it against a canonical fixture, surfaces the result in `dot init`, deprecates the misleading SDK util, and captures the deferred "CLI as host" option as a roadmap note. - -## Goals - -1. **One canonical implementation** of `deriveProductAccountPublicKey(parentPublicKey, productId, derivationIndex)`, living in `@parity/product-sdk-keys`. CLI imports it; no other consumer reimplements it. -2. **Algorithmic equivalence with the desktop host** locked by frozen vitest fixtures in product-sdk. Drift fails CI before it reaches consumers. -3. **`dot init` shows the user's root username (on-chain) and product account address (SS58 + H160)**, so the user can eyeball-confirm their CLI account matches what `playground-app` displays under "My apps." -4. **Rename the misleading `deriveProductAccount` in `@parity/product-sdk/identity`** (a blake2b util that is NOT the canonical product-account derivation) to a name reflecting its actual purpose. Hard rename, consistent across the product-sdk repo, no deprecation alias. -5. **Capture the "CLI as a host" option as a deferred roadmap note** with a written reason and a trigger for revisiting. - -## Non-goals - -- Making the CLI a host in the host-api sense (`host-container`-based). Deferred; rationale in the roadmap note (Section 5). -- Generating a CLI-only root mnemonic. Would defeat the shared-identity goal and is explicitly out of scope. -- Adding sr25519 *keypair* derivation (private + public) to `product-sdk-keys`. Only the public-key path is in scope. Private keys never leave the user's mobile under the current trust model. -- Refactoring `sessionSigner.ts` to use `@parity/product-sdk-signer`'s `SignerManager`. Half-measure; not needed without becoming a host. -- Touching `deriveAnonymousAlias`, `createRingProof`, `verifyRingProof` in `product-sdk/identity`. Unrelated to the misleading-name issue. - -## Background - -### How the four hosts derive product accounts today - -| Source | File | Algorithm | Input | -|---|---|---|---| -| Mobile (Android) | `polkadot-app-android-v2/feature/products/impl/.../ProductAccountDerivationUseCase.kt:56-58` | sr25519 soft derivation, path `"/product/{productId}/{derivationIndex}"` | bare-mnemonic seed | -| Desktop (Electron) | `polkadot-desktop/src/domains/product/account/service.ts:8-27` | sr25519 soft derivation, junctions `['product', productId, String(derivationIndex)]`, u64 numeric encoding, blake2b fallback for >32-byte chain codes | root public key | -| Dotli (web) | `dotli/packages/auth/src/account.ts` (per agent research) | sr25519 soft derivation | root public key (paired with mobile via QR, same trust model as CLI) | -| CLI | `playground-cli/src/utils/productAccountDerivation.ts:76-86` | sr25519 soft derivation, junctions `['product', productId, String(derivationIndex)]`, u32 numeric encoding, throws on >32-byte chain codes | root public key from `session.rootAccountId` | - -### Algorithmic equivalence (today) - -By code reading, the CLI and desktop produce **byte-identical** chain codes for every junction in our production input range: - -- Junction `'product'`: SCALE-encoded as 9 bytes (compact-length 1 byte + 7 utf8 bytes), zero-padded to 32. **Identical** between CLI and desktop. -- Junction `'playground.dot'`: SCALE-encoded as 15 bytes, zero-padded to 32. **Identical**. -- Junction `'0'`: CLI emits `u32.enc(0)` (4 zero bytes) padded to 32 zero bytes. Desktop emits `u64.enc(0n)` (8 zero bytes) padded to 32 zero bytes. **Identical at the chain-code level.** For any index `< 2^32`, the LE encoding agrees on the prefix bytes and the trailing bytes are zeros in both. - -Therefore: **CLI and desktop produce the same 32-byte product-account public key for `(rootPubKey, "playground.dot", 0)` today.** The same conclusion holds against mobile by virtue of sr25519 soft derivation being a standard with identical junction parsing. Cross-verification of mobile by Kotlin testing is out of scope here but flagged as an optional follow-up. - -The CLI's `u32` choice for the numeric branch is functionally equivalent to desktop's `u64` for any index < 2^32. We adopt `u64` in `product-sdk-keys` to match desktop byte-for-byte, eliminating any future ambiguity. - -### Why we cannot call the host-api from the CLI - -The `host-api` is a wire protocol over a `postMessage` channel between a product (running inside a webview/iframe) and a host (the mobile/desktop wallet). The CLI is a standalone Node/Bun process with no parent webview or `MessagePort`, so it is on neither end of the channel. The only existing CLI-to-mobile transport is `@parity/product-sdk-terminal`'s SSO/QR flow (statement-store messages, a different protocol). For derivation specifically, no host call is necessary: it is a pure function of `(rootPubKey, productId, index)`, and the CLI already has the root pubkey via `session.rootAccountId`. - -### Why we are not making the CLI a host (yet) - -`@novasamatech/host-container` is environment-agnostic at its core and could run inside a Node/Bun CLI process. The closest reference is `dotli`, which proxies the user's session to mobile via QR pairing (the same trust model the CLI already uses). However, becoming a host means implementing ~40 handler callbacks, a Provider transport, file-backed storage, and Ink consent prompts (roughly 2-3 weeks of work) **without any product to actually host inside the CLI process today.** The CLI is currently the product side of the host-product split. Until a `dot run X` workflow exists or a second tool wants host-api shaped abstractions in the CLI, becoming a host is speculative scaffolding. Captured as a roadmap note (Section 5). - -## Design - -### Section 1: add canonical derivation to `@parity/product-sdk-keys` - -**New file:** `product-sdk/packages/keys/src/productAccount.ts`. - -**Exports:** - -```ts -export function createChainCode(code: string): Uint8Array; - -export function deriveProductAccountPublicKey( - parentPublicKey: Uint8Array, - productId: string, - derivationIndex: number, -): Uint8Array; -``` - -**Algorithm.** Sr25519 soft derivation with junctions `['product', productId, String(derivationIndex)]`, applied left-to-right to `parentPublicKey`. For each junction: - -- if `code` matches `/^\d+$/`: encode as SCALE `u64` (BigInt), matching desktop byte-for-byte. -- else: encode as SCALE `str` (compact-length + UTF-8 bytes). -- if the encoded form is ≤ 32 bytes: zero-pad to 32 bytes. -- if the encoded form > 32 bytes: `blake2b(encoded, { dkLen: 32 })`. - -**Dependencies.** `@scure/sr25519` (`HDKD.publicSoft`), `@noble/hashes/blake2.js` (`blake2b`), `scale-ts` (`u64`, `str`). All already present in product-sdk; no new third-party deps. - -**Index re-export.** `product-sdk/packages/keys/src/index.ts` adds: - -```ts -export { deriveProductAccountPublicKey, createChainCode } from "./productAccount.js"; -``` - -**JSDoc header.** Block-comment establishing this as the canonical implementation, with citations to the desktop and mobile mirrors. Notes that the function works on the parent *public* key alone (no secret key required for derivation) because sr25519 soft derivation is composable on public keys. - -### Section 2: CLI imports from product-sdk-keys - -**Deletions in playground-cli:** - -- `src/utils/productAccountDerivation.ts`: entire file removed. -- Any local `*.test.ts` adjacent to the deleted file: removed (canonical tests live in product-sdk). - -**Import swaps:** - -- `src/utils/sessionSigner.ts:115-119`: change to `import { deriveProductAccountPublicKey } from "@parity/product-sdk-keys";`. Function body unchanged. -- Any other callsite of `deriveProductAccountPublicKey` in `src/`: swept and updated. - -**Package.json bump.** Bump `@parity/product-sdk-keys` to the version that ships the new export. Caret-range respected per existing CLAUDE.md guidance. - -**Rollout sequence.** - -1. PR 1 (product-sdk repo): ship `deriveProductAccountPublicKey` + `createChainCode` in `@parity/product-sdk-keys`. Publish a new minor version. -2. PR 2 (playground-cli repo): bump dep to that version, delete `productAccountDerivation.ts`, swap imports. -3. Both PRs verify `pnpm test && pnpm build && pnpm format:check && pnpm lint:license` per CLAUDE.md. - -**Risk: derivation drift on non-zero indices.** The CLI uses `u32` today; product-sdk-keys ships `u64`. Outputs agree byte-for-byte for any index < 2^32, and we always use index 0. Confirmed during implementation by sweeping for non-zero `derivationIndex` callsites before merging. - -### Section 3: verification fixtures (four layers) - -**Layer A: frozen vectors in `product-sdk-keys`.** New file `product-sdk/packages/keys/src/productAccount.test.ts` with four `(rootPublicKey, productId, derivationIndex) → expected publicKey hex` cases: - -1. `root = 0x00…` (32 zero bytes), `productId = "playground.dot"`, `index = 0`: production case. -2. `root = 0x01…` (32 0x01 bytes), `productId = "playground.dot"`, `index = 1`: exercises non-zero u64 numeric branch. -3. `root = 0x…`, `productId = "a-very-long-product.dot"`, `index = 0`: near-32-byte-boundary, no fallback. -4. `root = 0x…`, `productId = "this-name-is-deliberately-long-enough-to-trip-the-fallback.dot"`, `index = 0`: exercises blake2b fallback. - -The expected hex values are computed once by running desktop's `productAccountService.deriveProductPublicKey` against the same inputs, then frozen in the test file. If desktop's algorithm changes, these tests fail in product-sdk before any consumer drifts. - -**Layer B: desktop parity script.** `product-sdk/packages/keys/scripts/regenerate-fixtures.ts`: a maintenance tool (not run in CI) that replicates desktop's algorithm against the same input table and prints the expected vectors. Run manually if desktop's implementation is ever updated. Makes the "match desktop" contract auditable. - -**Layer C: integration test in playground-cli.** Lightweight: one vitest case that calls `deriveProductAccountPublicKey(parentPubKey, "playground.dot", 0)` against a fixed input and asserts the SS58-encoded result. Validates the import wiring and the `@parity/product-sdk-address::ss58Encode` integration; the algorithm itself is already covered by Layer A. - -**Layer D: playground-app console log.** Separate PR on `playground-app` repo. After `getProductAccount("playground.dot")` returns in `src/utils/contracts.ts:98`, add `console.info("product account:", account.address, account.h160Address)` (exact format TBD with playground-app maintainers, structured-log preferred). Gives the user an in-DevTools cross-check against the `dot init` display. - -### Section 4: `dot init` identity display - -**Where.** The success branch of `src/commands/init/InitScreen.tsx`. Two new lines after the existing "you're signed in" confirmation: - -``` -Logged in. - Username: alice.dot - Product account: 5GrwvaEF…rXKDi (0x1a2b…ef34) -``` - -**Address line.** Computed from `ss58Encode(deriveProductAccountPublicKey(session.rootAccountId, PLAYGROUND_PRODUCT_ID, 0))` (SS58) and `deriveH160(...)` (H160). Both formats shown: SS58 is substrate-native; H160 is what the registry's owner index uses (so users can match it against `playground-app`'s "My apps" filter which keys off `account.h160Address`). - -**Username line.** Queried from the on-chain identity registry (People parachain, RFC-0014 DotNS). Investigation step during implementation: confirm `@novasamatech/host-papp`'s `IdentityAdapter.readIdentities` is a chain query (not an IPC wrapper). If chain-query: import + use it directly. If IPC-wrapped: drop a layer and query People parachain via PAPI using the existing `peopleEndpoints` from `src/config.ts`. - -**Privacy.** DotNS identities are public on-chain registry entries. We are only displaying the user's own username back to them, sourced from public data. No host-api `getUserId` consent prompt is needed (and would not be available from a CLI anyway). The username is not stored locally; it is re-queried each `dot init` run. - -**Failure modes.** - -- People endpoint unreachable: `Username: (lookup failed)`. -- Account has no on-chain identity: `Username: (no username set on chain)`. -- Account has only `liteUsername` (no `fullUsername`): display `liteUsername` (per the `Identity` type from `@novasamatech/host-papp`). -- Identity lookup hangs: time-bounded (a few seconds); on timeout, show `(lookup failed)` and surface the address line regardless. - -The address line never fails; it is computed from data the CLI already holds. - -### Section 5: CLI-as-host roadmap note - -A markdown note at `docs/superpowers/specs/2026-05-18-cli-as-host-deferred.md`, committed alongside this spec. Captures: - -1. **What we found.** `host-container` is environment-agnostic; a Node/Bun CLI Provider over Worker MessagePorts, Unix sockets, stdio, or an in-memory event bus is feasible. dotli is the reference architecture (web host pairing to mobile via QR, same trust model as our CLI). -2. **What blocks it today.** ~40 handler callbacks, a Provider transport, file-backed storage, and Ink consent prompts (~2-3 weeks) with no product to host. The CLI is currently the product side; becoming a host without an in-process product is speculative scaffolding. -3. **What triggers revisiting.** Any of: a `dot run ` command running products inside the CLI; a second tool wanting `SignerManager`/`HostProvider` abstractions in the CLI; product-sdk publishing a Node-runnable `HostProvider` impl we can plug into. -4. **References.** File:line pointers to `host-container/src/createContainer.ts`, `dotli/packages/ui/src/container.ts`, `dotli/packages/auth/src/auth.ts`. -5. **Non-goals.** Generating a CLI-only root mnemonic; networked host-api; refactoring `sessionSigner.ts` to use `SignerManager` without a host. - -### Section 6: rename blake2b util in product-sdk - -**Target.** `product-sdk/packages/sdk/src/identity/product-account.ts:34-63`, `deriveProductAccount(parentAddress, productName, ss58Prefix)`. Algorithm: `blake2b-256(parentPublicKey || productName_bytes)`. This is **not** the canonical product-account derivation; it is a context-bound alias derivation whose name misleads. - -**Approach.** Hard rename, no deprecation alias, consistent across the product-sdk repo. - -**Investigation step (during implementation).** Sweep callsites: - -```bash -grep -rn "deriveProductAccount\b" product-sdk/ -grep -rn "@parity/product-sdk/identity" product-sdk/ playground-cli/ playground-app/ dotli/ polkadot-desktop/ polkadot-app-android-v2/ -``` - -Use the discovered callsites to pick a name that matches the function's actual usage. Best guesses pending investigation: `deriveContextAlias`, `deriveDomainSeparatedAccount`, `deriveContextAccount`, `deriveBoundedAlias`. Final choice deferred to the implementation plan. - -**Changes.** - -- Rename the function in `product-sdk/packages/sdk/src/identity/product-account.ts`. -- Update every callsite within `product-sdk/` (the consistency requirement). -- Update the index re-export (`@parity/product-sdk/identity`) to the new name only. Old name is removed in the same PR. -- Type renames (`ProductAccountInfo` to the corresponding new name, if appropriate). -- Changeset entry flagging the breaking change. - -**External consumers.** Any external repo importing the old name will get a build error after upgrading. This is the explicit choice; we are not maintaining a deprecation alias. The changeset note plus a release notes line make the migration discoverable. - -## Implementation order - -Three repos, four PRs in approximately this sequence: - -1. **product-sdk PR (Section 1 + Section 3 Layer A + Layer B):** add `deriveProductAccountPublicKey` and `createChainCode` to `@parity/product-sdk-keys`. Add the frozen vector test file. Add the desktop parity script. Publish a new minor version. -2. **product-sdk PR (Section 6):** rename `deriveProductAccount` (blake2b util) consistently across the repo. Publish. -3. **playground-cli PR (Sections 2, 3 Layer C, 4, 5):** bump SDK deps, swap imports, delete `productAccountDerivation.ts`. Add the integration vitest case. Add the `dot init` username + product account display lines. Commit the CLI-as-host roadmap note. Verify per CLAUDE.md. -4. **playground-app PR (Section 3 Layer D):** add the `console.info` line after `getProductAccount`. Trivial. - -PR 1 and 2 can land in either order in product-sdk; PR 3 depends on PR 1. PR 4 is independent and can land anytime. - -## Verification - -The success criteria for this work: - -- `pnpm test` in product-sdk passes the four-vector fixture for `deriveProductAccountPublicKey`. -- `pnpm test && pnpm build && pnpm format:check && pnpm lint:license` in playground-cli pass. -- `grep -rn "deriveProductAccountPublicKey" playground-cli/src/` shows imports only from `@parity/product-sdk-keys`, no local file. -- A `dot init` run shows the username and product account address lines (with appropriate fallback strings when applicable). -- The same user, logging into `playground-app`, sees the same SS58 and H160 in the browser console (Layer D log). -- `grep -rn "deriveProductAccount\b" product-sdk/ playground-cli/` shows the old blake2b util name nowhere; only the renamed version is referenced. - -## Open questions deferred to implementation - -- Final name of the renamed blake2b util (Section 6): picked after callsite investigation. -- Whether `IdentityAdapter.readIdentities` (Section 4) is on-chain or IPC-wrapped: decides whether we import host-papp's adapter or query People parachain directly. -- Exact UX phrasing for the two `dot init` lines: refined during implementation against the existing Ink screen styling. -- Mobile (Android) cross-verification of the derivation algorithm: currently relies on code-reading; an optional Kotlin-side fixture is out of scope here.