@parity/product-sdk-terminal 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -49
- package/dist/adapter-Dt3ryKnw.d.ts +72 -0
- package/dist/{chunk-5MCLSFKQ.js → chunk-GHOFKVUG.js} +1 -1
- package/dist/chunk-GHOFKVUG.js.map +1 -0
- package/dist/host.d.ts +113 -0
- package/dist/host.js +1405 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +59 -62
- package/dist/index.js +192 -202
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +12 -4
- package/dist/testing.js.map +1 -1
- package/package.json +15 -18
- package/dist/chunk-5MCLSFKQ.js.map +0 -1
- package/dist/loader.mjs +0 -76
- package/dist/register.d.ts +0 -2
- package/dist/register.js +0 -9
- package/dist/register.js.map +0 -1
- package/scripts/patch-wasm.sh +0 -27
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapter.ts","../src/signer.ts","../src/sessions.ts","../src/qr-encode.ts"],"sourcesContent":["/**\n * Node.js adapter for the Polkadot host-papp SDK.\n *\n * Provides Node.js-compatible implementations of the SDK's storage and\n * transport layers, enabling QR login, attestation, and signing in\n * terminal/CLI environments.\n */\nimport {\n createPappAdapter,\n type PappAdapter,\n type HostMetadata,\n SS_STABLE_STAGE_ENDPOINTS,\n SS_PASEO_STABLE_STAGE_ENDPOINTS,\n} from \"@novasamatech/host-papp\";\nimport {\n createLazyClient,\n createPapiStatementStoreAdapter,\n type LazyClient,\n} from \"@novasamatech/statement-store\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { getWsProvider } from \"@polkadot-api/ws-provider\";\n\nimport { createNodeStorageAdapter } from \"./node-storage.js\";\n\nconst log = createLogger(\"terminal\");\n\n/** Options for creating a terminal adapter. */\nexport interface TerminalAdapterOptions {\n /** Unique app identifier. Used as the storage namespace. */\n appId: string;\n /** URL to the app's metadata JSON (name + icon), shown during pairing. */\n metadataUrl: string;\n /** Statement store WebSocket endpoints. Defaults to Paseo stable endpoints. */\n endpoints?: string[];\n /** Optional host metadata for the Sign-In screen. */\n hostMetadata?: HostMetadata;\n /**\n * Directory where session files are persisted. Defaults to\n * `~/.polkadot-apps/`. Override in tests to point at a temporary\n * directory populated with `createTestSession` from\n * `@parity/product-sdk-terminal/testing`.\n */\n storageDir?: string;\n}\n\n/**\n * Create a terminal adapter backed by the host-papp SDK.\n *\n * This sets up:\n * - File-based storage in `~/.polkadot-apps/` (since Node.js has no localStorage)\n * - WebSocket connection to the statement store\n * - The full SSO flow: QR pairing + on-chain attestation\n * - Session manager for signing requests\n */\n/** A PappAdapter with the `appId` it was created with and a `destroy` method for cleanup. */\nexport type TerminalAdapter = PappAdapter & {\n /** The `appId` passed to {@link createTerminalAdapter}. Useful for {@link createSessionSigner}. */\n readonly appId: string;\n /**\n * Disconnect the WebSocket and release resources.\n *\n * @remarks\n * Idempotent. Returns a Promise that resolves once all in-flight\n * statement-subscription teardowns have settled and the underlying\n * substrate client has been disconnected. **Awaiting is recommended\n * but not required** — callers that don't await get the same\n * fire-and-forget shape the previous version had, but they may see\n * the destroy-time RPC traffic finish after the function returns.\n *\n * The implementation tracks the server-side `statement_unsubscribe…`\n * RPCs `sessions.dispose()` fires, then awaits them via\n * `Promise.allSettled` before destroying the substrate-client request\n * queue. No timing-based guesses; no global-state mutations of\n * `console.error` or `process.on('unhandledRejection')`. Pending\n * subscribes (where `onSuccess` hasn't fired yet) are cancelled by\n * the underlying `getSubscribeFn` teardown via `cancelRequest()`,\n * which is the in-band fast-path and doesn't surface as a rejection.\n */\n destroy(): Promise<void>;\n};\n\nexport function createTerminalAdapter(options: TerminalAdapterOptions): TerminalAdapter {\n const endpoints = options.endpoints ?? SS_PASEO_STABLE_STAGE_ENDPOINTS;\n\n const storage = createNodeStorageAdapter(options.appId, options.storageDir);\n // ws-provider 0.9 takes endpoints positionally; relies on the global\n // WebSocket (Node ≥21) unless `websocketClass` is supplied.\n //\n // heartbeatTimeout uses setTimeout under the hood, which clamps to a\n // 32-bit signed integer. Passing Infinity triggers a noisy\n // `TimeoutOverflowWarning` on every reschedule. Use the int32 max\n // (~24.8 days) — effectively-never for any CLI session.\n const HEARTBEAT_NEVER_MS = 2_147_483_647;\n const rawLazyClient = createLazyClient(\n getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS }),\n );\n const trackedLazyClient = wrapLazyClient(rawLazyClient);\n const statementStore = createPapiStatementStoreAdapter(trackedLazyClient);\n\n const adapter = createPappAdapter({\n appId: options.appId,\n metadata: options.metadataUrl,\n hostMetadata: options.hostMetadata,\n adapters: {\n storage,\n lazyClient: trackedLazyClient,\n statementStore,\n },\n });\n\n let destroyPromise: Promise<void> | null = null;\n return {\n ...adapter,\n appId: options.appId,\n destroy(): Promise<void> {\n if (destroyPromise) return destroyPromise;\n destroyPromise = teardown(adapter.sessions, trackedLazyClient);\n return destroyPromise;\n },\n };\n}\n\n/**\n * Lazy-client wrapper that tracks server-side unsubscribe RPCs as Promises.\n *\n * `lazyClient.getSubscribeFn` returns a function whose teardown callback\n * fires `c._request(unsubscribeMethod, ...)` with `noop` `onSuccess` /\n * `onError` — the unsubscribe is fire-and-forget by upstream design. We\n * intercept those requests by replacing their callbacks with handlers\n * that resolve a tracking Promise on either outcome (success OR error —\n * we just need to know the request settled, not whether it succeeded).\n *\n * `awaitPendingUnsubs()` returns a Promise that settles when every\n * tracked unsubscribe has completed. Used by `teardown` to drain before\n * calling `disconnect`.\n *\n * The wrapper is otherwise transparent: `getClient`, `getRequestFn`,\n * `disconnect` pass through unchanged.\n */\ntype TrackedLazyClient = LazyClient & {\n awaitPendingUnsubs(): Promise<void>;\n};\n\nfunction wrapLazyClient(inner: LazyClient): TrackedLazyClient {\n const pendingUnsubs = new Set<Promise<void>>();\n const innerGetSubscribeFn = inner.getSubscribeFn.bind(inner);\n\n return {\n ...inner,\n getClient: inner.getClient.bind(inner),\n getRequestFn: inner.getRequestFn.bind(inner),\n disconnect: inner.disconnect.bind(inner),\n\n getSubscribeFn() {\n // Each call returns a new SubscribeFn. We wrap the teardown\n // callback so any unsubscribe RPC it fires gets tracked.\n const innerSubscribe = innerGetSubscribeFn();\n return ((method, params, onMessage, onError) => {\n const innerTeardown = innerSubscribe(method, params, onMessage, onError);\n return () => {\n // Track the unsubscribe with a Promise that resolves\n // after the microtask queue drains. The upstream code\n // uses `noop` callbacks on the actual RPC, so we\n // can't directly observe completion — but by the\n // time `innerTeardown()` returns, the `_request`\n // has been queued. Two microtask hops are enough\n // for the request to flush through the\n // substrate-client's send pipeline.\n //\n // If `innerTeardown()` throws synchronously, we let\n // the throw escape (the caller — `sessions.dispose()`\n // — is in the best position to decide what to do)\n // but the tracker still resolves so\n // `awaitPendingUnsubs()` doesn't hang and\n // `destroy()` still completes.\n const tracked = new Promise<void>((resolve) => {\n queueMicrotask(() => queueMicrotask(resolve));\n });\n pendingUnsubs.add(tracked);\n void tracked.finally(() => pendingUnsubs.delete(tracked));\n innerTeardown();\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>;\n },\n\n async awaitPendingUnsubs(): Promise<void> {\n // Snapshot so additions made during the await don't extend\n // the wait indefinitely (sessions.dispose() should have fired\n // them all synchronously by the time we're called).\n const snapshot = Array.from(pendingUnsubs);\n await Promise.allSettled(snapshot);\n },\n };\n}\n\n/**\n * Drain pending unsubscribes, then disconnect. Order matters and is now\n * deterministic — no `setTimeout` guesses, no global-state mutations.\n *\n * 1. `sessions.dispose()` walks each open statement-subscription and\n * triggers their RPC unsubscribe via the wrapped subscribe-fn's\n * teardown callback. Each unsubscribe is recorded as a tracked\n * Promise on the wrapper.\n * 2. `awaitPendingUnsubs()` waits for those tracked Promises to\n * settle (resolution OR rejection — we just need confirmation the\n * RPC has left the substrate-client send pipeline).\n * 3. `disconnect()` calls `substrateClient.destroy()`. Nothing is\n * pending at this point, so no `DestroyedError` rejections fire.\n *\n * If the disconnect call itself throws, log and continue rather than\n * propagating — caller can `await destroy()` without `try/catch`.\n */\nasync function teardown(\n sessions: { dispose(): void },\n lazyClient: TrackedLazyClient,\n): Promise<void> {\n log.debug(\"destroying terminal adapter\");\n sessions.dispose();\n await lazyClient.awaitPendingUnsubs();\n try {\n lazyClient.disconnect();\n } catch (e) {\n log.warn(\"lazyClient.disconnect threw during destroy\", { error: e });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n /**\n * Build a fake LazyClient whose `getSubscribeFn` records each subscribe\n * call and whose returned teardown is observable. Lets tests assert on\n * teardown invocation and on `disconnect` ordering.\n */\n function fakeLazyClient(): {\n client: LazyClient;\n teardownCalls: number;\n disconnectCalls: number;\n } {\n let teardownCalls = 0;\n let disconnectCalls = 0;\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => {\n return () => {\n teardownCalls += 1;\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>,\n disconnect: () => {\n disconnectCalls += 1;\n },\n } as LazyClient;\n return {\n client: fake,\n get teardownCalls() {\n return teardownCalls;\n },\n get disconnectCalls() {\n return disconnectCalls;\n },\n };\n }\n\n describe(\"wrapLazyClient\", () => {\n test(\"passes getClient / getRequestFn / disconnect through unchanged\", () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n wrapped.getClient();\n wrapped.disconnect();\n expect(fake.disconnectCalls).toBe(1);\n expect(typeof wrapped.getRequestFn).toBe(\"function\");\n });\n\n test(\"getSubscribeFn returns a wrapped subscribe whose teardown invokes the inner teardown\", () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n const subscribe = wrapped.getSubscribeFn();\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n teardown();\n\n expect(fake.teardownCalls).toBe(1);\n });\n\n test(\"awaitPendingUnsubs resolves after all wrapped teardowns settle\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n const subscribe = wrapped.getSubscribeFn();\n\n // Simulate three live subscriptions being torn down in the\n // same tick (what `sessions.dispose()` does internally).\n for (let i = 0; i < 3; i++) {\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n teardown();\n }\n\n // All three pending — but resolves once microtasks flush.\n await wrapped.awaitPendingUnsubs();\n expect(fake.teardownCalls).toBe(3);\n });\n\n test(\"awaitPendingUnsubs with no pending unsubs resolves immediately\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();\n });\n\n test(\"an unsubscribe whose teardown throws is still tracked and resolved\", async () => {\n // Even if the inner teardown throws, the tracker still needs\n // to settle — otherwise destroy() would hang forever.\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => {\n return () => {\n throw new Error(\"teardown boom\");\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>,\n disconnect: () => {},\n } as LazyClient;\n\n const wrapped = wrapLazyClient(fake);\n const subscribe = wrapped.getSubscribeFn();\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n\n // The wrapper's inner try/finally lets the throw escape (as\n // expected — we don't swallow user-visible errors), but the\n // tracker still resolves. Catch-and-await pattern:\n try {\n teardown();\n } catch {\n // expected\n }\n await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();\n });\n });\n\n describe(\"teardown\", () => {\n test(\"orders sessions.dispose, drain, then disconnect\", async () => {\n const order: string[] = [];\n\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n // Override disconnect to capture ordering.\n const innerDisconnect = wrapped.disconnect.bind(wrapped);\n wrapped.disconnect = () => {\n order.push(\"disconnect\");\n innerDisconnect();\n };\n\n const sessions = {\n dispose: vi.fn(() => {\n order.push(\"dispose\");\n // Simulate sessions.dispose firing one teardown.\n const subscribe = wrapped.getSubscribeFn();\n subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n )();\n }),\n };\n\n await teardown(sessions, wrapped);\n\n expect(order).toEqual([\"dispose\", \"disconnect\"]);\n expect(sessions.dispose).toHaveBeenCalledTimes(1);\n expect(fake.disconnectCalls).toBe(1);\n });\n\n test(\"disconnect runs even when there are no pending unsubs\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n const sessions = { dispose: vi.fn() };\n\n await teardown(sessions, wrapped);\n\n expect(fake.disconnectCalls).toBe(1);\n });\n\n test(\"logs a warning if disconnect throws, doesn't propagate to caller\", async () => {\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => () => {}) as ReturnType<\n LazyClient[\"getSubscribeFn\"]\n >,\n disconnect: () => {\n throw new Error(\"boom\");\n },\n } as LazyClient;\n\n const wrapped = wrapLazyClient(fake);\n const sessions = { dispose: vi.fn() };\n\n await expect(teardown(sessions, wrapped)).resolves.toBeUndefined();\n });\n\n test(\"awaits pending unsubs before calling disconnect\", async () => {\n // The whole point of the fix: disconnect must not run while\n // unsubscribe RPCs are still queued. Verify ordering even\n // when the unsubs take multiple microtasks to settle.\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n let unsubResolved = false;\n const innerDisconnect = wrapped.disconnect.bind(wrapped);\n wrapped.disconnect = () => {\n // Disconnect must not run before the unsubscribe has\n // resolved. If it does, this assertion fires.\n expect(unsubResolved).toBe(true);\n innerDisconnect();\n };\n\n const sessions = {\n dispose: () => {\n const subscribe = wrapped.getSubscribeFn();\n subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n )();\n // mark resolution after the microtasks the wrapper queues\n queueMicrotask(() =>\n queueMicrotask(() => {\n unsubResolved = true;\n }),\n );\n },\n };\n\n await teardown(sessions, wrapped);\n expect(fake.disconnectCalls).toBe(1);\n });\n });\n}\n\nexport { SS_STABLE_STAGE_ENDPOINTS, SS_PASEO_STABLE_STAGE_ENDPOINTS };\n","/**\n * Create a PolkadotSigner from a QR-paired session.\n *\n * Bridges the host-papp session to polkadot-api's `PolkadotSigner` interface\n * via `getPolkadotSignerFromPjs`, routing **transaction signing** through\n * `session.signPayload` (no `<Bytes>...</Bytes>` envelope — produces a\n * signature over the actual extrinsic payload) and **raw-message signing**\n * through `session.signRaw` (mobile applies the `<Bytes>...</Bytes>`\n * anti-phishing wrap, as expected for arbitrary data).\n *\n * Routing both paths through `signRaw` (as a single PAPI callback would)\n * causes the chain to reject every tx with `BadProof`, because the mobile\n * wallet wraps even SCALE-encoded extrinsic payloads with the anti-phishing\n * envelope before signing.\n *\n * @example\n * ```ts\n * const [session] = adapter.sessions.sessions.read();\n *\n * // Default account — uses [adapter.appId, 0]:\n * const signer = createSessionSigner(session, adapter);\n *\n * // Non-default derivation index, or a different productId:\n * const subSigner = createSessionSignerForAccount(session, {\n * productId: \"my-product\",\n * derivationIndex: 3,\n * });\n *\n * await contract.publish.tx(domain, cid, { signer, origin });\n * ```\n */\nimport { getPolkadotSignerFromPjs } from \"polkadot-api/pjs-signer\";\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { fromHex, toHex } from \"@polkadot-api/utils\";\nimport type { UserSession } from \"@novasamatech/host-papp\";\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Identifies which sub-account of a paired session should sign.\n *\n * Mirrors the `host-papp` wire format `productAccountId: [productId, derivationIndex]`:\n * `productId` is the dotNS-style identifier for the requesting product (matches\n * the adapter's `appId` in normal usage); `derivationIndex` is the BIP32-style\n * child-key index, where `0` is the session's default account.\n */\nexport interface ProductAccountRef {\n /** The product identifier. Usually equal to the adapter's `appId`. */\n productId: string;\n /** Child-key derivation index. `0` is the default account. */\n derivationIndex: number;\n}\n\n/**\n * Defensively prepend `0x` if missing.\n *\n * PAPI's PJS payload types every hex field as `HexString = string` with no\n * runtime check. In practice the signed-extension mappers always emit\n * `0x`-prefixed values, so this should never actually prepend — but the\n * type contract doesn't guarantee it, and host-papp's SCALE codec rejects\n * unprefixed input. Mirrors the `asHex` helper in `@novasamatech/product-sdk`'s\n * in-host signer for the same reason.\n */\nfunction asHex(v: string): `0x${string}` {\n return v.startsWith(\"0x\") ? (v as `0x${string}`) : (`0x${v}` as `0x${string}`);\n}\n\n/**\n * Build the `signPayload` callback PAPI calls for transaction signing.\n *\n * Translates PAPI's `SignerPayloadJSON` into host-papp's `SigningPayloadRequest`\n * and routes it to `session.signPayload` — the mobile wallet's JSON-payload\n * interactor, which signs the SCALE-encoded extrinsic directly with no\n * `<Bytes>` envelope. This is the path that the chain accepts.\n *\n * Exported only via `import.meta.vitest` so unit tests can exercise the\n * callback wiring directly without standing up a full PolkadotSigner.\n */\nfunction makeSignPayloadCallback(session: UserSession, productAccountId: [string, number]) {\n return async (\n payload: PjsSignerPayloadJSON,\n ): Promise<{\n signature: `0x${string}`;\n signedTransaction?: `0x${string}`;\n }> => {\n const result = await session.signPayload({\n productAccountId,\n blockHash: asHex(payload.blockHash),\n blockNumber: asHex(payload.blockNumber),\n era: asHex(payload.era),\n genesisHash: asHex(payload.genesisHash),\n method: asHex(payload.method),\n nonce: asHex(payload.nonce),\n specVersion: asHex(payload.specVersion),\n tip: asHex(payload.tip),\n transactionVersion: asHex(payload.transactionVersion),\n signedExtensions: payload.signedExtensions,\n version: payload.version,\n // PJS types `assetId` as `number | object`. In practice the\n // ChargeAssetTxPayment mapper always emits a hex string when\n // present — we trust the mapper rather than runtime-checking,\n // matching the reference in `@novasamatech/product-sdk`.\n assetId:\n payload.assetId !== undefined\n ? (payload.assetId as never as `0x${string}`)\n : undefined,\n metadataHash: payload.metadataHash ? asHex(payload.metadataHash) : undefined,\n mode: payload.mode,\n withSignedTransaction: payload.withSignedTransaction,\n });\n\n if (result.isErr()) {\n throw new Error(`Mobile signing rejected: ${result.error.message}`);\n }\n\n return {\n signature: toHex(result.value.signature) as `0x${string}`,\n signedTransaction: result.value.signedTransaction\n ? (toHex(result.value.signedTransaction) as `0x${string}`)\n : undefined,\n };\n };\n}\n\n/**\n * Build the `signRaw` callback PAPI calls for arbitrary-byte signing\n * (`signBytes`). Routes to `session.signRaw` — the mobile wallet's raw\n * interactor, which applies the standard `<Bytes>...</Bytes>` anti-phishing\n * envelope before signing. Correct for arbitrary user data.\n *\n * Exported only via `import.meta.vitest` for direct unit tests.\n */\nfunction makeSignRawCallback(session: UserSession, productAccountId: [string, number]) {\n return async (\n payload: PjsSignRawPayload,\n ): Promise<{\n id: number;\n signature: `0x${string}`;\n }> => {\n const result = await session.signRaw({\n productAccountId,\n data: { tag: \"Bytes\" as const, value: fromHex(payload.data) },\n });\n\n if (result.isErr()) {\n throw new Error(`Mobile signing rejected: ${result.error.message}`);\n }\n\n return {\n id: 0,\n signature: toHex(result.value.signature) as `0x${string}`,\n };\n };\n}\n\n// Minimal local types for the PJS signer callback inputs. We don't import\n// the full `SignerPayloadJSON` from `@polkadot-api/pjs-signer` because the\n// version installed exposes it as an internal type. These are structurally\n// correct for the fields we actually read.\ntype PjsSignerPayloadJSON = {\n address: string;\n assetId?: number | object;\n blockHash: string;\n blockNumber: string;\n era: string;\n genesisHash: string;\n metadataHash?: string;\n method: string;\n mode?: number;\n nonce: string;\n specVersion: string;\n tip: string;\n transactionVersion: string;\n signedExtensions: string[];\n version: number;\n withSignedTransaction?: boolean;\n};\n\ntype PjsSignRawPayload = {\n address: string;\n data: string;\n type: \"bytes\";\n};\n\nfunction buildSessionSigner(session: UserSession, ref: ProductAccountRef): PolkadotSigner {\n const accountId = new Uint8Array(session.remoteAccount.accountId);\n const productAccountId: [string, number] = [ref.productId, ref.derivationIndex];\n // getPolkadotSignerFromPjs accepts a \"0x\"-prefixed hex AccountId as its\n // address; it derives `publicKey` from this directly. The host-papp side\n // of signing identifies accounts by `productAccountId` instead, so we\n // ignore the `address` field PAPI later passes back into our callbacks\n // and use the closure-captured `productAccountId` there.\n const accountIdHex = asHex(toHex(accountId));\n\n return getPolkadotSignerFromPjs(\n accountIdHex,\n makeSignPayloadCallback(session, productAccountId),\n makeSignRawCallback(session, productAccountId),\n );\n}\n\n/**\n * Create a `PolkadotSigner` backed by a QR-paired mobile wallet session,\n * using the session's **default account** (`derivationIndex: 0`).\n *\n * For non-default sub-accounts, use {@link createSessionSignerForAccount}.\n *\n * @param session The paired user session.\n * @param adapter The {@link TerminalAdapter} that loaded the session. Its `appId`\n * is used as the `productId` in the wire request.\n */\nexport function createSessionSigner(\n session: UserSession,\n adapter: TerminalAdapter,\n): PolkadotSigner {\n return buildSessionSigner(session, { productId: adapter.appId, derivationIndex: 0 });\n}\n\n/**\n * Create a `PolkadotSigner` for a specific sub-account of a paired session.\n *\n * Use this when you need a derivation index other than `0`, or a `productId`\n * different from the adapter's `appId`. For the common default-account case,\n * prefer {@link createSessionSigner}.\n *\n * @param session The paired user session.\n * @param ref The product account to sign as: `{ productId, derivationIndex }`.\n */\nexport function createSessionSignerForAccount(\n session: UserSession,\n ref: ProductAccountRef,\n): PolkadotSigner {\n return buildSessionSigner(session, ref);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n const { ok, err } = await import(\"neverthrow\");\n\n /**\n * Build a minimal `UserSession`-shaped stub. Both `signPayload` and\n * `signRaw` accept request-capturing functions so tests can assert on\n * exactly which host-papp method got called and with what payload.\n */\n function makeSession(opts: {\n signPayload?: (req: unknown) => Promise<unknown>;\n signRaw?: (req: unknown) => Promise<unknown>;\n accountIdBytes?: number[];\n }): UserSession {\n const accountIdBytes = opts.accountIdBytes ?? new Array(32).fill(0).map((_, i) => i);\n return {\n remoteAccount: { accountId: accountIdBytes },\n signPayload: vi.fn(\n opts.signPayload ??\n (async () => {\n throw new Error(\"signPayload not stubbed in this test\");\n }),\n ),\n signRaw: vi.fn(\n opts.signRaw ??\n (async () => {\n throw new Error(\"signRaw not stubbed in this test\");\n }),\n ),\n } as unknown as UserSession;\n }\n\n function fakeAdapter(appId: string): TerminalAdapter {\n // Only the `appId` field matters for these tests.\n return { appId } as unknown as TerminalAdapter;\n }\n\n describe(\"createSessionSigner\", () => {\n test(\"exposes Sr25519 public key matching remoteAccount.accountId\", () => {\n const bytes = Array.from({ length: 32 }, (_, i) => i);\n const signer = createSessionSigner(\n makeSession({ accountIdBytes: bytes }),\n fakeAdapter(\"test-app\"),\n );\n expect(signer.publicKey).toEqual(new Uint8Array(bytes));\n });\n\n test(\"signBytes routes through session.signRaw (anti-phishing wrap path)\", async () => {\n const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: sig });\n },\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n const out = await signer.signBytes(new Uint8Array([1, 2, 3]));\n expect(out).toEqual(sig);\n\n // The raw path forwards the bytes verbatim under the \"Bytes\" tag.\n // Mobile applies the <Bytes>...</Bytes> envelope on its side.\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"test-app\", 0]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toBeInstanceOf(Uint8Array);\n });\n\n test(\"signBytes does NOT call session.signPayload (regression guard for BadProof bug)\", async () => {\n // The original bug was a single PAPI callback that funneled\n // every signing operation through signRaw. After the fix,\n // signBytes is the only thing that should reach signRaw —\n // signPayload is reserved for tx signing. This test guards\n // against accidentally regressing to the unified-callback shape.\n const session = makeSession({\n signRaw: async () => ok({ signature: new Uint8Array([1]) }),\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await signer.signBytes(new Uint8Array([1, 2, 3]));\n\n const sessionWithSpies = session as unknown as {\n signPayload: { mock: { calls: unknown[] } };\n signRaw: { mock: { calls: unknown[] } };\n };\n expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);\n expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);\n });\n\n test(\"signRaw throws with a clear error when the mobile rejects\", async () => {\n const session = makeSession({\n signRaw: async () => err({ message: \"user declined\" }),\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(\n \"Mobile signing rejected: user declined\",\n );\n });\n });\n\n describe(\"makeSignPayloadCallback — tx signing path (the BadProof fix)\", () => {\n // These tests directly exercise the callback PAPI hands to its\n // signTx implementation. Bypassing PAPI's getPolkadotSignerFromPjs\n // wrapper lets us assert on the wire-format translation without\n // building synthetic extrinsic metadata. The full signTx → callback\n // → chain roundtrip is gated by the manual smoke test.\n\n function pjsPayload(overrides: Partial<PjsSignerPayloadJSON> = {}): PjsSignerPayloadJSON {\n return {\n address: `0x${\"00\".repeat(32)}`,\n blockHash: `0x${\"11\".repeat(32)}`,\n blockNumber: \"0x12345678\",\n era: \"0xc501\",\n genesisHash: `0x${\"22\".repeat(32)}`,\n method: \"0xabcdef\",\n nonce: \"0x00000001\",\n specVersion: \"0x000003e8\",\n tip: `0x${\"0\".repeat(32)}`,\n transactionVersion: \"0x00000001\",\n signedExtensions: [\"CheckMortality\", \"CheckNonce\"],\n version: 4,\n ...overrides,\n };\n }\n\n test(\"forwards request to session.signPayload with the right productAccountId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signPayload: async (req) => {\n captured.push(req);\n return ok({\n signature: new Uint8Array([0xaa, 0xbb]),\n signedTransaction: undefined,\n });\n },\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n await callback(pjsPayload());\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"my-app\", 0]);\n });\n\n test(\"does NOT call session.signRaw (BadProof regression guard)\", async () => {\n // The whole point of the fix: tx signing must hit signPayload,\n // never signRaw. signRaw applies the <Bytes> envelope which\n // produces signatures the chain rejects.\n const session = makeSession({\n signPayload: async () =>\n ok({ signature: new Uint8Array([1]), signedTransaction: undefined }),\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n await callback(pjsPayload());\n\n const sessionWithSpies = session as unknown as {\n signPayload: { mock: { calls: unknown[] } };\n signRaw: { mock: { calls: unknown[] } };\n };\n expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(1);\n expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(0);\n });\n\n test(\"translates every PJS hex field into a 0x-prefixed string\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signPayload: async (req) => {\n captured.push(req);\n return ok({\n signature: new Uint8Array([0]),\n signedTransaction: undefined,\n });\n },\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n await callback(\n pjsPayload({\n // Mix of already-prefixed and unprefixed inputs to exercise asHex.\n blockHash: `0x${\"ab\".repeat(32)}`,\n nonce: \"1234abcd\", // unprefixed — asHex must add 0x\n }),\n );\n\n const req = captured[0] as Record<string, unknown>;\n for (const field of [\n \"blockHash\",\n \"blockNumber\",\n \"era\",\n \"genesisHash\",\n \"method\",\n \"nonce\",\n \"specVersion\",\n \"tip\",\n \"transactionVersion\",\n ]) {\n expect(req[field], `${field} must be 0x-prefixed`).toMatch(/^0x[0-9a-fA-F]*$/);\n }\n });\n\n test(\"forwards optional fields when present, omits when absent\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signPayload: async (req) => {\n captured.push(req);\n return ok({\n signature: new Uint8Array([0]),\n signedTransaction: undefined,\n });\n },\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n await callback(\n pjsPayload({\n metadataHash: \"0xdeadbeef\",\n mode: 1,\n withSignedTransaction: true,\n assetId: \"0xfeed\" as unknown as object,\n }),\n );\n\n const req = captured[0] as Record<string, unknown>;\n expect(req.metadataHash).toBe(\"0xdeadbeef\");\n expect(req.mode).toBe(1);\n expect(req.withSignedTransaction).toBe(true);\n expect(req.assetId).toBe(\"0xfeed\");\n });\n\n test(\"hex-encodes signature and signedTransaction in the PJS return shape\", async () => {\n const session = makeSession({\n signPayload: async () =>\n ok({\n signature: new Uint8Array([0xab, 0xcd]),\n signedTransaction: new Uint8Array([0x01, 0x02, 0x03]),\n }),\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n const result = await callback(pjsPayload());\n\n expect(result.signature).toBe(\"0xabcd\");\n expect(result.signedTransaction).toBe(\"0x010203\");\n });\n\n test(\"omits signedTransaction when host-papp returns undefined\", async () => {\n const session = makeSession({\n signPayload: async () =>\n ok({\n signature: new Uint8Array([0]),\n signedTransaction: undefined,\n }),\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n const result = await callback(pjsPayload());\n\n expect(result.signature).toBe(\"0x00\");\n expect(result.signedTransaction).toBeUndefined();\n });\n\n test(\"throws a clear error when the mobile rejects\", async () => {\n const session = makeSession({\n signPayload: async () => err({ message: \"user declined\" }),\n });\n\n const callback = makeSignPayloadCallback(session, [\"my-app\", 0]);\n\n await expect(callback(pjsPayload())).rejects.toThrow(\n \"Mobile signing rejected: user declined\",\n );\n });\n });\n\n describe(\"makeSignRawCallback\", () => {\n test(\"forwards request to session.signRaw with the right productAccountId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0xff]) });\n },\n });\n\n const callback = makeSignRawCallback(session, [\"my-app\", 5]);\n await callback({\n address: `0x${\"00\".repeat(32)}`,\n data: \"0xdeadbeef\",\n type: \"bytes\",\n });\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 5]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(Array.from(req.data.value)).toEqual([0xde, 0xad, 0xbe, 0xef]);\n });\n\n test(\"hex-encodes signature in the PJS return shape with id: 0\", async () => {\n const session = makeSession({\n signRaw: async () => ok({ signature: new Uint8Array([0x42]) }),\n });\n\n const callback = makeSignRawCallback(session, [\"my-app\", 0]);\n const result = await callback({\n address: `0x${\"00\".repeat(32)}`,\n data: \"0x00\",\n type: \"bytes\",\n });\n\n expect(result.id).toBe(0);\n expect(result.signature).toBe(\"0x42\");\n });\n });\n\n describe(\"createSessionSignerForAccount\", () => {\n test(\"forwards productAccountId from the explicit ref\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([42]) });\n },\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"my-app\",\n derivationIndex: 7,\n });\n await signer.signBytes(new Uint8Array([10, 20, 30]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 7]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toBeInstanceOf(Uint8Array);\n });\n\n test(\"supports a productId different from any adapter's appId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0]) });\n },\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"external-product\",\n derivationIndex: 0,\n });\n await signer.signBytes(new Uint8Array([1]));\n\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"external-product\", 0]);\n });\n });\n}\n","/**\n * Helpers for working with persisted sessions on a `TerminalAdapter`.\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Wait for the adapter to load at least one persisted session, or resolve\n * with an empty array after `timeoutMs`.\n *\n * The session manager loads sessions from storage asynchronously, so a\n * synchronous `adapter.sessions.sessions.read()` immediately after\n * `createTerminalAdapter()` may return `[]` even when sessions exist on\n * disk. Use this helper to give the loader a chance to populate before\n * deciding whether the user is logged in.\n */\nexport function waitForSessions(\n adapter: TerminalAdapter,\n timeoutMs = 3000,\n): Promise<UserSession[]> {\n return new Promise((resolve) => {\n let resolved = false;\n let unsub: (() => void) | null = null;\n\n const finish = (sessions: UserSession[]) => {\n if (resolved) return;\n resolved = true;\n // Defer unsub so the synchronous initial emission can complete\n // before we tear down the subscription.\n queueMicrotask(() => unsub?.());\n resolve(sessions);\n };\n\n const timer = setTimeout(() => finish([]), timeoutMs);\n\n unsub = adapter.sessions.sessions.subscribe((sessions: UserSession[]) => {\n if (sessions.length > 0) {\n clearTimeout(timer);\n finish(sessions);\n }\n });\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n type Subscriber = (sessions: UserSession[]) => void;\n\n function fakeAdapter(): {\n adapter: TerminalAdapter;\n emit: (sessions: UserSession[]) => void;\n subscriberCount: () => number;\n } {\n const subscribers = new Set<Subscriber>();\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n },\n },\n } as unknown as TerminalAdapter;\n return {\n adapter,\n emit: (sessions) => {\n for (const cb of subscribers) cb(sessions);\n },\n subscriberCount: () => subscribers.size,\n };\n }\n\n describe(\"waitForSessions\", () => {\n test(\"resolves with sessions on first non-empty emission\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"ignores empty emissions and waits for a non-empty one\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n emit([]);\n emit([]);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"resolves with [] after timeout when nothing is emitted\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n const promise = waitForSessions(adapter, 50);\n vi.advanceTimersByTime(50);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"unsubscribes after resolving\", async () => {\n const { adapter, emit, subscriberCount } = fakeAdapter();\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const promise = waitForSessions(adapter, 1000);\n emit([session]);\n await promise;\n // Wait for queueMicrotask\n await Promise.resolve();\n expect(subscriberCount()).toBe(0);\n });\n\n test(\"uses default 3000ms timeout when none is passed\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n // No timeoutMs argument — exercises the `?? 3000` default branch.\n const promise = waitForSessions(adapter);\n // Just short of 3000ms: still pending.\n vi.advanceTimersByTime(2999);\n let settled = false;\n promise.then(() => {\n settled = true;\n });\n await Promise.resolve();\n expect(settled).toBe(false);\n // Cross the default boundary.\n vi.advanceTimersByTime(1);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"handles synchronous initial emission\", async () => {\n // Some subscribables emit current value synchronously inside subscribe().\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n cb([session]);\n return () => {};\n },\n },\n },\n } as unknown as TerminalAdapter;\n await expect(waitForSessions(adapter, 1000)).resolves.toEqual([session]);\n });\n });\n}\n","/**\n * Terminal QR code rendering using Unicode half-block characters.\n *\n * Wraps the `qrcode` npm package's UTF-8 renderer, which packs two\n * module rows per terminal line using half-block characters.\n */\n\n/** Options for QR code rendering. */\nexport interface QrRenderOptions {\n /** Error correction level. Default: \"M\". */\n errorCorrectionLevel?: \"L\" | \"M\" | \"Q\" | \"H\";\n /** Quiet zone size in modules. Default: 2. */\n margin?: number;\n}\n\n/**\n * Encode a string as a QR code rendered in Unicode half-block characters.\n *\n * Returns a multi-line string suitable for `console.log`.\n */\nexport async function renderQrCode(data: string, options?: QrRenderOptions): Promise<string> {\n const QRCode = await import(\"qrcode\");\n const result = await QRCode.toString(data, {\n type: \"utf8\",\n errorCorrectionLevel: options?.errorCorrectionLevel ?? \"M\",\n margin: options?.margin ?? 2,\n });\n return result;\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"renderQrCode produces non-empty output\", async () => {\n const result = await renderQrCode(\"https://example.com\");\n expect(result.length).toBeGreaterThan(0);\n expect(result).toContain(\"\\n\");\n });\n\n test(\"renderQrCode contains Unicode block characters\", async () => {\n const result = await renderQrCode(\"test\");\n expect(/[▀▄█]/.test(result)).toBe(true);\n });\n\n test(\"different inputs produce different QR codes\", async () => {\n const a = await renderQrCode(\"hello\");\n const b = await renderQrCode(\"world\");\n expect(a).not.toBe(b);\n });\n}\n"],"mappings":"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","names":["fakeLazyClient","teardown","makeSession","fakeAdapter","req","fakeAdapter"]}
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/signer.ts","../src/sessions.ts","../src/qr-encode.ts"],"sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Node.js adapter for the Polkadot host-papp SDK.\n *\n * Provides Node.js-compatible implementations of the SDK's storage and\n * transport layers, enabling QR login, attestation, and signing in\n * terminal/CLI environments.\n */\nimport {\n createPappAdapter,\n type PappAdapter,\n type HostMetadata,\n SS_STABLE_STAGE_ENDPOINTS,\n SS_PASEO_STABLE_STAGE_ENDPOINTS,\n} from \"@novasamatech/host-papp\";\nimport {\n createLazyClient,\n createPapiStatementStoreAdapter,\n type LazyClient,\n} from \"@novasamatech/statement-store\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { getWsProvider } from \"@polkadot-api/ws-provider\";\n\nimport { createNodeStorageAdapter } from \"./node-storage.js\";\n\nconst log = createLogger(\"terminal\");\n\n/** Options for creating a terminal adapter. */\nexport interface TerminalAdapterOptions {\n /** Unique app identifier. Used as the storage namespace. */\n appId: string;\n /** Statement store WebSocket endpoints. Defaults to Paseo stable endpoints. */\n endpoints?: string[];\n /** Optional host metadata for the Sign-In screen. */\n hostMetadata?: HostMetadata;\n /**\n * Directory where session files are persisted. Defaults to\n * `~/.polkadot-apps/`. Override in tests to point at a temporary\n * directory populated with `createTestSession` from\n * `@parity/product-sdk-terminal/testing`.\n */\n storageDir?: string;\n}\n\n/**\n * Create a terminal adapter backed by the host-papp SDK.\n *\n * This sets up:\n * - File-based storage in `~/.polkadot-apps/` (since Node.js has no localStorage)\n * - WebSocket connection to the statement store\n * - The full SSO flow: QR pairing + on-chain attestation\n * - Session manager for signing requests\n */\n/** A PappAdapter with the `appId` it was created with and a `destroy` method for cleanup. */\nexport type TerminalAdapter = PappAdapter & {\n /** The `appId` passed to {@link createTerminalAdapter}. Useful for {@link createSessionSigner}. */\n readonly appId: string;\n /**\n * The on-disk storage directory used for sessions and (when the\n * host-runner facet is in use) for the allowance-key cache the\n * `./host` subpath maintains. `undefined` when the default\n * is in use; set explicitly when the caller passed `storageDir` to\n * {@link createTerminalAdapter}.\n */\n readonly storageDir?: string;\n /**\n * Disconnect the WebSocket and release resources.\n *\n * @remarks\n * Idempotent. Returns a Promise that resolves once all in-flight\n * statement-subscription teardowns have settled and the underlying\n * substrate client has been disconnected. **Awaiting is recommended\n * but not required** — callers that don't await get the same\n * fire-and-forget shape the previous version had, but they may see\n * the destroy-time RPC traffic finish after the function returns.\n *\n * The implementation tracks the server-side `statement_unsubscribe…`\n * RPCs `sessions.dispose()` fires, then awaits them via\n * `Promise.allSettled` before destroying the substrate-client request\n * queue. No timing-based guesses; no global-state mutations of\n * `console.error` or `process.on('unhandledRejection')`. Pending\n * subscribes (where `onSuccess` hasn't fired yet) are cancelled by\n * the underlying `getSubscribeFn` teardown via `cancelRequest()`,\n * which is the in-band fast-path and doesn't surface as a rejection.\n */\n destroy(): Promise<void>;\n};\n\nexport function createTerminalAdapter(options: TerminalAdapterOptions): TerminalAdapter {\n const endpoints = options.endpoints ?? SS_PASEO_STABLE_STAGE_ENDPOINTS;\n\n const storage = createNodeStorageAdapter(options.appId, options.storageDir);\n // ws-provider 0.9 takes endpoints positionally; relies on the global\n // WebSocket (Node ≥21) unless `websocketClass` is supplied.\n //\n // heartbeatTimeout uses setTimeout under the hood, which clamps to a\n // 32-bit signed integer. Passing Infinity triggers a noisy\n // `TimeoutOverflowWarning` on every reschedule. Use the int32 max\n // (~24.8 days) — effectively-never for any CLI session.\n const HEARTBEAT_NEVER_MS = 2_147_483_647;\n const rawLazyClient = createLazyClient(\n getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS }),\n );\n const trackedLazyClient = wrapLazyClient(rawLazyClient);\n const statementStore = createPapiStatementStoreAdapter(trackedLazyClient);\n\n const adapter = createPappAdapter({\n appId: options.appId,\n hostMetadata: options.hostMetadata,\n adapters: {\n storage,\n lazyClient: trackedLazyClient,\n statementStore,\n },\n });\n\n let destroyPromise: Promise<void> | null = null;\n return {\n ...adapter,\n appId: options.appId,\n storageDir: options.storageDir,\n destroy(): Promise<void> {\n if (destroyPromise) return destroyPromise;\n destroyPromise = teardown(adapter.sessions, trackedLazyClient);\n return destroyPromise;\n },\n };\n}\n\n/**\n * Lazy-client wrapper that tracks server-side unsubscribe RPCs as Promises.\n *\n * `lazyClient.getSubscribeFn` returns a function whose teardown callback\n * fires `c._request(unsubscribeMethod, ...)` with `noop` `onSuccess` /\n * `onError` — the unsubscribe is fire-and-forget by upstream design. We\n * intercept those requests by replacing their callbacks with handlers\n * that resolve a tracking Promise on either outcome (success OR error —\n * we just need to know the request settled, not whether it succeeded).\n *\n * `awaitPendingUnsubs()` returns a Promise that settles when every\n * tracked unsubscribe has completed. Used by `teardown` to drain before\n * calling `disconnect`.\n *\n * The wrapper is otherwise transparent: `getClient`, `getRequestFn`,\n * `disconnect` pass through unchanged.\n */\ntype TrackedLazyClient = LazyClient & {\n awaitPendingUnsubs(): Promise<void>;\n};\n\nfunction wrapLazyClient(inner: LazyClient): TrackedLazyClient {\n const pendingUnsubs = new Set<Promise<void>>();\n const innerGetSubscribeFn = inner.getSubscribeFn.bind(inner);\n\n return {\n ...inner,\n getClient: inner.getClient.bind(inner),\n getRequestFn: inner.getRequestFn.bind(inner),\n disconnect: inner.disconnect.bind(inner),\n\n getSubscribeFn() {\n // Each call returns a new SubscribeFn. We wrap the teardown\n // callback so any unsubscribe RPC it fires gets tracked.\n const innerSubscribe = innerGetSubscribeFn();\n return ((method, params, onMessage, onError) => {\n const innerTeardown = innerSubscribe(method, params, onMessage, onError);\n return () => {\n // Track the unsubscribe with a Promise that resolves\n // after the microtask queue drains. The upstream code\n // uses `noop` callbacks on the actual RPC, so we\n // can't directly observe completion — but by the\n // time `innerTeardown()` returns, the `_request`\n // has been queued. Two microtask hops are enough\n // for the request to flush through the\n // substrate-client's send pipeline.\n //\n // If `innerTeardown()` throws synchronously, we let\n // the throw escape (the caller — `sessions.dispose()`\n // — is in the best position to decide what to do)\n // but the tracker still resolves so\n // `awaitPendingUnsubs()` doesn't hang and\n // `destroy()` still completes.\n const tracked = new Promise<void>((resolve) => {\n queueMicrotask(() => queueMicrotask(resolve));\n });\n pendingUnsubs.add(tracked);\n void tracked.finally(() => pendingUnsubs.delete(tracked));\n innerTeardown();\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>;\n },\n\n async awaitPendingUnsubs(): Promise<void> {\n // Snapshot so additions made during the await don't extend\n // the wait indefinitely (sessions.dispose() should have fired\n // them all synchronously by the time we're called).\n const snapshot = Array.from(pendingUnsubs);\n await Promise.allSettled(snapshot);\n },\n };\n}\n\n/**\n * Drain pending unsubscribes, then disconnect. Order matters and is now\n * deterministic — no `setTimeout` guesses, no global-state mutations.\n *\n * 1. `sessions.dispose()` walks each open statement-subscription and\n * triggers their RPC unsubscribe via the wrapped subscribe-fn's\n * teardown callback. Each unsubscribe is recorded as a tracked\n * Promise on the wrapper.\n * 2. `awaitPendingUnsubs()` waits for those tracked Promises to\n * settle (resolution OR rejection — we just need confirmation the\n * RPC has left the substrate-client send pipeline).\n * 3. `disconnect()` calls `substrateClient.destroy()`. Nothing is\n * pending at this point, so no `DestroyedError` rejections fire.\n *\n * If the disconnect call itself throws, log and continue rather than\n * propagating — caller can `await destroy()` without `try/catch`.\n */\nasync function teardown(\n sessions: { dispose(): void },\n lazyClient: TrackedLazyClient,\n): Promise<void> {\n log.debug(\"destroying terminal adapter\");\n sessions.dispose();\n await lazyClient.awaitPendingUnsubs();\n try {\n lazyClient.disconnect();\n } catch (e) {\n log.warn(\"lazyClient.disconnect threw during destroy\", { error: e });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n /**\n * Build a fake LazyClient whose `getSubscribeFn` records each subscribe\n * call and whose returned teardown is observable. Lets tests assert on\n * teardown invocation and on `disconnect` ordering.\n */\n function fakeLazyClient(): {\n client: LazyClient;\n teardownCalls: number;\n disconnectCalls: number;\n } {\n let teardownCalls = 0;\n let disconnectCalls = 0;\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => {\n return () => {\n teardownCalls += 1;\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>,\n disconnect: () => {\n disconnectCalls += 1;\n },\n } as LazyClient;\n return {\n client: fake,\n get teardownCalls() {\n return teardownCalls;\n },\n get disconnectCalls() {\n return disconnectCalls;\n },\n };\n }\n\n describe(\"wrapLazyClient\", () => {\n test(\"passes getClient / getRequestFn / disconnect through unchanged\", () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n wrapped.getClient();\n wrapped.disconnect();\n expect(fake.disconnectCalls).toBe(1);\n expect(typeof wrapped.getRequestFn).toBe(\"function\");\n });\n\n test(\"getSubscribeFn returns a wrapped subscribe whose teardown invokes the inner teardown\", () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n const subscribe = wrapped.getSubscribeFn();\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n teardown();\n\n expect(fake.teardownCalls).toBe(1);\n });\n\n test(\"awaitPendingUnsubs resolves after all wrapped teardowns settle\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n const subscribe = wrapped.getSubscribeFn();\n\n // Simulate three live subscriptions being torn down in the\n // same tick (what `sessions.dispose()` does internally).\n for (let i = 0; i < 3; i++) {\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n teardown();\n }\n\n // All three pending — but resolves once microtasks flush.\n await wrapped.awaitPendingUnsubs();\n expect(fake.teardownCalls).toBe(3);\n });\n\n test(\"awaitPendingUnsubs with no pending unsubs resolves immediately\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();\n });\n\n test(\"an unsubscribe whose teardown throws is still tracked and resolved\", async () => {\n // Even if the inner teardown throws, the tracker still needs\n // to settle — otherwise destroy() would hang forever.\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => {\n return () => {\n throw new Error(\"teardown boom\");\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>,\n disconnect: () => {},\n } as LazyClient;\n\n const wrapped = wrapLazyClient(fake);\n const subscribe = wrapped.getSubscribeFn();\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n\n // The wrapper's inner try/finally lets the throw escape (as\n // expected — we don't swallow user-visible errors), but the\n // tracker still resolves. Catch-and-await pattern:\n try {\n teardown();\n } catch {\n // expected\n }\n await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();\n });\n });\n\n describe(\"teardown\", () => {\n test(\"orders sessions.dispose, drain, then disconnect\", async () => {\n const order: string[] = [];\n\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n // Override disconnect to capture ordering.\n const innerDisconnect = wrapped.disconnect.bind(wrapped);\n wrapped.disconnect = () => {\n order.push(\"disconnect\");\n innerDisconnect();\n };\n\n const sessions = {\n dispose: vi.fn(() => {\n order.push(\"dispose\");\n // Simulate sessions.dispose firing one teardown.\n const subscribe = wrapped.getSubscribeFn();\n subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n )();\n }),\n };\n\n await teardown(sessions, wrapped);\n\n expect(order).toEqual([\"dispose\", \"disconnect\"]);\n expect(sessions.dispose).toHaveBeenCalledTimes(1);\n expect(fake.disconnectCalls).toBe(1);\n });\n\n test(\"disconnect runs even when there are no pending unsubs\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n const sessions = { dispose: vi.fn() };\n\n await teardown(sessions, wrapped);\n\n expect(fake.disconnectCalls).toBe(1);\n });\n\n test(\"logs a warning if disconnect throws, doesn't propagate to caller\", async () => {\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => () => {}) as ReturnType<\n LazyClient[\"getSubscribeFn\"]\n >,\n disconnect: () => {\n throw new Error(\"boom\");\n },\n } as LazyClient;\n\n const wrapped = wrapLazyClient(fake);\n const sessions = { dispose: vi.fn() };\n\n await expect(teardown(sessions, wrapped)).resolves.toBeUndefined();\n });\n\n test(\"awaits pending unsubs before calling disconnect\", async () => {\n // The whole point of the fix: disconnect must not run while\n // unsubscribe RPCs are still queued. Verify ordering even\n // when the unsubs take multiple microtasks to settle.\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n let unsubResolved = false;\n const innerDisconnect = wrapped.disconnect.bind(wrapped);\n wrapped.disconnect = () => {\n // Disconnect must not run before the unsubscribe has\n // resolved. If it does, this assertion fires.\n expect(unsubResolved).toBe(true);\n innerDisconnect();\n };\n\n const sessions = {\n dispose: () => {\n const subscribe = wrapped.getSubscribeFn();\n subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n )();\n // mark resolution after the microtasks the wrapper queues\n queueMicrotask(() =>\n queueMicrotask(() => {\n unsubResolved = true;\n }),\n );\n },\n };\n\n await teardown(sessions, wrapped);\n expect(fake.disconnectCalls).toBe(1);\n });\n });\n}\n\nexport { SS_STABLE_STAGE_ENDPOINTS, SS_PASEO_STABLE_STAGE_ENDPOINTS };\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Create a PolkadotSigner from a QR-paired session.\n *\n * Routes **transaction signing** through `session.createTransaction` — the\n * host-papp `CreateTransactionRequest`/`CreateTransactionResponse` SSO\n * message pair. We hand the mobile app the structured\n * `ProductAccountTransaction` (`signer`, `genesisHash`, `callData`, the\n * per-extension `{ extra, additionalSigned }`, and `txExtVersion`) and it\n * **builds and signs** the full extrinsic, returning the signed bytes.\n *\n * Two properties fall out of this:\n * - The wallet can **decode and display** the transaction instead of\n * blind-signing an opaque hash (which is what the older\n * `session.signRaw({ tag: \"Payload\" })` route did).\n * - Every signed extension the chain declares — including ones PAPI's PJS\n * adapter doesn't know (e.g. `AsPgas` on Paseo Next v2) — is forwarded\n * verbatim as `{ id, extra, additionalSigned }`, so nothing is lost in\n * translation.\n *\n * **Raw-message signing** (`signBytes`) keeps the `session.signRaw` `Bytes`\n * tag so the wallet applies the `<Bytes>...</Bytes>` anti-phishing wrap once\n * on its side. Correct for arbitrary user data; wrong for extrinsic payloads\n * (which is why the two paths split).\n *\n * @example\n * ```ts\n * const [session] = adapter.sessions.sessions.read();\n *\n * // Default account — uses [adapter.appId, 0]:\n * const signer = createSessionSigner(session, adapter);\n *\n * // Non-default derivation index, or a different productId:\n * const subSigner = createSessionSignerForAccount(session, {\n * productId: \"my-product\",\n * derivationIndex: 3,\n * });\n *\n * await contract.publish.tx(domain, cid, { signer, origin });\n * ```\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\nimport { decAnyMetadata, unifyMetadata } from \"@polkadot-api/substrate-bindings\";\nimport type { PolkadotSigner } from \"polkadot-api\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Identifies which sub-account of a paired session should sign.\n *\n * Mirrors the `host-papp` wire format `productAccountId: [productId, derivationIndex]`:\n * `productId` is the dotNS-style identifier for the requesting product (matches\n * the adapter's `appId` in normal usage); `derivationIndex` is the BIP32-style\n * child-key index, where `0` is the session's default account.\n */\nexport interface ProductAccountRef {\n /** The product identifier. Usually equal to the adapter's `appId`. */\n productId: string;\n /** Child-key derivation index. `0` is the default account. */\n derivationIndex: number;\n /**\n * The **product account's** sr25519 public key (32 bytes), as derived by the\n * host for `[productId, derivationIndex]`.\n *\n * PAPI stamps this into the extrinsic's signer address and verifies the\n * signature against it, so it must be the *product* account's key — not the\n * wallet's selected/root account (`session.remoteAccount.accountId`). A\n * mismatch produces an invalid signature.\n *\n * When omitted, the signer falls back to the session's selected account,\n * which is only correct when the product account *is* the selected account.\n */\n publicKey?: Uint8Array;\n}\n\n/**\n * The `signedExtensions` map PAPI hands to `PolkadotSigner.signTx`: keyed by\n * extension identifier, each entry carries the SCALE-encoded `extra` (goes in\n * the extrinsic body, as `value`) and `additionalSigned` (implicit data).\n */\ntype PapiSignedExtensions = Parameters<PolkadotSigner[\"signTx\"]>[1];\n\n/**\n * Build the host-papp `CreateTransactionRequest` from PAPI's `signTx` inputs.\n *\n * Maps every signed extension verbatim into the wire `{ id, extra,\n * additionalSigned }` triplets — no allow-list, no reshaping — so chain-specific\n * extensions (e.g. `AsPgas` on Paseo Next v2) survive. The mobile app uses\n * these plus `txExtVersion` to assemble and sign the extrinsic.\n *\n * Pure and synchronous so the wire-shape contract can be unit-tested without a\n * paired phone; the metadata decode + SSO round-trip live in {@link makeTxSignTx}.\n */\nfunction buildCreateTransactionRequest(\n productAccountId: [string, number],\n callData: Uint8Array,\n signedExtensions: PapiSignedExtensions,\n txExtVersion: number,\n): Parameters<UserSession[\"createTransaction\"]>[0] {\n const checkGenesis = signedExtensions.CheckGenesis;\n if (!checkGenesis) {\n throw new Error(\n \"Cannot build transaction: chain did not provide the CheckGenesis signed extension\",\n );\n }\n\n return {\n payload: {\n tag: \"v1\" as const,\n value: {\n signer: productAccountId,\n // CheckGenesis carries the genesis hash as its additionalSigned (implicit) data.\n genesisHash: checkGenesis.additionalSigned,\n callData,\n extensions: Object.values(signedExtensions).map(\n ({ identifier, value, additionalSigned }) => ({\n id: identifier,\n extra: value,\n additionalSigned,\n }),\n ),\n txExtVersion,\n },\n },\n };\n}\n\n/**\n * Transaction-extension version the mobile app must assemble:\n * - Extrinsic V4 → `0` (the protocol mandates 0 for V4).\n * - Extrinsic V5 → the runtime's version (highest the metadata advertises).\n */\nfunction txExtVersionFromMetadata(metadata: Uint8Array): number {\n const decoded = unifyMetadata(decAnyMetadata(metadata));\n const latestVersion = decoded.extrinsic.version.reduce((max, v) => Math.max(max, v), 0);\n return latestVersion === 4 ? 0 : latestVersion;\n}\n\n/**\n * Send the request to the paired wallet's `createTransaction` and unwrap it.\n *\n * Extracted and named so the SSO round-trip + error handling can be\n * unit-tested without a real phone or chain metadata; the metadata decode and\n * payload assembly live in {@link makeTxSignTx} / {@link buildCreateTransactionRequest}.\n */\nasync function requestSignedTransaction(\n session: UserSession,\n request: Parameters<UserSession[\"createTransaction\"]>[0],\n): Promise<Uint8Array> {\n const result = await session.createTransaction(request);\n if (result.isErr()) {\n throw new Error(`Mobile transaction signing rejected: ${result.error.message}`);\n }\n // host-papp returns the fully signed extrinsic bytes.\n return result.value;\n}\n\n/**\n * The `signTx` function PAPI calls for transaction signing.\n *\n * Forwards the structured payload to the paired mobile wallet via\n * `session.createTransaction`; the wallet builds + signs the extrinsic and\n * returns the signed bytes. Unlike the old `signRaw({ tag: \"Payload\" })`\n * route, the wallet sees a real transaction (not opaque bytes) and unknown\n * signed extensions are preserved by {@link buildCreateTransactionRequest}.\n */\nfunction makeTxSignTx(\n session: UserSession,\n productAccountId: [string, number],\n): PolkadotSigner[\"signTx\"] {\n return async (callData, signedExtensions, metadata) =>\n requestSignedTransaction(\n session,\n buildCreateTransactionRequest(\n productAccountId,\n callData,\n signedExtensions,\n txExtVersionFromMetadata(metadata),\n ),\n );\n}\n\n/**\n * Sign function for arbitrary-byte signing (`PolkadotSigner.signBytes`).\n *\n * Routes to `session.signRaw` under the `Bytes` tag — the wallet's\n * raw-bytes interactor, which applies the standard `<Bytes>...</Bytes>`\n * anti-phishing envelope before signing.\n *\n * Provided as a separate function so `buildSessionSigner` can override\n * the `signBytes` produced by `getPolkadotSigner` (which would otherwise\n * funnel raw-bytes signing through the same `sign` callback as tx signing\n * — the wrong wire tag for arbitrary user data).\n */\nfunction makeRawBytesSignCallback(session: UserSession, productAccountId: [string, number]) {\n return async (data: Uint8Array): Promise<Uint8Array> => {\n const result = await session.signRaw({\n productAccountId,\n data: { tag: \"Bytes\", value: data },\n });\n\n if (result.isErr()) {\n throw new Error(`Mobile signing rejected: ${result.error.message}`);\n }\n\n return result.value.signature;\n };\n}\n\nfunction buildSessionSigner(session: UserSession, ref: ProductAccountRef): PolkadotSigner {\n const productAccountId: [string, number] = [ref.productId, ref.derivationIndex];\n\n // The signer's public key must be the *product* account's key — the one the\n // wallet signs with for [productId, derivationIndex] — not the wallet's\n // selected root account. PAPI stamps this into the extrinsic's address and\n // verifies against it, so a mismatch yields an invalid signature. The host\n // supplies the derived key via `ref.publicKey`; we fall back to the\n // session's selected account only when none is provided (i.e. product\n // account == selected account). See {@link ProductAccountRef.publicKey}.\n const publicKey = ref.publicKey ?? new Uint8Array(session.remoteAccount.accountId);\n\n return {\n publicKey,\n // Transaction signing → host-papp `createTransaction`: the wallet builds\n // and signs the extrinsic, so it can show a decoded tx (no `<Bytes>`\n // envelope) and unknown signed extensions (e.g. `AsPgas`) survive.\n signTx: makeTxSignTx(session, productAccountId),\n // Arbitrary-byte signing keeps the `Bytes` tag so the wallet applies the\n // `<Bytes>...</Bytes>` anti-phishing envelope exactly once on its side\n // (correct for non-extrinsic user data).\n signBytes: makeRawBytesSignCallback(session, productAccountId),\n };\n}\n\n/**\n * Create a `PolkadotSigner` backed by a QR-paired mobile wallet session,\n * using the session's **default account** (`derivationIndex: 0`).\n *\n * For non-default sub-accounts, use {@link createSessionSignerForAccount}.\n *\n * @param session The paired user session.\n * @param adapter The {@link TerminalAdapter} that loaded the session. Its `appId`\n * is used as the `productId` in the wire request.\n * @param publicKey The product account's sr25519 public key for\n * `[adapter.appId, 0]`, as derived by the host. See\n * {@link ProductAccountRef.publicKey}; omit only when the product account is\n * the session's selected account.\n */\nexport function createSessionSigner(\n session: UserSession,\n adapter: TerminalAdapter,\n publicKey?: Uint8Array,\n): PolkadotSigner {\n return buildSessionSigner(session, { productId: adapter.appId, derivationIndex: 0, publicKey });\n}\n\n/**\n * Create a `PolkadotSigner` for a specific sub-account of a paired session.\n *\n * Use this when you need a derivation index other than `0`, or a `productId`\n * different from the adapter's `appId`. For the common default-account case,\n * prefer {@link createSessionSigner}.\n *\n * @param session The paired user session.\n * @param ref The product account to sign as: `{ productId, derivationIndex }`.\n */\nexport function createSessionSignerForAccount(\n session: UserSession,\n ref: ProductAccountRef,\n): PolkadotSigner {\n return buildSessionSigner(session, ref);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n const { ok, err } = await import(\"neverthrow\");\n\n /**\n * Build a minimal `UserSession`-shaped stub. `signPayload`, `signRaw`, and\n * `createTransaction` accept request-capturing functions so tests can assert\n * on exactly which host-papp method got called and with what payload.\n */\n function makeSession(opts: {\n signPayload?: (req: unknown) => Promise<unknown>;\n signRaw?: (req: unknown) => Promise<unknown>;\n createTransaction?: (req: unknown) => Promise<unknown>;\n accountIdBytes?: number[];\n }): UserSession {\n const accountIdBytes = opts.accountIdBytes ?? new Array(32).fill(0).map((_, i) => i);\n return {\n remoteAccount: { accountId: accountIdBytes },\n signPayload: vi.fn(\n opts.signPayload ??\n (async () => {\n throw new Error(\"signPayload not stubbed in this test\");\n }),\n ),\n signRaw: vi.fn(\n opts.signRaw ??\n (async () => {\n throw new Error(\"signRaw not stubbed in this test\");\n }),\n ),\n createTransaction: vi.fn(\n opts.createTransaction ??\n (async () => {\n throw new Error(\"createTransaction not stubbed in this test\");\n }),\n ),\n } as unknown as UserSession;\n }\n\n /**\n * A minimal PAPI `signedExtensions` entry. `value` is the SCALE-encoded\n * \"extra\" (extrinsic body); `additionalSigned` is the implicit data.\n */\n function ext(identifier: string, value: number[], additionalSigned: number[]) {\n return {\n identifier,\n value: new Uint8Array(value),\n additionalSigned: new Uint8Array(additionalSigned),\n };\n }\n\n function fakeAdapter(appId: string): TerminalAdapter {\n // Only the `appId` field matters for these tests.\n return { appId } as unknown as TerminalAdapter;\n }\n\n describe(\"createSessionSigner\", () => {\n test(\"falls back to remoteAccount.accountId when no product key is passed\", () => {\n const bytes = Array.from({ length: 32 }, (_, i) => i);\n const signer = createSessionSigner(\n makeSession({ accountIdBytes: bytes }),\n fakeAdapter(\"test-app\"),\n );\n expect(signer.publicKey).toEqual(new Uint8Array(bytes));\n });\n\n test(\"uses the host-supplied product-account public key when provided\", () => {\n // The product account's key differs from the wallet's selected\n // account (`remoteAccount.accountId`). PAPI must stamp the *product*\n // key into the extrinsic, so the explicit key must win.\n const walletBytes = Array.from({ length: 32 }, (_, i) => i);\n const productKey = new Uint8Array(32).fill(0xab);\n const signer = createSessionSigner(\n makeSession({ accountIdBytes: walletBytes }),\n fakeAdapter(\"test-app\"),\n productKey,\n );\n expect(signer.publicKey).toEqual(productKey);\n expect(signer.publicKey).not.toEqual(new Uint8Array(walletBytes));\n });\n\n test(\"signBytes routes through session.signRaw with the Bytes tag\", async () => {\n const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: sig });\n },\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n const out = await signer.signBytes(new Uint8Array([1, 2, 3]));\n expect(out).toEqual(sig);\n\n // The bytes route forwards data verbatim under the `Bytes` tag.\n // Mobile applies the <Bytes>...</Bytes> envelope on its side.\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"test-app\", 0]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toEqual(new Uint8Array([1, 2, 3]));\n });\n\n test(\"signBytes never calls session.signPayload (legacy-path regression guard)\", async () => {\n // The PJS-bridged path used to route tx signing through\n // `session.signPayload`. The new PAPI-native path retires\n // `signPayload` entirely for this signer. This test guards\n // against accidentally re-introducing the legacy call.\n const session = makeSession({\n signRaw: async () => ok({ signature: new Uint8Array([1]) }),\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await signer.signBytes(new Uint8Array([1, 2, 3]));\n\n const sessionWithSpies = session as unknown as {\n signPayload: { mock: { calls: unknown[] } };\n signRaw: { mock: { calls: unknown[] } };\n };\n expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);\n expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);\n });\n\n test(\"signBytes throws with a clear error when the mobile rejects\", async () => {\n const session = makeSession({\n signRaw: async () => err({ message: \"user declined\" }),\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(\n \"Mobile signing rejected: user declined\",\n );\n });\n });\n\n describe(\"buildCreateTransactionRequest — tx payload (the AsPgas fix)\", () => {\n // Asserts the wire shape handed to the wallet. The full PAPI signTx →\n // metadata decode → SSO round-trip is exercised by the manual smoke\n // test in `manual-tests/qr-pair-and-sign.mjs` since CI cannot drive\n // a real phone.\n\n const checkGenesis = ext(\"CheckGenesis\", [], [0x11, 0x22, 0x33]);\n\n test(\"wraps the payload as v1 with signer, callData, and txExtVersion\", () => {\n const callData = new Uint8Array([0xca, 0x11]);\n const req = buildCreateTransactionRequest(\n [\"my-app\", 3],\n callData,\n { CheckGenesis: checkGenesis },\n 5,\n );\n expect(req.payload.tag).toBe(\"v1\");\n expect(req.payload.value.signer).toEqual([\"my-app\", 3]);\n expect(req.payload.value.callData).toEqual(callData);\n expect(req.payload.value.txExtVersion).toBe(5);\n });\n\n test(\"takes the genesis hash from CheckGenesis.additionalSigned\", () => {\n const req = buildCreateTransactionRequest(\n [\"my-app\", 0],\n new Uint8Array([0]),\n { CheckGenesis: checkGenesis },\n 0,\n );\n expect(req.payload.value.genesisHash).toEqual(new Uint8Array([0x11, 0x22, 0x33]));\n });\n\n test(\"maps every signed extension to { id, extra, additionalSigned }\", () => {\n const req = buildCreateTransactionRequest(\n [\"my-app\", 0],\n new Uint8Array([0]),\n { CheckGenesis: checkGenesis, CheckNonce: ext(\"CheckNonce\", [0x07], []) },\n 0,\n );\n expect(req.payload.value.extensions).toEqual([\n {\n id: \"CheckGenesis\",\n extra: new Uint8Array([]),\n additionalSigned: new Uint8Array([0x11, 0x22, 0x33]),\n },\n {\n id: \"CheckNonce\",\n extra: new Uint8Array([0x07]),\n additionalSigned: new Uint8Array([]),\n },\n ]);\n });\n\n test(\"preserves an unknown signed extension verbatim (e.g. AsPgas)\", () => {\n // The whole reason for moving off PJS: an extension PAPI's PJS\n // adapter doesn't know must pass through untouched.\n const req = buildCreateTransactionRequest(\n [\"my-app\", 0],\n new Uint8Array([0]),\n { CheckGenesis: checkGenesis, AsPgas: ext(\"AsPgas\", [0xde, 0xad], [0xbe, 0xef]) },\n 0,\n );\n expect(req.payload.value.extensions).toContainEqual({\n id: \"AsPgas\",\n extra: new Uint8Array([0xde, 0xad]),\n additionalSigned: new Uint8Array([0xbe, 0xef]),\n });\n });\n\n test(\"throws a clear error when CheckGenesis is absent\", () => {\n expect(() =>\n buildCreateTransactionRequest([\"my-app\", 0], new Uint8Array([0]), {}, 0),\n ).toThrow(/CheckGenesis/);\n });\n });\n\n describe(\"requestSignedTransaction — SSO round-trip\", () => {\n const request = buildCreateTransactionRequest(\n [\"my-app\", 0],\n new Uint8Array([0]),\n { CheckGenesis: ext(\"CheckGenesis\", [], [0x01]) },\n 0,\n );\n\n test(\"returns the signed extrinsic bytes from the mobile response\", async () => {\n const signed = new Uint8Array([0xab, 0xcd, 0xef]);\n const session = makeSession({ createTransaction: async () => ok(signed) });\n\n const out = await requestSignedTransaction(session, request);\n expect(out).toBe(signed);\n });\n\n test(\"calls session.createTransaction — never signRaw/signPayload — with the request\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n createTransaction: async (req) => {\n captured.push(req);\n return ok(new Uint8Array([1]));\n },\n });\n\n await requestSignedTransaction(session, request);\n\n expect(captured).toEqual([request]);\n const spies = session as unknown as {\n signPayload: { mock: { calls: unknown[] } };\n signRaw: { mock: { calls: unknown[] } };\n createTransaction: { mock: { calls: unknown[] } };\n };\n expect(spies.createTransaction.mock.calls).toHaveLength(1);\n expect(spies.signPayload.mock.calls).toHaveLength(0);\n expect(spies.signRaw.mock.calls).toHaveLength(0);\n });\n\n test(\"throws a clear error when the mobile rejects\", async () => {\n const session = makeSession({\n createTransaction: async () => err({ message: \"user declined\" }),\n });\n await expect(requestSignedTransaction(session, request)).rejects.toThrow(\n \"Mobile transaction signing rejected: user declined\",\n );\n });\n });\n\n describe(\"makeRawBytesSignCallback\", () => {\n test(\"forwards data verbatim under the Bytes tag with the right productAccountId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0xff]) });\n },\n });\n\n const callback = makeRawBytesSignCallback(session, [\"my-app\", 5]);\n await callback(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 5]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(Array.from(req.data.value)).toEqual([0xde, 0xad, 0xbe, 0xef]);\n });\n\n test(\"returns the raw signature bytes from the mobile response\", async () => {\n const sig = new Uint8Array([0x42]);\n const session = makeSession({\n signRaw: async () => ok({ signature: sig }),\n });\n\n const callback = makeRawBytesSignCallback(session, [\"my-app\", 0]);\n const out = await callback(new Uint8Array([0]));\n\n expect(out).toBe(sig);\n });\n });\n\n describe(\"createSessionSignerForAccount\", () => {\n test(\"forwards productAccountId from the explicit ref\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([42]) });\n },\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"my-app\",\n derivationIndex: 7,\n });\n await signer.signBytes(new Uint8Array([10, 20, 30]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 7]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toBeInstanceOf(Uint8Array);\n });\n\n test(\"supports a productId different from any adapter's appId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0]) });\n },\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"external-product\",\n derivationIndex: 0,\n });\n await signer.signBytes(new Uint8Array([1]));\n\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"external-product\", 0]);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Helpers for working with persisted sessions on a `TerminalAdapter`.\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Wait for the adapter to load at least one persisted session, or resolve\n * with an empty array after `timeoutMs`.\n *\n * The session manager loads sessions from storage asynchronously, so a\n * synchronous `adapter.sessions.sessions.read()` immediately after\n * `createTerminalAdapter()` may return `[]` even when sessions exist on\n * disk. Use this helper to give the loader a chance to populate before\n * deciding whether the user is logged in.\n */\nexport function waitForSessions(\n adapter: TerminalAdapter,\n timeoutMs = 3000,\n): Promise<UserSession[]> {\n return new Promise((resolve) => {\n let resolved = false;\n let unsub: (() => void) | null = null;\n\n const finish = (sessions: UserSession[]) => {\n if (resolved) return;\n resolved = true;\n // Defer unsub so the synchronous initial emission can complete\n // before we tear down the subscription.\n queueMicrotask(() => unsub?.());\n resolve(sessions);\n };\n\n const timer = setTimeout(() => finish([]), timeoutMs);\n\n unsub = adapter.sessions.sessions.subscribe((sessions: UserSession[]) => {\n if (sessions.length > 0) {\n clearTimeout(timer);\n finish(sessions);\n }\n });\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n type Subscriber = (sessions: UserSession[]) => void;\n\n function fakeAdapter(): {\n adapter: TerminalAdapter;\n emit: (sessions: UserSession[]) => void;\n subscriberCount: () => number;\n } {\n const subscribers = new Set<Subscriber>();\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n },\n },\n } as unknown as TerminalAdapter;\n return {\n adapter,\n emit: (sessions) => {\n for (const cb of subscribers) cb(sessions);\n },\n subscriberCount: () => subscribers.size,\n };\n }\n\n describe(\"waitForSessions\", () => {\n test(\"resolves with sessions on first non-empty emission\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"ignores empty emissions and waits for a non-empty one\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n emit([]);\n emit([]);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"resolves with [] after timeout when nothing is emitted\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n const promise = waitForSessions(adapter, 50);\n vi.advanceTimersByTime(50);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"unsubscribes after resolving\", async () => {\n const { adapter, emit, subscriberCount } = fakeAdapter();\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const promise = waitForSessions(adapter, 1000);\n emit([session]);\n await promise;\n // Wait for queueMicrotask\n await Promise.resolve();\n expect(subscriberCount()).toBe(0);\n });\n\n test(\"uses default 3000ms timeout when none is passed\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n // No timeoutMs argument — exercises the `?? 3000` default branch.\n const promise = waitForSessions(adapter);\n // Just short of 3000ms: still pending.\n vi.advanceTimersByTime(2999);\n let settled = false;\n promise.then(() => {\n settled = true;\n });\n await Promise.resolve();\n expect(settled).toBe(false);\n // Cross the default boundary.\n vi.advanceTimersByTime(1);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"handles synchronous initial emission\", async () => {\n // Some subscribables emit current value synchronously inside subscribe().\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n cb([session]);\n return () => {};\n },\n },\n },\n } as unknown as TerminalAdapter;\n await expect(waitForSessions(adapter, 1000)).resolves.toEqual([session]);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Terminal QR code rendering using Unicode half-block characters.\n *\n * Wraps the `qrcode` npm package's UTF-8 renderer, which packs two\n * module rows per terminal line using half-block characters.\n */\n\n/** Options for QR code rendering. */\nexport interface QrRenderOptions {\n /** Error correction level. Default: \"M\". */\n errorCorrectionLevel?: \"L\" | \"M\" | \"Q\" | \"H\";\n /** Quiet zone size in modules. Default: 2. */\n margin?: number;\n}\n\n/**\n * Encode a string as a QR code rendered in Unicode half-block characters.\n *\n * Returns a multi-line string suitable for `console.log`.\n */\nexport async function renderQrCode(data: string, options?: QrRenderOptions): Promise<string> {\n const QRCode = await import(\"qrcode\");\n const result = await QRCode.toString(data, {\n type: \"utf8\",\n errorCorrectionLevel: options?.errorCorrectionLevel ?? \"M\",\n margin: options?.margin ?? 2,\n });\n return result;\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"renderQrCode produces non-empty output\", async () => {\n const result = await renderQrCode(\"https://example.com\");\n expect(result.length).toBeGreaterThan(0);\n expect(result).toContain(\"\\n\");\n });\n\n test(\"renderQrCode contains Unicode block characters\", async () => {\n const result = await renderQrCode(\"test\");\n expect(/[▀▄█]/.test(result)).toBe(true);\n });\n\n test(\"different inputs produce different QR codes\", async () => {\n const a = await renderQrCode(\"hello\");\n const b = await renderQrCode(\"world\");\n expect(a).not.toBe(b);\n });\n}\n"],"mappings":"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","names":["fakeLazyClient","teardown","makeSession","ext","fakeAdapter","req","fakeAdapter"]}
|
package/dist/testing.d.ts
CHANGED
|
@@ -82,7 +82,7 @@ interface TestSession {
|
|
|
82
82
|
* const storageDir = mkdtempSync(join(tmpdir(), "e2e-"));
|
|
83
83
|
* const { sessionId } = await createTestSession({ appId: "dot-cli", storageDir });
|
|
84
84
|
*
|
|
85
|
-
* const adapter = createTerminalAdapter({ appId: "dot-cli",
|
|
85
|
+
* const adapter = createTerminalAdapter({ appId: "dot-cli", storageDir });
|
|
86
86
|
* const sessions = await waitForSessions(adapter);
|
|
87
87
|
* // sessions[0].id === sessionId
|
|
88
88
|
* ```
|
package/dist/testing.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sanitizeKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GHOFKVUG.js";
|
|
4
4
|
|
|
5
5
|
// src/testing.ts
|
|
6
6
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
7
7
|
import { p256 } from "@noble/curves/nist.js";
|
|
8
8
|
import { blake2b } from "@noble/hashes/blake2.js";
|
|
9
9
|
import {
|
|
10
|
+
AccountIdCodec,
|
|
10
11
|
LocalSessionAccountCodec,
|
|
11
12
|
RemoteSessionAccountCodec,
|
|
12
13
|
createAccountId,
|
|
@@ -24,11 +25,14 @@ import {
|
|
|
24
25
|
import { mkdir, writeFile } from "fs/promises";
|
|
25
26
|
import { join } from "path";
|
|
26
27
|
import { nanoid } from "nanoid";
|
|
27
|
-
import { Bytes, Struct, Vector, str } from "scale-ts";
|
|
28
|
+
import { Bytes, Option, Struct, Vector, str } from "scale-ts";
|
|
28
29
|
var storedUserSessionCodec = Struct({
|
|
29
30
|
id: str,
|
|
30
31
|
localAccount: LocalSessionAccountCodec,
|
|
31
|
-
remoteAccount: RemoteSessionAccountCodec
|
|
32
|
+
remoteAccount: RemoteSessionAccountCodec,
|
|
33
|
+
rootAccountId: AccountIdCodec,
|
|
34
|
+
identityAccountId: Option(AccountIdCodec),
|
|
35
|
+
identityChatPublicKey: Option(Bytes(65))
|
|
32
36
|
});
|
|
33
37
|
var sessionsCodec = Vector(storedUserSessionCodec);
|
|
34
38
|
var storedUserSecretsCodec = Struct({
|
|
@@ -69,7 +73,11 @@ async function createTestSession(options) {
|
|
|
69
73
|
createAccountId(remotePublicKey),
|
|
70
74
|
sharedSecret,
|
|
71
75
|
void 0
|
|
72
|
-
)
|
|
76
|
+
),
|
|
77
|
+
// synthesized session: the remote (wallet) account doubles as root.
|
|
78
|
+
rootAccountId: createAccountId(remotePublicKey),
|
|
79
|
+
identityAccountId: void 0,
|
|
80
|
+
identityChatPublicKey: void 0
|
|
73
81
|
};
|
|
74
82
|
await writeFile(
|
|
75
83
|
join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessions")}.json`),
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Test helpers for synthesizing persisted sessions.\n *\n * Production sessions are persisted to the storage directory by\n * `createNodeStorageAdapter` in the SCALE-encoded / AES-GCM-encrypted\n * format defined by `@novasamatech/host-papp`'s internal repositories.\n * There is no public API on the adapter to write a session without going\n * through the QR pairing + on-chain attestation flow, which makes E2E\n * testing of session-dependent CLI flows impossible without a real phone.\n *\n * This module mirrors that format using only public primitives, so E2E\n * tests can inject a known-good session file. The `testing.test.ts`\n * integration test reads the synthesized files back through the real\n * `ssoSessionRepository` + `userSecretRepository` from `@novasamatech/host-papp`\n * and fails if the upstream format ever drifts from our reproduction.\n */\nimport { gcm } from \"@noble/ciphers/aes.js\";\nimport { p256 } from \"@noble/curves/nist.js\";\nimport { blake2b } from \"@noble/hashes/blake2.js\";\nimport {\n AccountIdCodec,\n LocalSessionAccountCodec,\n RemoteSessionAccountCodec,\n createAccountId,\n createLocalSessionAccount,\n createRemoteSessionAccount,\n createSr25519Secret,\n deriveSr25519PublicKey,\n} from \"@novasamatech/statement-store\";\nimport { toHex } from \"@polkadot-api/utils\";\nimport {\n entropyToMiniSecret,\n generateMnemonic,\n mnemonicToEntropy,\n} from \"@polkadot-labs/hdkd-helpers\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { nanoid } from \"nanoid\";\nimport { Bytes, Struct, Vector, str } from \"scale-ts\";\n\nimport { sanitizeKey } from \"./node-storage.js\";\n\n// Mirrors the internal codec in @novasamatech/host-papp's userSessionRepository.\nconst storedUserSessionCodec = Struct({\n id: str,\n localAccount: LocalSessionAccountCodec,\n remoteAccount: RemoteSessionAccountCodec,\n});\nconst sessionsCodec = Vector(storedUserSessionCodec);\n\n// Mirrors the internal StoredUserSecretsCodec in host-papp's userSecretRepository.\nconst storedUserSecretsCodec = Struct({\n ssSecret: Bytes(),\n encrSecret: Bytes(),\n entropy: Bytes(),\n});\n\n// Mirrors host-papp's userSecretRepository AES-GCM wrapper: the encryption\n// key is blake2b(appId, 16) and the nonce is blake2b(\"nonce\", 32). The appId\n// doubles as the salt, so sessions written by this helper are only readable\n// by a TerminalAdapter configured with the same appId.\nfunction encryptSecrets(appId: string, plaintext: Uint8Array): string {\n const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });\n const nonce = blake2b(new TextEncoder().encode(\"nonce\"), { dkLen: 32 });\n return toHex(gcm(key, nonce).encrypt(plaintext));\n}\n\n/** Mirrors host-papp's createEncrSecret: pad the mini-secret to 48 bytes and feed it to P256 keygen. */\nfunction p256SecretFromEntropy(entropy: Uint8Array): Uint8Array {\n const seed = new Uint8Array(48);\n seed.set(entropyToMiniSecret(entropy));\n return p256.keygen(seed).secretKey;\n}\n\nexport interface CreateTestSessionOptions {\n /** Unique app identifier. Must match the one passed to `createTerminalAdapter`. */\n appId: string;\n /**\n * Directory where session files are written. Must match the one the\n * CLI reads from — pass the same value to `createTerminalAdapter`'s\n * `storageDir` option.\n */\n storageDir: string;\n /** BIP39 mnemonic for the local (host-side) account. Default: freshly generated. */\n localMnemonic?: string;\n /** Derivation path for the local account. Default: `\"//wallet//sso\"` (matches production). */\n localDerivation?: string;\n /** BIP39 mnemonic for the remote (phone) account. Default: freshly generated. */\n remoteMnemonic?: string;\n /** Derivation path for the remote account. Default: `\"\"`. */\n remoteDerivation?: string;\n /** Stable session id. Default: random nanoid(12). */\n sessionId?: string;\n /**\n * Whether to write the encrypted `UserSecrets_<id>` file. Default: `true`.\n * Set to `false` to exercise recovery paths where a session exists on disk\n * but its secrets are missing.\n */\n includeSecrets?: boolean;\n}\n\nexport interface TestSession {\n sessionId: string;\n /** Sr25519 public key of the local (host) account, 32 bytes. */\n localAccountId: Uint8Array;\n /** Sr25519 public key of the remote (phone) account, 32 bytes. */\n remoteAccountId: Uint8Array;\n localMnemonic: string;\n localDerivation: string;\n remoteMnemonic: string;\n remoteDerivation: string;\n}\n\n/**\n * Write a valid persisted session to `storageDir`, as if the user had\n * completed QR pairing + attestation. A `TerminalAdapter` created with the\n * same `appId` + `storageDir` will emit the synthesized session from\n * `adapter.sessions.sessions`.\n *\n * @remarks\n * **Dev utility — not a stable contract.** This helper hand-rolls SCALE codecs\n * that mirror an internal on-disk format used by `@novasamatech/host-papp`'s\n * session repositories. The format is not part of host-papp's documented public\n * API and may change on minor version bumps. The `testing.interop.test.ts`\n * round-trip test fails loudly when the format drifts. Use this only in test code.\n *\n * ## Limits\n *\n * - **Signing does not round-trip.** Sending a request via `session.signRaw`\n * still goes out over the statement store and expects a real phone to\n * respond. Use this helper for flows that test session discovery,\n * persistence, and logout — not end-to-end signing.\n * - **Signing attempts surface `NoAllowanceError`.** The synthesized local\n * account was never registered on the People chain, so the first write\n * to the statement store fails with `NoAllowanceError`. That's the same\n * error path the CLI sees when a previously valid session's on-chain\n * attestation has expired, so tests that assert \"CLI handles an expired\n * session\" *can* be written against a synthesized session even though\n * there's no `expiresAt` knob to turn.\n * - **No `expiresAt`.** The on-disk codec has no expiry field; validity is\n * tracked via on-chain attestation state. See above for how expiry-path\n * tests still work in practice.\n * - **Corrupted-session cases** don't need a helper — write garbage to\n * `<storageDir>/<appId>_SsoSessions.json` with `fs.writeFile` directly.\n * - **Repeated calls replace the session list.** Each call writes a fresh\n * single-entry `SsoSessions` file, so calling twice on the same\n * `storageDir`+`appId` leaves only the second session on disk. Use a\n * fresh `mkdtempSync` per test to keep cases isolated.\n *\n * @example\n * ```ts\n * import { mkdtempSync } from \"node:fs\";\n * import { tmpdir } from \"node:os\";\n * import { join } from \"node:path\";\n * import { createTestSession } from \"@parity/product-sdk-terminal/testing\";\n * import { createTerminalAdapter, waitForSessions } from \"@parity/product-sdk-terminal\";\n *\n * const storageDir = mkdtempSync(join(tmpdir(), \"e2e-\"));\n * const { sessionId } = await createTestSession({ appId: \"dot-cli\", storageDir });\n *\n * const adapter = createTerminalAdapter({ appId: \"dot-cli\", metadataUrl: \"…\", storageDir });\n * const sessions = await waitForSessions(adapter);\n * // sessions[0].id === sessionId\n * ```\n */\nexport async function createTestSession(options: CreateTestSessionOptions): Promise<TestSession> {\n await mkdir(options.storageDir, { recursive: true });\n\n const localMnemonic = options.localMnemonic ?? generateMnemonic();\n const localDerivation = options.localDerivation ?? \"//wallet//sso\";\n const localEntropy = mnemonicToEntropy(localMnemonic);\n const localSecret = createSr25519Secret(localEntropy, localDerivation);\n const localPublicKey = deriveSr25519PublicKey(localSecret);\n const localEncrSecret = p256SecretFromEntropy(localEntropy);\n\n const remoteMnemonic = options.remoteMnemonic ?? generateMnemonic();\n const remoteDerivation = options.remoteDerivation ?? \"\";\n const remoteEntropy = mnemonicToEntropy(remoteMnemonic);\n const remoteSecret = createSr25519Secret(remoteEntropy, remoteDerivation);\n const remotePublicKey = deriveSr25519PublicKey(remoteSecret);\n const remoteEncrPublicKey = p256.getPublicKey(p256SecretFromEntropy(remoteEntropy), false);\n\n // In production, `remoteAccount.publicKey` is the ECDH shared secret\n // between the host's P256 encryption key and the phone's P256 encryption\n // key. We compute the same thing from two mnemonic-derived P256 keys so\n // the synthesized session is cryptographically well-formed.\n const sharedSecret = p256.getSharedSecret(localEncrSecret, remoteEncrPublicKey).slice(1, 33);\n\n const sessionId = options.sessionId ?? nanoid(12);\n\n const session = {\n id: sessionId,\n localAccount: createLocalSessionAccount(createAccountId(localPublicKey), undefined),\n remoteAccount: createRemoteSessionAccount(\n createAccountId(remotePublicKey),\n sharedSecret,\n undefined,\n ),\n };\n\n await writeFile(\n join(options.storageDir, `${sanitizeKey(options.appId, \"SsoSessions\")}.json`),\n toHex(sessionsCodec.enc([session])),\n \"utf-8\",\n );\n\n const includeSecrets = options.includeSecrets ?? true;\n if (includeSecrets) {\n const encoded = storedUserSecretsCodec.enc({\n ssSecret: localSecret,\n encrSecret: localEncrSecret,\n entropy: localEntropy,\n });\n await writeFile(\n join(\n options.storageDir,\n `${sanitizeKey(options.appId, `UserSecrets_${sessionId}`)}.json`,\n ),\n encryptSecrets(options.appId, encoded),\n \"utf-8\",\n );\n }\n\n return {\n sessionId,\n localAccountId: localPublicKey,\n remoteAccountId: remotePublicKey,\n localMnemonic,\n localDerivation,\n remoteMnemonic,\n remoteDerivation,\n };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, beforeEach, afterAll } = import.meta.vitest;\n const { mkdtemp, readFile, rm } = await import(\"node:fs/promises\");\n const { tmpdir } = await import(\"node:os\");\n const { fromHex } = await import(\"@polkadot-api/utils\");\n\n // Stable dev mnemonics — deterministic inputs keep the tests hermetic.\n // The first is the well-known Substrate dev root (the source of `//Alice`\n // et al.); the second is the BIP39 all-abandon test vector.\n const LOCAL_MNEMONIC = \"bottom drive obey lake curtain smoke basket hold race lonely fit walk\";\n const REMOTE_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n let storageDir: string;\n\n beforeEach(async () => {\n storageDir = await mkdtemp(join(tmpdir(), \"terminal-testing-\"));\n });\n\n afterAll(async () => {\n try {\n await rm(storageDir, { recursive: true });\n } catch {\n /* ignore */\n }\n });\n\n describe(\"createTestSession\", () => {\n test(\"writes both SsoSessions and UserSecrets files by default\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const sessions = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n expect(sessions).toMatch(/^0x[0-9a-f]+$/);\n\n const secrets = await readFile(\n join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`),\n \"utf-8\",\n );\n expect(secrets).toMatch(/^0x[0-9a-f]+$/);\n });\n\n test(\"omits UserSecrets file when includeSecrets is false\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n includeSecrets: false,\n });\n\n await expect(\n readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`), \"utf-8\"),\n ).rejects.toThrow(/ENOENT/);\n });\n\n test(\"SsoSessions file decodes with the host-papp session codec shape\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n sessionId: \"stable-test-id\",\n });\n\n const hex = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n const decoded = sessionsCodec.dec(fromHex(hex));\n\n expect(decoded).toHaveLength(1);\n expect(decoded[0].id).toBe(\"stable-test-id\");\n expect(decoded[0].localAccount.accountId).toEqual(result.localAccountId);\n expect(decoded[0].localAccount.pin).toBeUndefined();\n expect(decoded[0].remoteAccount.accountId).toEqual(result.remoteAccountId);\n expect(decoded[0].remoteAccount.publicKey).toHaveLength(32);\n expect(decoded[0].remoteAccount.pin).toBeUndefined();\n });\n\n test(\"localAccountId matches the Sr25519 public key derived from the mnemonic\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const expected = deriveSr25519PublicKey(\n createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), \"//wallet//sso\"),\n );\n expect(result.localAccountId).toEqual(expected);\n expect(result.localAccountId).toHaveLength(32);\n });\n\n test(\"UserSecrets file decrypts and decodes with the host-papp secret codec shape\", async () => {\n const appId = \"my-app\";\n const result = await createTestSession({\n appId,\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const hex = await readFile(\n join(storageDir, `${appId}_UserSecrets_${result.sessionId}.json`),\n \"utf-8\",\n );\n const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });\n const nonce = blake2b(new TextEncoder().encode(\"nonce\"), { dkLen: 32 });\n const decrypted = gcm(key, nonce).decrypt(fromHex(hex));\n const decoded = storedUserSecretsCodec.dec(decrypted);\n\n expect(decoded.entropy).toEqual(mnemonicToEntropy(LOCAL_MNEMONIC));\n // Sr25519 secret is 64 bytes (32-byte secret + 32-byte nonce).\n expect(decoded.ssSecret).toHaveLength(64);\n // P256 secret is 32 bytes.\n expect(decoded.encrSecret).toHaveLength(32);\n });\n\n test(\"different appIds produce files under different prefixes\", async () => {\n await createTestSession({ appId: \"app-a\", storageDir, sessionId: \"id\" });\n await createTestSession({ appId: \"app-b\", storageDir, sessionId: \"id\" });\n\n await expect(\n readFile(join(storageDir, \"app-a_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n await expect(\n readFile(join(storageDir, \"app-b_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"sanitizes appIds with special characters\", async () => {\n await createTestSession({\n appId: \"app/with spaces\",\n storageDir,\n sessionId: \"id\",\n });\n await expect(\n readFile(join(storageDir, \"app_with_spaces_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"creates storageDir when it does not yet exist\", async () => {\n const nested = join(storageDir, \"does\", \"not\", \"exist\");\n await createTestSession({ appId: \"my-app\", storageDir: nested });\n await expect(\n readFile(join(nested, \"my-app_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"generates fresh mnemonics when none are supplied\", async () => {\n const a = await createTestSession({ appId: \"a\", storageDir });\n const b = await createTestSession({ appId: \"b\", storageDir });\n expect(a.localMnemonic).not.toBe(b.localMnemonic);\n expect(a.remoteMnemonic).not.toBe(b.remoteMnemonic);\n expect(a.sessionId).not.toBe(b.sessionId);\n });\n\n test(\"respects an explicit sessionId\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"pinned-id\",\n });\n expect(result.sessionId).toBe(\"pinned-id\");\n await expect(\n readFile(join(storageDir, \"my-app_UserSecrets_pinned-id.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"respects a custom localDerivation\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n localDerivation: \"//custom//path\",\n });\n expect(result.localDerivation).toBe(\"//custom//path\");\n const expected = deriveSr25519PublicKey(\n createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), \"//custom//path\"),\n );\n expect(result.localAccountId).toEqual(expected);\n });\n\n test(\"repeated calls replace the session list\", async () => {\n // Documented behavior — second call leaves only the second session\n // on disk. Callers who want multiple sessions should use separate\n // storage dirs.\n const first = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"first\",\n });\n const second = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"second\",\n });\n\n const hex = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n const decoded = sessionsCodec.dec(fromHex(hex));\n expect(decoded).toHaveLength(1);\n expect(decoded[0].id).toBe(\"second\");\n // The first session's UserSecrets file is left behind (not cleaned).\n // This matches a real logout flow as well, so we don't try to hide it.\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${first.sessionId}.json`), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${second.sessionId}.json`), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"AccountIdCodec is Bytes(32) — keep this invariant if upstream changes\", () => {\n // Guards against silent upstream changes to the session-account\n // codec shape that would make our synthesized files unreadable.\n const encoded = AccountIdCodec.enc(createAccountId(new Uint8Array(32).fill(7)));\n expect(encoded).toHaveLength(32);\n });\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Test helpers for synthesizing persisted sessions.\n *\n * Production sessions are persisted to the storage directory by\n * `createNodeStorageAdapter` in the SCALE-encoded / AES-GCM-encrypted\n * format defined by `@novasamatech/host-papp`'s internal repositories.\n * There is no public API on the adapter to write a session without going\n * through the QR pairing + on-chain attestation flow, which makes E2E\n * testing of session-dependent CLI flows impossible without a real phone.\n *\n * This module mirrors that format using only public primitives, so E2E\n * tests can inject a known-good session file. The `testing.test.ts`\n * integration test reads the synthesized files back through the real\n * `ssoSessionRepository` + `userSecretRepository` from `@novasamatech/host-papp`\n * and fails if the upstream format ever drifts from our reproduction.\n */\nimport { gcm } from \"@noble/ciphers/aes.js\";\nimport { p256 } from \"@noble/curves/nist.js\";\nimport { blake2b } from \"@noble/hashes/blake2.js\";\nimport {\n AccountIdCodec,\n LocalSessionAccountCodec,\n RemoteSessionAccountCodec,\n createAccountId,\n createLocalSessionAccount,\n createRemoteSessionAccount,\n createSr25519Secret,\n deriveSr25519PublicKey,\n} from \"@novasamatech/statement-store\";\nimport { toHex } from \"@polkadot-api/utils\";\nimport {\n entropyToMiniSecret,\n generateMnemonic,\n mnemonicToEntropy,\n} from \"@polkadot-labs/hdkd-helpers\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { nanoid } from \"nanoid\";\nimport { Bytes, type Codec, Option, Struct, Vector, str } from \"scale-ts\";\nimport type { StoredUserSession } from \"@novasamatech/host-papp\";\n\nimport { sanitizeKey } from \"./node-storage.js\";\n\n// Mirror of host-papp's internal stored-session codec (not exported — only the\n// `StoredUserSession` type is). `satisfies` guards drift at build time, the\n// interop test at runtime; field order and the `Bytes(65)` chat key must match.\nconst storedUserSessionCodec = Struct({\n id: str,\n localAccount: LocalSessionAccountCodec,\n remoteAccount: RemoteSessionAccountCodec,\n rootAccountId: AccountIdCodec,\n identityAccountId: Option(AccountIdCodec),\n identityChatPublicKey: Option(Bytes(65)),\n}) satisfies Codec<StoredUserSession>;\nconst sessionsCodec = Vector(storedUserSessionCodec);\n\n// Mirrors the internal StoredUserSecretsCodec in host-papp's userSecretRepository.\nconst storedUserSecretsCodec = Struct({\n ssSecret: Bytes(),\n encrSecret: Bytes(),\n entropy: Bytes(),\n});\n\n// Mirrors host-papp's userSecretRepository AES-GCM wrapper: the encryption\n// key is blake2b(appId, 16) and the nonce is blake2b(\"nonce\", 32). The appId\n// doubles as the salt, so sessions written by this helper are only readable\n// by a TerminalAdapter configured with the same appId.\nfunction encryptSecrets(appId: string, plaintext: Uint8Array): string {\n const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });\n const nonce = blake2b(new TextEncoder().encode(\"nonce\"), { dkLen: 32 });\n return toHex(gcm(key, nonce).encrypt(plaintext));\n}\n\n/** Mirrors host-papp's createEncrSecret: pad the mini-secret to 48 bytes and feed it to P256 keygen. */\nfunction p256SecretFromEntropy(entropy: Uint8Array): Uint8Array {\n const seed = new Uint8Array(48);\n seed.set(entropyToMiniSecret(entropy));\n return p256.keygen(seed).secretKey;\n}\n\nexport interface CreateTestSessionOptions {\n /** Unique app identifier. Must match the one passed to `createTerminalAdapter`. */\n appId: string;\n /**\n * Directory where session files are written. Must match the one the\n * CLI reads from — pass the same value to `createTerminalAdapter`'s\n * `storageDir` option.\n */\n storageDir: string;\n /** BIP39 mnemonic for the local (host-side) account. Default: freshly generated. */\n localMnemonic?: string;\n /** Derivation path for the local account. Default: `\"//wallet//sso\"` (matches production). */\n localDerivation?: string;\n /** BIP39 mnemonic for the remote (phone) account. Default: freshly generated. */\n remoteMnemonic?: string;\n /** Derivation path for the remote account. Default: `\"\"`. */\n remoteDerivation?: string;\n /** Stable session id. Default: random nanoid(12). */\n sessionId?: string;\n /**\n * Whether to write the encrypted `UserSecrets_<id>` file. Default: `true`.\n * Set to `false` to exercise recovery paths where a session exists on disk\n * but its secrets are missing.\n */\n includeSecrets?: boolean;\n}\n\nexport interface TestSession {\n sessionId: string;\n /** Sr25519 public key of the local (host) account, 32 bytes. */\n localAccountId: Uint8Array;\n /** Sr25519 public key of the remote (phone) account, 32 bytes. */\n remoteAccountId: Uint8Array;\n localMnemonic: string;\n localDerivation: string;\n remoteMnemonic: string;\n remoteDerivation: string;\n}\n\n/**\n * Write a valid persisted session to `storageDir`, as if the user had\n * completed QR pairing + attestation. A `TerminalAdapter` created with the\n * same `appId` + `storageDir` will emit the synthesized session from\n * `adapter.sessions.sessions`.\n *\n * @remarks\n * **Dev utility — not a stable contract.** This helper hand-rolls SCALE codecs\n * that mirror an internal on-disk format used by `@novasamatech/host-papp`'s\n * session repositories. The format is not part of host-papp's documented public\n * API and may change on minor version bumps. The `testing.interop.test.ts`\n * round-trip test fails loudly when the format drifts. Use this only in test code.\n *\n * ## Limits\n *\n * - **Signing does not round-trip.** Sending a request via `session.signRaw`\n * still goes out over the statement store and expects a real phone to\n * respond. Use this helper for flows that test session discovery,\n * persistence, and logout — not end-to-end signing.\n * - **Signing attempts surface `NoAllowanceError`.** The synthesized local\n * account was never registered on the People chain, so the first write\n * to the statement store fails with `NoAllowanceError`. That's the same\n * error path the CLI sees when a previously valid session's on-chain\n * attestation has expired, so tests that assert \"CLI handles an expired\n * session\" *can* be written against a synthesized session even though\n * there's no `expiresAt` knob to turn.\n * - **No `expiresAt`.** The on-disk codec has no expiry field; validity is\n * tracked via on-chain attestation state. See above for how expiry-path\n * tests still work in practice.\n * - **Corrupted-session cases** don't need a helper — write garbage to\n * `<storageDir>/<appId>_SsoSessions.json` with `fs.writeFile` directly.\n * - **Repeated calls replace the session list.** Each call writes a fresh\n * single-entry `SsoSessions` file, so calling twice on the same\n * `storageDir`+`appId` leaves only the second session on disk. Use a\n * fresh `mkdtempSync` per test to keep cases isolated.\n *\n * @example\n * ```ts\n * import { mkdtempSync } from \"node:fs\";\n * import { tmpdir } from \"node:os\";\n * import { join } from \"node:path\";\n * import { createTestSession } from \"@parity/product-sdk-terminal/testing\";\n * import { createTerminalAdapter, waitForSessions } from \"@parity/product-sdk-terminal\";\n *\n * const storageDir = mkdtempSync(join(tmpdir(), \"e2e-\"));\n * const { sessionId } = await createTestSession({ appId: \"dot-cli\", storageDir });\n *\n * const adapter = createTerminalAdapter({ appId: \"dot-cli\", storageDir });\n * const sessions = await waitForSessions(adapter);\n * // sessions[0].id === sessionId\n * ```\n */\nexport async function createTestSession(options: CreateTestSessionOptions): Promise<TestSession> {\n await mkdir(options.storageDir, { recursive: true });\n\n const localMnemonic = options.localMnemonic ?? generateMnemonic();\n const localDerivation = options.localDerivation ?? \"//wallet//sso\";\n const localEntropy = mnemonicToEntropy(localMnemonic);\n const localSecret = createSr25519Secret(localEntropy, localDerivation);\n const localPublicKey = deriveSr25519PublicKey(localSecret);\n const localEncrSecret = p256SecretFromEntropy(localEntropy);\n\n const remoteMnemonic = options.remoteMnemonic ?? generateMnemonic();\n const remoteDerivation = options.remoteDerivation ?? \"\";\n const remoteEntropy = mnemonicToEntropy(remoteMnemonic);\n const remoteSecret = createSr25519Secret(remoteEntropy, remoteDerivation);\n const remotePublicKey = deriveSr25519PublicKey(remoteSecret);\n const remoteEncrPublicKey = p256.getPublicKey(p256SecretFromEntropy(remoteEntropy), false);\n\n // In production, `remoteAccount.publicKey` is the ECDH shared secret\n // between the host's P256 encryption key and the phone's P256 encryption\n // key. We compute the same thing from two mnemonic-derived P256 keys so\n // the synthesized session is cryptographically well-formed.\n const sharedSecret = p256.getSharedSecret(localEncrSecret, remoteEncrPublicKey).slice(1, 33);\n\n const sessionId = options.sessionId ?? nanoid(12);\n\n const session = {\n id: sessionId,\n localAccount: createLocalSessionAccount(createAccountId(localPublicKey), undefined),\n remoteAccount: createRemoteSessionAccount(\n createAccountId(remotePublicKey),\n sharedSecret,\n undefined,\n ),\n // synthesized session: the remote (wallet) account doubles as root.\n rootAccountId: createAccountId(remotePublicKey),\n identityAccountId: undefined,\n identityChatPublicKey: undefined,\n };\n\n await writeFile(\n join(options.storageDir, `${sanitizeKey(options.appId, \"SsoSessions\")}.json`),\n toHex(sessionsCodec.enc([session])),\n \"utf-8\",\n );\n\n const includeSecrets = options.includeSecrets ?? true;\n if (includeSecrets) {\n const encoded = storedUserSecretsCodec.enc({\n ssSecret: localSecret,\n encrSecret: localEncrSecret,\n entropy: localEntropy,\n });\n await writeFile(\n join(\n options.storageDir,\n `${sanitizeKey(options.appId, `UserSecrets_${sessionId}`)}.json`,\n ),\n encryptSecrets(options.appId, encoded),\n \"utf-8\",\n );\n }\n\n return {\n sessionId,\n localAccountId: localPublicKey,\n remoteAccountId: remotePublicKey,\n localMnemonic,\n localDerivation,\n remoteMnemonic,\n remoteDerivation,\n };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, beforeEach, afterAll } = import.meta.vitest;\n const { mkdtemp, readFile, rm } = await import(\"node:fs/promises\");\n const { tmpdir } = await import(\"node:os\");\n const { fromHex } = await import(\"@polkadot-api/utils\");\n\n // Stable dev mnemonics — deterministic inputs keep the tests hermetic.\n // The first is the well-known Substrate dev root (the source of `//Alice`\n // et al.); the second is the BIP39 all-abandon test vector.\n const LOCAL_MNEMONIC = \"bottom drive obey lake curtain smoke basket hold race lonely fit walk\";\n const REMOTE_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n let storageDir: string;\n\n beforeEach(async () => {\n storageDir = await mkdtemp(join(tmpdir(), \"terminal-testing-\"));\n });\n\n afterAll(async () => {\n try {\n await rm(storageDir, { recursive: true });\n } catch {\n /* ignore */\n }\n });\n\n describe(\"createTestSession\", () => {\n test(\"writes both SsoSessions and UserSecrets files by default\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const sessions = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n expect(sessions).toMatch(/^0x[0-9a-f]+$/);\n\n const secrets = await readFile(\n join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`),\n \"utf-8\",\n );\n expect(secrets).toMatch(/^0x[0-9a-f]+$/);\n });\n\n test(\"omits UserSecrets file when includeSecrets is false\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n includeSecrets: false,\n });\n\n await expect(\n readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`), \"utf-8\"),\n ).rejects.toThrow(/ENOENT/);\n });\n\n test(\"SsoSessions file decodes with the host-papp session codec shape\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n sessionId: \"stable-test-id\",\n });\n\n const hex = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n const decoded = sessionsCodec.dec(fromHex(hex));\n\n expect(decoded).toHaveLength(1);\n expect(decoded[0].id).toBe(\"stable-test-id\");\n expect(decoded[0].localAccount.accountId).toEqual(result.localAccountId);\n expect(decoded[0].localAccount.pin).toBeUndefined();\n expect(decoded[0].remoteAccount.accountId).toEqual(result.remoteAccountId);\n expect(decoded[0].remoteAccount.publicKey).toHaveLength(32);\n expect(decoded[0].remoteAccount.pin).toBeUndefined();\n });\n\n test(\"localAccountId matches the Sr25519 public key derived from the mnemonic\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const expected = deriveSr25519PublicKey(\n createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), \"//wallet//sso\"),\n );\n expect(result.localAccountId).toEqual(expected);\n expect(result.localAccountId).toHaveLength(32);\n });\n\n test(\"UserSecrets file decrypts and decodes with the host-papp secret codec shape\", async () => {\n const appId = \"my-app\";\n const result = await createTestSession({\n appId,\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n });\n\n const hex = await readFile(\n join(storageDir, `${appId}_UserSecrets_${result.sessionId}.json`),\n \"utf-8\",\n );\n const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });\n const nonce = blake2b(new TextEncoder().encode(\"nonce\"), { dkLen: 32 });\n const decrypted = gcm(key, nonce).decrypt(fromHex(hex));\n const decoded = storedUserSecretsCodec.dec(decrypted);\n\n expect(decoded.entropy).toEqual(mnemonicToEntropy(LOCAL_MNEMONIC));\n // Sr25519 secret is 64 bytes (32-byte secret + 32-byte nonce).\n expect(decoded.ssSecret).toHaveLength(64);\n // P256 secret is 32 bytes.\n expect(decoded.encrSecret).toHaveLength(32);\n });\n\n test(\"different appIds produce files under different prefixes\", async () => {\n await createTestSession({ appId: \"app-a\", storageDir, sessionId: \"id\" });\n await createTestSession({ appId: \"app-b\", storageDir, sessionId: \"id\" });\n\n await expect(\n readFile(join(storageDir, \"app-a_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n await expect(\n readFile(join(storageDir, \"app-b_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"sanitizes appIds with special characters\", async () => {\n await createTestSession({\n appId: \"app/with spaces\",\n storageDir,\n sessionId: \"id\",\n });\n await expect(\n readFile(join(storageDir, \"app_with_spaces_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"creates storageDir when it does not yet exist\", async () => {\n const nested = join(storageDir, \"does\", \"not\", \"exist\");\n await createTestSession({ appId: \"my-app\", storageDir: nested });\n await expect(\n readFile(join(nested, \"my-app_SsoSessions.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"generates fresh mnemonics when none are supplied\", async () => {\n const a = await createTestSession({ appId: \"a\", storageDir });\n const b = await createTestSession({ appId: \"b\", storageDir });\n expect(a.localMnemonic).not.toBe(b.localMnemonic);\n expect(a.remoteMnemonic).not.toBe(b.remoteMnemonic);\n expect(a.sessionId).not.toBe(b.sessionId);\n });\n\n test(\"respects an explicit sessionId\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"pinned-id\",\n });\n expect(result.sessionId).toBe(\"pinned-id\");\n await expect(\n readFile(join(storageDir, \"my-app_UserSecrets_pinned-id.json\"), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"respects a custom localDerivation\", async () => {\n const result = await createTestSession({\n appId: \"my-app\",\n storageDir,\n localMnemonic: LOCAL_MNEMONIC,\n remoteMnemonic: REMOTE_MNEMONIC,\n localDerivation: \"//custom//path\",\n });\n expect(result.localDerivation).toBe(\"//custom//path\");\n const expected = deriveSr25519PublicKey(\n createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), \"//custom//path\"),\n );\n expect(result.localAccountId).toEqual(expected);\n });\n\n test(\"repeated calls replace the session list\", async () => {\n // Documented behavior — second call leaves only the second session\n // on disk. Callers who want multiple sessions should use separate\n // storage dirs.\n const first = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"first\",\n });\n const second = await createTestSession({\n appId: \"my-app\",\n storageDir,\n sessionId: \"second\",\n });\n\n const hex = await readFile(join(storageDir, \"my-app_SsoSessions.json\"), \"utf-8\");\n const decoded = sessionsCodec.dec(fromHex(hex));\n expect(decoded).toHaveLength(1);\n expect(decoded[0].id).toBe(\"second\");\n // The first session's UserSecrets file is left behind (not cleaned).\n // This matches a real logout flow as well, so we don't try to hide it.\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${first.sessionId}.json`), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n await expect(\n readFile(join(storageDir, `my-app_UserSecrets_${second.sessionId}.json`), \"utf-8\"),\n ).resolves.toMatch(/^0x/);\n });\n\n test(\"AccountIdCodec is Bytes(32) — keep this invariant if upstream changes\", () => {\n // Guards against silent upstream changes to the session-account\n // codec shape that would make our synthesized files unreadable.\n const encoded = AccountIdCodec.enc(createAccountId(new Uint8Array(32).fill(7)));\n expect(encoded).toHaveLength(32);\n });\n });\n}\n"],"mappings":"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","names":[]}
|