@parity/product-sdk-terminal 0.1.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/LICENSE +201 -0
- package/README.md +261 -0
- package/dist/chunk-5MCLSFKQ.js +210 -0
- package/dist/chunk-5MCLSFKQ.js.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +327 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.mjs +76 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +9 -0
- package/dist/register.js.map +1 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.js +294 -0
- package/dist/testing.js.map +1 -0
- package/package.json +64 -0
- package/scripts/patch-wasm.sh +27 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node-storage.ts"],"sourcesContent":["/**\n * File-based StorageAdapter for Node.js environments.\n *\n * Implements the @novasamatech/storage-adapter interface using JSON files\n * in ~/.polkadot-apps/. Node.js doesn't have localStorage, so this\n * provides persistent storage for the SDK's session and secret data.\n */\nimport type { StorageAdapter } from \"@novasamatech/storage-adapter\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { fromPromise } from \"neverthrow\";\nimport { join } from \"node:path\";\nimport { readFile, writeFile, mkdir, unlink } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\n\nconst log = createLogger(\"terminal\");\nconst DEFAULT_STORAGE_DIR = join(homedir(), \".polkadot-apps\");\n\n/**\n * Compute the storage filename for a given appId+key pair.\n *\n * Exposed (rather than kept private) so the test-session helper in\n * `./testing.ts` can target the same file the live adapter reads from\n * without having to duplicate the sanitization rule.\n */\nexport function sanitizeKey(appId: string, key: string): string {\n return `${appId}_${key}`.replace(/[^a-zA-Z0-9_.-]/g, \"_\");\n}\n\nfunction toError(e: unknown): Error {\n return e instanceof Error ? e : new Error(String(e));\n}\n\n/**\n * Create a file-based StorageAdapter for use with the host-papp SDK in Node.js.\n *\n * Data is stored as individual JSON files in the given directory\n * (defaults to `~/.polkadot-apps/`).\n */\nexport function createNodeStorageAdapter(appId: string, storageDir?: string): StorageAdapter {\n const dir = storageDir ?? DEFAULT_STORAGE_DIR;\n let dirCreated = false;\n const subscribers = new Map<string, Set<(value: string | null) => unknown>>();\n\n function fp(key: string): string {\n return join(dir, `${sanitizeKey(appId, key)}.json`);\n }\n\n async function ensureDir(): Promise<void> {\n if (dirCreated) return;\n await mkdir(dir, { recursive: true });\n dirCreated = true;\n }\n\n function notifySubscribers(key: string, value: string | null) {\n const subs = subscribers.get(key);\n if (subs) {\n for (const cb of subs) {\n try {\n cb(value);\n } catch (e) {\n log.warn(\"storage subscriber callback threw\", { key, error: e });\n }\n }\n }\n }\n\n return {\n read(key: string) {\n return fromPromise(\n readFile(fp(key), \"utf-8\").catch((e) => {\n // Missing files are expected (a missing key reads as null);\n // log at debug so consumers can opt in to seeing reads when\n // diagnosing \"why isn't my session loading\".\n if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") {\n log.warn(\"storage read failed\", { key, error: e });\n } else {\n log.debug(\"storage read miss\", { key });\n }\n return null;\n }),\n toError,\n );\n },\n\n write(key: string, value: string) {\n return fromPromise(\n ensureDir()\n .then(() => writeFile(fp(key), value, \"utf-8\"))\n .then(() => {\n notifySubscribers(key, value);\n }),\n toError,\n ).map(() => undefined as undefined);\n },\n\n clear(key: string) {\n return fromPromise(\n unlink(fp(key))\n .catch(() => {})\n .then(() => {\n notifySubscribers(key, null);\n }),\n toError,\n ).map(() => undefined as undefined);\n },\n\n subscribe(key: string, callback: (value: string | null) => unknown) {\n if (!subscribers.has(key)) {\n subscribers.set(key, new Set());\n }\n subscribers.get(key)!.add(callback);\n return () => {\n subscribers.get(key)?.delete(callback);\n };\n },\n };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, beforeEach, afterAll } = import.meta.vitest;\n const { mkdtemp, rm } = await import(\"node:fs/promises\");\n const { tmpdir } = await import(\"node:os\");\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n let testDir: string;\n\n beforeEach(async () => {\n // Silence the logger so tests that exercise the warn paths don't\n // pollute stderr with expected log output.\n configure({ handler: () => {} });\n testDir = await mkdtemp(join(tmpdir(), \"terminal-storage-test-\"));\n });\n\n afterAll(async () => {\n // Clean up any remaining test dirs\n try {\n await rm(testDir, { recursive: true });\n } catch {\n /* ignore */\n }\n });\n\n describe(\"createNodeStorageAdapter\", () => {\n test(\"read returns null for missing key\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const result = await store.read(\"nonexistent\");\n expect(result.isOk()).toBe(true);\n expect(result._unsafeUnwrap()).toBeNull();\n });\n\n test(\"write and read round-trip\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n await store.write(\"key1\", \"hello\");\n const result = await store.read(\"key1\");\n expect(result._unsafeUnwrap()).toBe(\"hello\");\n });\n\n test(\"write overwrites existing value\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n await store.write(\"key1\", \"first\");\n await store.write(\"key1\", \"second\");\n const result = await store.read(\"key1\");\n expect(result._unsafeUnwrap()).toBe(\"second\");\n });\n\n test(\"clear removes key\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n await store.write(\"key1\", \"value\");\n await store.clear(\"key1\");\n const result = await store.read(\"key1\");\n expect(result._unsafeUnwrap()).toBeNull();\n });\n\n test(\"clear is safe for missing key\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const result = await store.clear(\"nonexistent\");\n expect(result.isOk()).toBe(true);\n });\n\n test(\"different appIds are isolated\", async () => {\n const storeA = createNodeStorageAdapter(\"app-a\", testDir);\n const storeB = createNodeStorageAdapter(\"app-b\", testDir);\n await storeA.write(\"key\", \"from-a\");\n await storeB.write(\"key\", \"from-b\");\n expect((await storeA.read(\"key\"))._unsafeUnwrap()).toBe(\"from-a\");\n expect((await storeB.read(\"key\"))._unsafeUnwrap()).toBe(\"from-b\");\n });\n\n test(\"subscribe notifies on write\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const values: (string | null)[] = [];\n store.subscribe(\"key1\", (v) => values.push(v));\n\n await store.write(\"key1\", \"hello\");\n expect(values).toEqual([\"hello\"]);\n });\n\n test(\"subscribe notifies on clear\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const values: (string | null)[] = [];\n await store.write(\"key1\", \"hello\");\n store.subscribe(\"key1\", (v) => values.push(v));\n\n await store.clear(\"key1\");\n expect(values).toEqual([null]);\n });\n\n test(\"unsubscribe stops notifications\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const values: (string | null)[] = [];\n const unsub = store.subscribe(\"key1\", (v) => values.push(v));\n\n await store.write(\"key1\", \"first\");\n unsub();\n await store.write(\"key1\", \"second\");\n\n expect(values).toEqual([\"first\"]);\n });\n\n test(\"subscriber errors do not break other subscribers\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const values: string[] = [];\n store.subscribe(\"key1\", () => {\n throw new Error(\"boom\");\n });\n store.subscribe(\"key1\", (v) => {\n if (v) values.push(v);\n });\n\n await store.write(\"key1\", \"hello\");\n expect(values).toEqual([\"hello\"]);\n });\n\n test(\"sanitizes special characters in keys\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n await store.write(\"key/with:special chars!\", \"value\");\n const result = await store.read(\"key/with:special chars!\");\n expect(result._unsafeUnwrap()).toBe(\"value\");\n });\n\n test(\"handles JSON values\", async () => {\n const store = createNodeStorageAdapter(\"test\", testDir);\n const obj = { name: \"test\", count: 42, nested: { ok: true } };\n await store.write(\"json\", JSON.stringify(obj));\n const raw = (await store.read(\"json\"))._unsafeUnwrap();\n expect(JSON.parse(raw!)).toEqual(obj);\n });\n });\n\n describe(\"toError\", () => {\n test(\"returns Error instances unchanged\", () => {\n const original = new TypeError(\"boom\");\n expect(toError(original)).toBe(original);\n });\n\n test(\"wraps non-Error string values\", () => {\n const result = toError(\"primitive failure\");\n expect(result).toBeInstanceOf(Error);\n expect(result.message).toBe(\"primitive failure\");\n });\n\n test(\"wraps non-Error nullish values\", () => {\n const result = toError(null);\n expect(result).toBeInstanceOf(Error);\n expect(result.message).toBe(\"null\");\n });\n });\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { PappAdapter, HostMetadata, UserSession } from '@novasamatech/host-papp';
|
|
2
|
+
export { AttestationStatus, HostMetadata, PairingStatus, PappAdapter, SS_PASEO_STABLE_STAGE_ENDPOINTS, SS_STABLE_STAGE_ENDPOINTS, SigningPayloadRequest, SigningPayloadResponse, SigningRawRequest, StoredUserSession, UserSession } from '@novasamatech/host-papp';
|
|
3
|
+
import { PolkadotSigner } from 'polkadot-api';
|
|
4
|
+
import { StorageAdapter } from '@novasamatech/storage-adapter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Node.js adapter for the Polkadot host-papp SDK.
|
|
8
|
+
*
|
|
9
|
+
* Provides Node.js-compatible implementations of the SDK's storage and
|
|
10
|
+
* transport layers, enabling QR login, attestation, and signing in
|
|
11
|
+
* terminal/CLI environments.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Options for creating a terminal adapter. */
|
|
15
|
+
interface TerminalAdapterOptions {
|
|
16
|
+
/** Unique app identifier. Used as the storage namespace. */
|
|
17
|
+
appId: string;
|
|
18
|
+
/** URL to the app's metadata JSON (name + icon), shown during pairing. */
|
|
19
|
+
metadataUrl: string;
|
|
20
|
+
/** Statement store WebSocket endpoints. Defaults to Paseo stable endpoints. */
|
|
21
|
+
endpoints?: string[];
|
|
22
|
+
/** Optional host metadata for the Sign-In screen. */
|
|
23
|
+
hostMetadata?: HostMetadata;
|
|
24
|
+
/**
|
|
25
|
+
* Directory where session files are persisted. Defaults to
|
|
26
|
+
* `~/.polkadot-apps/`. Override in tests to point at a temporary
|
|
27
|
+
* directory populated with `createTestSession` from
|
|
28
|
+
* `@parity/product-sdk-terminal/testing`.
|
|
29
|
+
*/
|
|
30
|
+
storageDir?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a terminal adapter backed by the host-papp SDK.
|
|
34
|
+
*
|
|
35
|
+
* This sets up:
|
|
36
|
+
* - File-based storage in `~/.polkadot-apps/` (since Node.js has no localStorage)
|
|
37
|
+
* - WebSocket connection to the statement store
|
|
38
|
+
* - The full SSO flow: QR pairing + on-chain attestation
|
|
39
|
+
* - Session manager for signing requests
|
|
40
|
+
*/
|
|
41
|
+
/** A PappAdapter with the `appId` it was created with and a `destroy` method for cleanup. */
|
|
42
|
+
type TerminalAdapter = PappAdapter & {
|
|
43
|
+
/** The `appId` passed to {@link createTerminalAdapter}. Useful for {@link createSessionSigner}. */
|
|
44
|
+
readonly appId: string;
|
|
45
|
+
/**
|
|
46
|
+
* Disconnect the WebSocket and release resources.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* Idempotent. While this method is running and for ~50 ms after, the global
|
|
50
|
+
* `console.error` is monkey-patched to suppress the noisy
|
|
51
|
+
* `"Statement subscription error: …"` message that
|
|
52
|
+
* `@novasamatech/statement-store` emits when its WebSocket disconnects with
|
|
53
|
+
* live subscriptions still attached. Unrelated `console.error` calls in
|
|
54
|
+
* that window may be silently swallowed if their first argument is a string
|
|
55
|
+
* starting with `"Statement subscription"`. This is a pragmatic workaround
|
|
56
|
+
* for the upstream noise; ideally we contribute a `silent` option upstream.
|
|
57
|
+
*/
|
|
58
|
+
destroy(): void;
|
|
59
|
+
};
|
|
60
|
+
declare function createTerminalAdapter(options: TerminalAdapterOptions): TerminalAdapter;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Identifies which sub-account of a paired session should sign.
|
|
64
|
+
*
|
|
65
|
+
* Mirrors the `host-papp` wire format `productAccountId: [productId, derivationIndex]`:
|
|
66
|
+
* `productId` is the dotNS-style identifier for the requesting product (matches
|
|
67
|
+
* the adapter's `appId` in normal usage); `derivationIndex` is the BIP32-style
|
|
68
|
+
* child-key index, where `0` is the session's default account.
|
|
69
|
+
*/
|
|
70
|
+
interface ProductAccountRef {
|
|
71
|
+
/** The product identifier. Usually equal to the adapter's `appId`. */
|
|
72
|
+
productId: string;
|
|
73
|
+
/** Child-key derivation index. `0` is the default account. */
|
|
74
|
+
derivationIndex: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a `PolkadotSigner` backed by a QR-paired mobile wallet session,
|
|
78
|
+
* using the session's **default account** (`derivationIndex: 0`).
|
|
79
|
+
*
|
|
80
|
+
* For non-default sub-accounts, use {@link createSessionSignerForAccount}.
|
|
81
|
+
*
|
|
82
|
+
* @param session The paired user session.
|
|
83
|
+
* @param adapter The {@link TerminalAdapter} that loaded the session. Its `appId`
|
|
84
|
+
* is used as the `productId` in the wire request.
|
|
85
|
+
*/
|
|
86
|
+
declare function createSessionSigner(session: UserSession, adapter: TerminalAdapter): PolkadotSigner;
|
|
87
|
+
/**
|
|
88
|
+
* Create a `PolkadotSigner` for a specific sub-account of a paired session.
|
|
89
|
+
*
|
|
90
|
+
* Use this when you need a derivation index other than `0`, or a `productId`
|
|
91
|
+
* different from the adapter's `appId`. For the common default-account case,
|
|
92
|
+
* prefer {@link createSessionSigner}.
|
|
93
|
+
*
|
|
94
|
+
* @param session The paired user session.
|
|
95
|
+
* @param ref The product account to sign as: `{ productId, derivationIndex }`.
|
|
96
|
+
*/
|
|
97
|
+
declare function createSessionSignerForAccount(session: UserSession, ref: ProductAccountRef): PolkadotSigner;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Helpers for working with persisted sessions on a `TerminalAdapter`.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Wait for the adapter to load at least one persisted session, or resolve
|
|
105
|
+
* with an empty array after `timeoutMs`.
|
|
106
|
+
*
|
|
107
|
+
* The session manager loads sessions from storage asynchronously, so a
|
|
108
|
+
* synchronous `adapter.sessions.sessions.read()` immediately after
|
|
109
|
+
* `createTerminalAdapter()` may return `[]` even when sessions exist on
|
|
110
|
+
* disk. Use this helper to give the loader a chance to populate before
|
|
111
|
+
* deciding whether the user is logged in.
|
|
112
|
+
*/
|
|
113
|
+
declare function waitForSessions(adapter: TerminalAdapter, timeoutMs?: number): Promise<UserSession[]>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Terminal QR code rendering using Unicode half-block characters.
|
|
117
|
+
*
|
|
118
|
+
* Wraps the `qrcode` npm package's UTF-8 renderer, which packs two
|
|
119
|
+
* module rows per terminal line using half-block characters.
|
|
120
|
+
*/
|
|
121
|
+
/** Options for QR code rendering. */
|
|
122
|
+
interface QrRenderOptions {
|
|
123
|
+
/** Error correction level. Default: "M". */
|
|
124
|
+
errorCorrectionLevel?: "L" | "M" | "Q" | "H";
|
|
125
|
+
/** Quiet zone size in modules. Default: 2. */
|
|
126
|
+
margin?: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Encode a string as a QR code rendered in Unicode half-block characters.
|
|
130
|
+
*
|
|
131
|
+
* Returns a multi-line string suitable for `console.log`.
|
|
132
|
+
*/
|
|
133
|
+
declare function renderQrCode(data: string, options?: QrRenderOptions): Promise<string>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* File-based StorageAdapter for Node.js environments.
|
|
137
|
+
*
|
|
138
|
+
* Implements the @novasamatech/storage-adapter interface using JSON files
|
|
139
|
+
* in ~/.polkadot-apps/. Node.js doesn't have localStorage, so this
|
|
140
|
+
* provides persistent storage for the SDK's session and secret data.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a file-based StorageAdapter for use with the host-papp SDK in Node.js.
|
|
145
|
+
*
|
|
146
|
+
* Data is stored as individual JSON files in the given directory
|
|
147
|
+
* (defaults to `~/.polkadot-apps/`).
|
|
148
|
+
*/
|
|
149
|
+
declare function createNodeStorageAdapter(appId: string, storageDir?: string): StorageAdapter;
|
|
150
|
+
|
|
151
|
+
export { type ProductAccountRef, type QrRenderOptions, type TerminalAdapter, type TerminalAdapterOptions, createNodeStorageAdapter, createSessionSigner, createSessionSignerForAccount, createTerminalAdapter, renderQrCode, waitForSessions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNodeStorageAdapter
|
|
3
|
+
} from "./chunk-5MCLSFKQ.js";
|
|
4
|
+
|
|
5
|
+
// src/adapter.ts
|
|
6
|
+
import {
|
|
7
|
+
createPappAdapter,
|
|
8
|
+
SS_STABLE_STAGE_ENDPOINTS,
|
|
9
|
+
SS_PASEO_STABLE_STAGE_ENDPOINTS
|
|
10
|
+
} from "@novasamatech/host-papp";
|
|
11
|
+
import { createLazyClient, createPapiStatementStoreAdapter } from "@novasamatech/statement-store";
|
|
12
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
13
|
+
import { getWsProvider } from "@polkadot-api/ws-provider";
|
|
14
|
+
var log = createLogger("terminal");
|
|
15
|
+
function createTerminalAdapter(options) {
|
|
16
|
+
const endpoints = options.endpoints ?? SS_PASEO_STABLE_STAGE_ENDPOINTS;
|
|
17
|
+
const storage = createNodeStorageAdapter(options.appId, options.storageDir);
|
|
18
|
+
const HEARTBEAT_NEVER_MS = 2147483647;
|
|
19
|
+
const lazyClient = createLazyClient(
|
|
20
|
+
getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS })
|
|
21
|
+
);
|
|
22
|
+
const statementStore = createPapiStatementStoreAdapter(lazyClient);
|
|
23
|
+
const adapter = createPappAdapter({
|
|
24
|
+
appId: options.appId,
|
|
25
|
+
metadata: options.metadataUrl,
|
|
26
|
+
hostMetadata: options.hostMetadata,
|
|
27
|
+
adapters: {
|
|
28
|
+
storage,
|
|
29
|
+
lazyClient,
|
|
30
|
+
statementStore
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
let destroyed = false;
|
|
34
|
+
return {
|
|
35
|
+
...adapter,
|
|
36
|
+
appId: options.appId,
|
|
37
|
+
destroy() {
|
|
38
|
+
if (destroyed) return;
|
|
39
|
+
destroyed = true;
|
|
40
|
+
log.debug("destroying terminal adapter; suppressing statement-store teardown noise");
|
|
41
|
+
const origError = console.error;
|
|
42
|
+
console.error = (...args) => {
|
|
43
|
+
if (typeof args[0] === "string" && args[0].includes("Statement subscription")) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
origError.apply(console, args);
|
|
47
|
+
};
|
|
48
|
+
adapter.sessions.dispose();
|
|
49
|
+
try {
|
|
50
|
+
lazyClient.disconnect();
|
|
51
|
+
} catch (e) {
|
|
52
|
+
log.warn("lazyClient.disconnect threw during destroy", { error: e });
|
|
53
|
+
}
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
console.error = origError;
|
|
56
|
+
}, 50);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/signer.ts
|
|
62
|
+
import { getPolkadotSigner } from "polkadot-api/signer";
|
|
63
|
+
function buildSessionSigner(session, ref) {
|
|
64
|
+
const accountId = new Uint8Array(session.remoteAccount.accountId);
|
|
65
|
+
const productAccountId = [ref.productId, ref.derivationIndex];
|
|
66
|
+
return getPolkadotSigner(
|
|
67
|
+
accountId,
|
|
68
|
+
"Sr25519",
|
|
69
|
+
async (data) => {
|
|
70
|
+
const result = await session.signRaw({
|
|
71
|
+
productAccountId,
|
|
72
|
+
data: { tag: "Bytes", value: data }
|
|
73
|
+
});
|
|
74
|
+
if (result.isErr()) {
|
|
75
|
+
throw new Error(`Mobile signing rejected: ${result.error.message}`);
|
|
76
|
+
}
|
|
77
|
+
return result.value.signature;
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function createSessionSigner(session, adapter) {
|
|
82
|
+
return buildSessionSigner(session, { productId: adapter.appId, derivationIndex: 0 });
|
|
83
|
+
}
|
|
84
|
+
function createSessionSignerForAccount(session, ref) {
|
|
85
|
+
return buildSessionSigner(session, ref);
|
|
86
|
+
}
|
|
87
|
+
if (void 0) {
|
|
88
|
+
let makeSession = function(signRaw, accountIdBytes = new Array(32).fill(0).map((_, i) => i)) {
|
|
89
|
+
return {
|
|
90
|
+
remoteAccount: { accountId: accountIdBytes },
|
|
91
|
+
signRaw: vi.fn(signRaw)
|
|
92
|
+
};
|
|
93
|
+
}, fakeAdapter = function(appId) {
|
|
94
|
+
return { appId };
|
|
95
|
+
};
|
|
96
|
+
makeSession2 = makeSession, fakeAdapter2 = fakeAdapter;
|
|
97
|
+
const { describe, test, expect, vi } = void 0;
|
|
98
|
+
const { ok, err } = await null;
|
|
99
|
+
describe("createSessionSigner", () => {
|
|
100
|
+
test("exposes Sr25519 public key matching remoteAccount.accountId", () => {
|
|
101
|
+
const bytes = Array.from({ length: 32 }, (_, i) => i);
|
|
102
|
+
const signer = createSessionSigner(
|
|
103
|
+
makeSession(async () => ok({ signature: new Uint8Array() }), bytes),
|
|
104
|
+
fakeAdapter("test-app")
|
|
105
|
+
);
|
|
106
|
+
expect(signer.publicKey).toEqual(new Uint8Array(bytes));
|
|
107
|
+
});
|
|
108
|
+
test("signBytes returns signature on success", async () => {
|
|
109
|
+
const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
|
110
|
+
const session = makeSession(async () => ok({ signature: sig }));
|
|
111
|
+
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
112
|
+
const out = await signer.signBytes(new Uint8Array([1, 2, 3]));
|
|
113
|
+
expect(out).toEqual(sig);
|
|
114
|
+
});
|
|
115
|
+
test("forwards request with productAccountId = [adapter.appId, 0]", async () => {
|
|
116
|
+
const captured = [];
|
|
117
|
+
const session = makeSession(async (req2) => {
|
|
118
|
+
captured.push(req2);
|
|
119
|
+
return ok({ signature: new Uint8Array([1]) });
|
|
120
|
+
});
|
|
121
|
+
const signer = createSessionSigner(session, fakeAdapter("inferred-app"));
|
|
122
|
+
await signer.signBytes(new Uint8Array([1, 2, 3]));
|
|
123
|
+
const req = captured[0];
|
|
124
|
+
expect(req.productAccountId).toEqual(["inferred-app", 0]);
|
|
125
|
+
});
|
|
126
|
+
test("signBytes throws when mobile signing is rejected", async () => {
|
|
127
|
+
const session = makeSession(async () => err({ message: "user declined" }));
|
|
128
|
+
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
129
|
+
await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(
|
|
130
|
+
"Mobile signing rejected: user declined"
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("createSessionSignerForAccount", () => {
|
|
135
|
+
test("forwards request with the given productId and derivationIndex", async () => {
|
|
136
|
+
const captured = [];
|
|
137
|
+
const session = makeSession(async (req2) => {
|
|
138
|
+
captured.push(req2);
|
|
139
|
+
return ok({ signature: new Uint8Array([42]) });
|
|
140
|
+
});
|
|
141
|
+
const signer = createSessionSignerForAccount(session, {
|
|
142
|
+
productId: "my-app",
|
|
143
|
+
derivationIndex: 7
|
|
144
|
+
});
|
|
145
|
+
await signer.signBytes(new Uint8Array([10, 20, 30]));
|
|
146
|
+
expect(captured).toHaveLength(1);
|
|
147
|
+
const req = captured[0];
|
|
148
|
+
expect(req.productAccountId).toEqual(["my-app", 7]);
|
|
149
|
+
expect(req.data.tag).toBe("Bytes");
|
|
150
|
+
expect(req.data.value).toBeInstanceOf(Uint8Array);
|
|
151
|
+
});
|
|
152
|
+
test("supports a productId different from any adapter's appId", async () => {
|
|
153
|
+
const captured = [];
|
|
154
|
+
const session = makeSession(async (req2) => {
|
|
155
|
+
captured.push(req2);
|
|
156
|
+
return ok({ signature: new Uint8Array([0]) });
|
|
157
|
+
});
|
|
158
|
+
const signer = createSessionSignerForAccount(session, {
|
|
159
|
+
productId: "external-product",
|
|
160
|
+
derivationIndex: 0
|
|
161
|
+
});
|
|
162
|
+
await signer.signBytes(new Uint8Array([1]));
|
|
163
|
+
const req = captured[0];
|
|
164
|
+
expect(req.productAccountId).toEqual(["external-product", 0]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
var makeSession2;
|
|
169
|
+
var fakeAdapter2;
|
|
170
|
+
|
|
171
|
+
// src/sessions.ts
|
|
172
|
+
function waitForSessions(adapter, timeoutMs = 3e3) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
let resolved = false;
|
|
175
|
+
let unsub = null;
|
|
176
|
+
const finish = (sessions) => {
|
|
177
|
+
if (resolved) return;
|
|
178
|
+
resolved = true;
|
|
179
|
+
queueMicrotask(() => unsub?.());
|
|
180
|
+
resolve(sessions);
|
|
181
|
+
};
|
|
182
|
+
const timer = setTimeout(() => finish([]), timeoutMs);
|
|
183
|
+
unsub = adapter.sessions.sessions.subscribe((sessions) => {
|
|
184
|
+
if (sessions.length > 0) {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
finish(sessions);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (void 0) {
|
|
192
|
+
let fakeAdapter = function() {
|
|
193
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
194
|
+
const adapter = {
|
|
195
|
+
sessions: {
|
|
196
|
+
sessions: {
|
|
197
|
+
subscribe(cb) {
|
|
198
|
+
subscribers.add(cb);
|
|
199
|
+
return () => {
|
|
200
|
+
subscribers.delete(cb);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
adapter,
|
|
208
|
+
emit: (sessions) => {
|
|
209
|
+
for (const cb of subscribers) cb(sessions);
|
|
210
|
+
},
|
|
211
|
+
subscriberCount: () => subscribers.size
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
fakeAdapter2 = fakeAdapter;
|
|
215
|
+
const { describe, test, expect, vi } = void 0;
|
|
216
|
+
describe("waitForSessions", () => {
|
|
217
|
+
test("resolves with sessions on first non-empty emission", async () => {
|
|
218
|
+
const { adapter, emit } = fakeAdapter();
|
|
219
|
+
const promise = waitForSessions(adapter, 1e3);
|
|
220
|
+
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
221
|
+
emit([session]);
|
|
222
|
+
await expect(promise).resolves.toEqual([session]);
|
|
223
|
+
});
|
|
224
|
+
test("ignores empty emissions and waits for a non-empty one", async () => {
|
|
225
|
+
const { adapter, emit } = fakeAdapter();
|
|
226
|
+
const promise = waitForSessions(adapter, 1e3);
|
|
227
|
+
emit([]);
|
|
228
|
+
emit([]);
|
|
229
|
+
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
230
|
+
emit([session]);
|
|
231
|
+
await expect(promise).resolves.toEqual([session]);
|
|
232
|
+
});
|
|
233
|
+
test("resolves with [] after timeout when nothing is emitted", async () => {
|
|
234
|
+
vi.useFakeTimers();
|
|
235
|
+
try {
|
|
236
|
+
const { adapter } = fakeAdapter();
|
|
237
|
+
const promise = waitForSessions(adapter, 50);
|
|
238
|
+
vi.advanceTimersByTime(50);
|
|
239
|
+
await expect(promise).resolves.toEqual([]);
|
|
240
|
+
} finally {
|
|
241
|
+
vi.useRealTimers();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
test("unsubscribes after resolving", async () => {
|
|
245
|
+
const { adapter, emit, subscriberCount } = fakeAdapter();
|
|
246
|
+
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
247
|
+
const promise = waitForSessions(adapter, 1e3);
|
|
248
|
+
emit([session]);
|
|
249
|
+
await promise;
|
|
250
|
+
await Promise.resolve();
|
|
251
|
+
expect(subscriberCount()).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
test("uses default 3000ms timeout when none is passed", async () => {
|
|
254
|
+
vi.useFakeTimers();
|
|
255
|
+
try {
|
|
256
|
+
const { adapter } = fakeAdapter();
|
|
257
|
+
const promise = waitForSessions(adapter);
|
|
258
|
+
vi.advanceTimersByTime(2999);
|
|
259
|
+
let settled = false;
|
|
260
|
+
promise.then(() => {
|
|
261
|
+
settled = true;
|
|
262
|
+
});
|
|
263
|
+
await Promise.resolve();
|
|
264
|
+
expect(settled).toBe(false);
|
|
265
|
+
vi.advanceTimersByTime(1);
|
|
266
|
+
await expect(promise).resolves.toEqual([]);
|
|
267
|
+
} finally {
|
|
268
|
+
vi.useRealTimers();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
test("handles synchronous initial emission", async () => {
|
|
272
|
+
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
273
|
+
const adapter = {
|
|
274
|
+
sessions: {
|
|
275
|
+
sessions: {
|
|
276
|
+
subscribe(cb) {
|
|
277
|
+
cb([session]);
|
|
278
|
+
return () => {
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
await expect(waitForSessions(adapter, 1e3)).resolves.toEqual([session]);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
var fakeAdapter2;
|
|
289
|
+
|
|
290
|
+
// src/qr-encode.ts
|
|
291
|
+
async function renderQrCode(data, options) {
|
|
292
|
+
const QRCode = await import("qrcode");
|
|
293
|
+
const result = await QRCode.toString(data, {
|
|
294
|
+
type: "utf8",
|
|
295
|
+
errorCorrectionLevel: options?.errorCorrectionLevel ?? "M",
|
|
296
|
+
margin: options?.margin ?? 2
|
|
297
|
+
});
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
if (void 0) {
|
|
301
|
+
const { test, expect } = void 0;
|
|
302
|
+
test("renderQrCode produces non-empty output", async () => {
|
|
303
|
+
const result = await renderQrCode("https://example.com");
|
|
304
|
+
expect(result.length).toBeGreaterThan(0);
|
|
305
|
+
expect(result).toContain("\n");
|
|
306
|
+
});
|
|
307
|
+
test("renderQrCode contains Unicode block characters", async () => {
|
|
308
|
+
const result = await renderQrCode("test");
|
|
309
|
+
expect(/[▀▄█]/.test(result)).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
test("different inputs produce different QR codes", async () => {
|
|
312
|
+
const a = await renderQrCode("hello");
|
|
313
|
+
const b = await renderQrCode("world");
|
|
314
|
+
expect(a).not.toBe(b);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
export {
|
|
318
|
+
SS_PASEO_STABLE_STAGE_ENDPOINTS,
|
|
319
|
+
SS_STABLE_STAGE_ENDPOINTS,
|
|
320
|
+
createNodeStorageAdapter,
|
|
321
|
+
createSessionSigner,
|
|
322
|
+
createSessionSignerForAccount,
|
|
323
|
+
createTerminalAdapter,
|
|
324
|
+
renderQrCode,
|
|
325
|
+
waitForSessions
|
|
326
|
+
};
|
|
327
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 { createLazyClient, createPapiStatementStoreAdapter } 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. While this method is running and for ~50 ms after, the global\n * `console.error` is monkey-patched to suppress the noisy\n * `\"Statement subscription error: …\"` message that\n * `@novasamatech/statement-store` emits when its WebSocket disconnects with\n * live subscriptions still attached. Unrelated `console.error` calls in\n * that window may be silently swallowed if their first argument is a string\n * starting with `\"Statement subscription\"`. This is a pragmatic workaround\n * for the upstream noise; ideally we contribute a `silent` option upstream.\n */\n destroy(): 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 lazyClient = createLazyClient(\n getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS }),\n );\n const statementStore = createPapiStatementStoreAdapter(lazyClient);\n\n const adapter = createPappAdapter({\n appId: options.appId,\n metadata: options.metadataUrl,\n hostMetadata: options.hostMetadata,\n adapters: {\n storage,\n lazyClient,\n statementStore,\n },\n });\n\n let destroyed = false;\n return {\n ...adapter,\n appId: options.appId,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n log.debug(\"destroying terminal adapter; suppressing statement-store teardown noise\");\n\n // The statement-store logs `console.error(\"Statement subscription error:\", err)`\n // when the WebSocket disconnects while subscriptions are still active.\n // This is expected during teardown. Temporarily mute it.\n const origError = console.error;\n console.error = (...args: unknown[]) => {\n if (typeof args[0] === \"string\" && args[0].includes(\"Statement subscription\")) {\n return;\n }\n origError.apply(console, args);\n };\n\n adapter.sessions.dispose();\n try {\n lazyClient.disconnect();\n } catch (e) {\n log.warn(\"lazyClient.disconnect threw during destroy\", { error: e });\n }\n\n setTimeout(() => {\n console.error = origError;\n }, 50);\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's `signRaw()` to polkadot-api's\n * `PolkadotSigner` interface via `getPolkadotSigner`, enabling\n * mobile-approved signing for on-chain transactions from the CLI.\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 { getPolkadotSigner } from \"polkadot-api/signer\";\nimport type { PolkadotSigner } from \"polkadot-api\";\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\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\n return getPolkadotSigner(\n accountId,\n \"Sr25519\",\n async (data: Uint8Array): Promise<Uint8Array> => {\n const result = await session.signRaw({\n productAccountId,\n data: { tag: \"Bytes\" as const, 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}\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 whose `signRaw` is a Vitest spy.\n * Only the fields used by `createSessionSigner` are populated.\n */\n function makeSession(\n signRaw: (req: unknown) => Promise<unknown>,\n accountIdBytes: number[] = new Array(32).fill(0).map((_, i) => i),\n ): UserSession {\n return {\n remoteAccount: { accountId: accountIdBytes },\n signRaw: vi.fn(signRaw),\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(async () => ok({ signature: new Uint8Array() }), bytes),\n fakeAdapter(\"test-app\"),\n );\n expect(signer.publicKey).toEqual(new Uint8Array(bytes));\n });\n\n test(\"signBytes returns signature on success\", async () => {\n const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);\n const session = makeSession(async () => ok({ signature: sig }));\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\n test(\"forwards request with productAccountId = [adapter.appId, 0]\", async () => {\n const captured: unknown[] = [];\n const session = makeSession(async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([1]) });\n });\n\n const signer = createSessionSigner(session, fakeAdapter(\"inferred-app\"));\n await signer.signBytes(new Uint8Array([1, 2, 3]));\n\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"inferred-app\", 0]);\n });\n\n test(\"signBytes throws when mobile signing is rejected\", async () => {\n const session = makeSession(async () => err({ message: \"user declined\" }));\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(\"createSessionSignerForAccount\", () => {\n test(\"forwards request with the given productId and derivationIndex\", async () => {\n const captured: unknown[] = [];\n const session = makeSession(async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([42]) });\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"my-app\",\n derivationIndex: 7,\n });\n\n // Note: polkadot-api wraps signBytes payloads in <Bytes>...</Bytes>\n // before invoking the underlying callback. We only care here that\n // our wrapping (`{ tag: 'Bytes', value }` envelope + productAccountId\n // tuple) is correct — not the byte-level payload contents.\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(async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0]) });\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":["makeSession","fakeAdapter","req","fakeAdapter"]}
|
package/dist/loader.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js ESM loader hook that redirects `verifiablejs/bundler` imports
|
|
3
|
+
* to the nodejs WASM build (which loads .wasm from disk instead of inline).
|
|
4
|
+
*
|
|
5
|
+
* The host-papp SDK imports `verifiablejs/bundler` which inlines .wasm —
|
|
6
|
+
* this doesn't work in Node.js. The nodejs build loads .wasm from the
|
|
7
|
+
* filesystem instead.
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
// Find verifiablejs/pkg-nodejs by walking node_modules from host-papp
|
|
14
|
+
let nodejsEntry = null;
|
|
15
|
+
let hostPappFound = false;
|
|
16
|
+
try {
|
|
17
|
+
const require = createRequire(join(process.cwd(), "_"));
|
|
18
|
+
const hostPappPath = dirname(require.resolve("@novasamatech/host-papp"));
|
|
19
|
+
hostPappFound = true;
|
|
20
|
+
|
|
21
|
+
let dir = hostPappPath;
|
|
22
|
+
for (let i = 0; i < 10; i++) {
|
|
23
|
+
const candidate = join(dir, "node_modules", "verifiablejs", "pkg-nodejs", "verifiablejs.js");
|
|
24
|
+
if (existsSync(candidate)) {
|
|
25
|
+
nodejsEntry = candidate;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// host-papp not installed — loader is a no-op (legitimate during smoke tests).
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (hostPappFound && !nodejsEntry) {
|
|
37
|
+
// Found host-papp but couldn't locate verifiablejs/pkg-nodejs after the walk-up.
|
|
38
|
+
// Subsequent imports of `verifiablejs/bundler` will resolve to the inline-WASM
|
|
39
|
+
// build and fail at import time with a cryptic loader error. Warn early.
|
|
40
|
+
//
|
|
41
|
+
// Uses raw console.warn (rather than @parity/product-sdk-logger) because this
|
|
42
|
+
// loader runs before any user code has had a chance to call `configure()` on
|
|
43
|
+
// the logger — routing through the logger would emit via the *default*
|
|
44
|
+
// handler (console.warn) anyway, with extra import overhead for nothing.
|
|
45
|
+
console.warn(
|
|
46
|
+
"[@parity/product-sdk-terminal/register] Found @novasamatech/host-papp but could not locate verifiablejs/pkg-nodejs/verifiablejs.js after walking 10 directories. The Node.js WASM patch will not be applied — host-papp imports may fail. Check that verifiablejs is hoisted into a node_modules dir near host-papp.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
51
|
+
if (specifier === "verifiablejs/bundler" && nodejsEntry) {
|
|
52
|
+
return { shortCircuit: true, url: "verifiablejs-node://shim" };
|
|
53
|
+
}
|
|
54
|
+
return nextResolve(specifier, context);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function load(url, context, nextLoad) {
|
|
58
|
+
if (url === "verifiablejs-node://shim") {
|
|
59
|
+
// The shim runs in the main thread. It loads the CJS module via
|
|
60
|
+
// createRequire with the path we resolved in the loader thread.
|
|
61
|
+
const source = `
|
|
62
|
+
import { createRequire } from "node:module";
|
|
63
|
+
const _require = createRequire(${JSON.stringify(nodejsEntry)});
|
|
64
|
+
const _mod = _require(${JSON.stringify(nodejsEntry)});
|
|
65
|
+
export const sign = _mod.sign;
|
|
66
|
+
export const member_from_entropy = _mod.member_from_entropy;
|
|
67
|
+
export const members_intermediate = _mod.members_intermediate;
|
|
68
|
+
export const verify_signature = _mod.verify_signature;
|
|
69
|
+
export const members_root = _mod.members_root;
|
|
70
|
+
export const validate = _mod.validate;
|
|
71
|
+
export default _mod;
|
|
72
|
+
`;
|
|
73
|
+
return { shortCircuit: true, format: "module", source };
|
|
74
|
+
}
|
|
75
|
+
return nextLoad(url, context);
|
|
76
|
+
}
|