@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.
@@ -0,0 +1,9 @@
1
+ // src/register.ts
2
+ import { register } from "module";
3
+ import { pathToFileURL } from "url";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ var __dirname = dirname(fileURLToPath(import.meta.url));
7
+ var loaderPath = join(__dirname, "loader.mjs");
8
+ register(pathToFileURL(loaderPath).href, { parentURL: import.meta.url });
9
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/register.ts"],"sourcesContent":["/**\n * Register the verifiablejs Node.js loader hook.\n *\n * Must be loaded BEFORE the application entry point so the loader\n * intercepts `verifiablejs/bundler` imports from the host-papp SDK.\n *\n * @example\n * ```bash\n * node --import @parity/product-sdk-terminal/register app.js\n * tsx --import @parity/product-sdk-terminal/register app.ts\n * ```\n */\nimport { register } from \"node:module\";\nimport { pathToFileURL } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst loaderPath = join(__dirname, \"loader.mjs\");\n\nregister(pathToFileURL(loaderPath).href, { parentURL: import.meta.url });\n"],"mappings":";AAYA,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAE9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxD,IAAM,aAAa,KAAK,WAAW,YAAY;AAE/C,SAAS,cAAc,UAAU,EAAE,MAAM,EAAE,WAAW,YAAY,IAAI,CAAC;","names":[]}
@@ -0,0 +1,92 @@
1
+ interface CreateTestSessionOptions {
2
+ /** Unique app identifier. Must match the one passed to `createTerminalAdapter`. */
3
+ appId: string;
4
+ /**
5
+ * Directory where session files are written. Must match the one the
6
+ * CLI reads from — pass the same value to `createTerminalAdapter`'s
7
+ * `storageDir` option.
8
+ */
9
+ storageDir: string;
10
+ /** BIP39 mnemonic for the local (host-side) account. Default: freshly generated. */
11
+ localMnemonic?: string;
12
+ /** Derivation path for the local account. Default: `"//wallet//sso"` (matches production). */
13
+ localDerivation?: string;
14
+ /** BIP39 mnemonic for the remote (phone) account. Default: freshly generated. */
15
+ remoteMnemonic?: string;
16
+ /** Derivation path for the remote account. Default: `""`. */
17
+ remoteDerivation?: string;
18
+ /** Stable session id. Default: random nanoid(12). */
19
+ sessionId?: string;
20
+ /**
21
+ * Whether to write the encrypted `UserSecrets_<id>` file. Default: `true`.
22
+ * Set to `false` to exercise recovery paths where a session exists on disk
23
+ * but its secrets are missing.
24
+ */
25
+ includeSecrets?: boolean;
26
+ }
27
+ interface TestSession {
28
+ sessionId: string;
29
+ /** Sr25519 public key of the local (host) account, 32 bytes. */
30
+ localAccountId: Uint8Array;
31
+ /** Sr25519 public key of the remote (phone) account, 32 bytes. */
32
+ remoteAccountId: Uint8Array;
33
+ localMnemonic: string;
34
+ localDerivation: string;
35
+ remoteMnemonic: string;
36
+ remoteDerivation: string;
37
+ }
38
+ /**
39
+ * Write a valid persisted session to `storageDir`, as if the user had
40
+ * completed QR pairing + attestation. A `TerminalAdapter` created with the
41
+ * same `appId` + `storageDir` will emit the synthesized session from
42
+ * `adapter.sessions.sessions`.
43
+ *
44
+ * @remarks
45
+ * **Dev utility — not a stable contract.** This helper hand-rolls SCALE codecs
46
+ * that mirror an internal on-disk format used by `@novasamatech/host-papp`'s
47
+ * session repositories. The format is not part of host-papp's documented public
48
+ * API and may change on minor version bumps. The `testing.interop.test.ts`
49
+ * round-trip test fails loudly when the format drifts. Use this only in test code.
50
+ *
51
+ * ## Limits
52
+ *
53
+ * - **Signing does not round-trip.** Sending a request via `session.signRaw`
54
+ * still goes out over the statement store and expects a real phone to
55
+ * respond. Use this helper for flows that test session discovery,
56
+ * persistence, and logout — not end-to-end signing.
57
+ * - **Signing attempts surface `NoAllowanceError`.** The synthesized local
58
+ * account was never registered on the People chain, so the first write
59
+ * to the statement store fails with `NoAllowanceError`. That's the same
60
+ * error path the CLI sees when a previously valid session's on-chain
61
+ * attestation has expired, so tests that assert "CLI handles an expired
62
+ * session" *can* be written against a synthesized session even though
63
+ * there's no `expiresAt` knob to turn.
64
+ * - **No `expiresAt`.** The on-disk codec has no expiry field; validity is
65
+ * tracked via on-chain attestation state. See above for how expiry-path
66
+ * tests still work in practice.
67
+ * - **Corrupted-session cases** don't need a helper — write garbage to
68
+ * `<storageDir>/<appId>_SsoSessions.json` with `fs.writeFile` directly.
69
+ * - **Repeated calls replace the session list.** Each call writes a fresh
70
+ * single-entry `SsoSessions` file, so calling twice on the same
71
+ * `storageDir`+`appId` leaves only the second session on disk. Use a
72
+ * fresh `mkdtempSync` per test to keep cases isolated.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { mkdtempSync } from "node:fs";
77
+ * import { tmpdir } from "node:os";
78
+ * import { join } from "node:path";
79
+ * import { createTestSession } from "@parity/product-sdk-terminal/testing";
80
+ * import { createTerminalAdapter, waitForSessions } from "@parity/product-sdk-terminal";
81
+ *
82
+ * const storageDir = mkdtempSync(join(tmpdir(), "e2e-"));
83
+ * const { sessionId } = await createTestSession({ appId: "dot-cli", storageDir });
84
+ *
85
+ * const adapter = createTerminalAdapter({ appId: "dot-cli", metadataUrl: "…", storageDir });
86
+ * const sessions = await waitForSessions(adapter);
87
+ * // sessions[0].id === sessionId
88
+ * ```
89
+ */
90
+ declare function createTestSession(options: CreateTestSessionOptions): Promise<TestSession>;
91
+
92
+ export { type CreateTestSessionOptions, type TestSession, createTestSession };
@@ -0,0 +1,294 @@
1
+ import {
2
+ sanitizeKey
3
+ } from "./chunk-5MCLSFKQ.js";
4
+
5
+ // src/testing.ts
6
+ import { gcm } from "@noble/ciphers/aes.js";
7
+ import { p256 } from "@noble/curves/nist.js";
8
+ import { blake2b } from "@noble/hashes/blake2.js";
9
+ import {
10
+ LocalSessionAccountCodec,
11
+ RemoteSessionAccountCodec,
12
+ createAccountId,
13
+ createLocalSessionAccount,
14
+ createRemoteSessionAccount,
15
+ createSr25519Secret,
16
+ deriveSr25519PublicKey
17
+ } from "@novasamatech/statement-store";
18
+ import { toHex } from "@polkadot-api/utils";
19
+ import {
20
+ entropyToMiniSecret,
21
+ generateMnemonic,
22
+ mnemonicToEntropy
23
+ } from "@polkadot-labs/hdkd-helpers";
24
+ import { mkdir, writeFile } from "fs/promises";
25
+ import { join } from "path";
26
+ import { nanoid } from "nanoid";
27
+ import { Bytes, Struct, Vector, str } from "scale-ts";
28
+ var storedUserSessionCodec = Struct({
29
+ id: str,
30
+ localAccount: LocalSessionAccountCodec,
31
+ remoteAccount: RemoteSessionAccountCodec
32
+ });
33
+ var sessionsCodec = Vector(storedUserSessionCodec);
34
+ var storedUserSecretsCodec = Struct({
35
+ ssSecret: Bytes(),
36
+ encrSecret: Bytes(),
37
+ entropy: Bytes()
38
+ });
39
+ function encryptSecrets(appId, plaintext) {
40
+ const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });
41
+ const nonce = blake2b(new TextEncoder().encode("nonce"), { dkLen: 32 });
42
+ return toHex(gcm(key, nonce).encrypt(plaintext));
43
+ }
44
+ function p256SecretFromEntropy(entropy) {
45
+ const seed = new Uint8Array(48);
46
+ seed.set(entropyToMiniSecret(entropy));
47
+ return p256.keygen(seed).secretKey;
48
+ }
49
+ async function createTestSession(options) {
50
+ await mkdir(options.storageDir, { recursive: true });
51
+ const localMnemonic = options.localMnemonic ?? generateMnemonic();
52
+ const localDerivation = options.localDerivation ?? "//wallet//sso";
53
+ const localEntropy = mnemonicToEntropy(localMnemonic);
54
+ const localSecret = createSr25519Secret(localEntropy, localDerivation);
55
+ const localPublicKey = deriveSr25519PublicKey(localSecret);
56
+ const localEncrSecret = p256SecretFromEntropy(localEntropy);
57
+ const remoteMnemonic = options.remoteMnemonic ?? generateMnemonic();
58
+ const remoteDerivation = options.remoteDerivation ?? "";
59
+ const remoteEntropy = mnemonicToEntropy(remoteMnemonic);
60
+ const remoteSecret = createSr25519Secret(remoteEntropy, remoteDerivation);
61
+ const remotePublicKey = deriveSr25519PublicKey(remoteSecret);
62
+ const remoteEncrPublicKey = p256.getPublicKey(p256SecretFromEntropy(remoteEntropy), false);
63
+ const sharedSecret = p256.getSharedSecret(localEncrSecret, remoteEncrPublicKey).slice(1, 33);
64
+ const sessionId = options.sessionId ?? nanoid(12);
65
+ const session = {
66
+ id: sessionId,
67
+ localAccount: createLocalSessionAccount(createAccountId(localPublicKey), void 0),
68
+ remoteAccount: createRemoteSessionAccount(
69
+ createAccountId(remotePublicKey),
70
+ sharedSecret,
71
+ void 0
72
+ )
73
+ };
74
+ await writeFile(
75
+ join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessions")}.json`),
76
+ toHex(sessionsCodec.enc([session])),
77
+ "utf-8"
78
+ );
79
+ const includeSecrets = options.includeSecrets ?? true;
80
+ if (includeSecrets) {
81
+ const encoded = storedUserSecretsCodec.enc({
82
+ ssSecret: localSecret,
83
+ encrSecret: localEncrSecret,
84
+ entropy: localEntropy
85
+ });
86
+ await writeFile(
87
+ join(
88
+ options.storageDir,
89
+ `${sanitizeKey(options.appId, `UserSecrets_${sessionId}`)}.json`
90
+ ),
91
+ encryptSecrets(options.appId, encoded),
92
+ "utf-8"
93
+ );
94
+ }
95
+ return {
96
+ sessionId,
97
+ localAccountId: localPublicKey,
98
+ remoteAccountId: remotePublicKey,
99
+ localMnemonic,
100
+ localDerivation,
101
+ remoteMnemonic,
102
+ remoteDerivation
103
+ };
104
+ }
105
+ if (void 0) {
106
+ const { describe, test, expect, beforeEach, afterAll } = void 0;
107
+ const { mkdtemp, readFile, rm } = await null;
108
+ const { tmpdir } = await null;
109
+ const { fromHex } = await null;
110
+ const LOCAL_MNEMONIC = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
111
+ const REMOTE_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
112
+ let storageDir;
113
+ beforeEach(async () => {
114
+ storageDir = await mkdtemp(join(tmpdir(), "terminal-testing-"));
115
+ });
116
+ afterAll(async () => {
117
+ try {
118
+ await rm(storageDir, { recursive: true });
119
+ } catch {
120
+ }
121
+ });
122
+ describe("createTestSession", () => {
123
+ test("writes both SsoSessions and UserSecrets files by default", async () => {
124
+ const result = await createTestSession({
125
+ appId: "my-app",
126
+ storageDir,
127
+ localMnemonic: LOCAL_MNEMONIC,
128
+ remoteMnemonic: REMOTE_MNEMONIC
129
+ });
130
+ const sessions = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
131
+ expect(sessions).toMatch(/^0x[0-9a-f]+$/);
132
+ const secrets = await readFile(
133
+ join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`),
134
+ "utf-8"
135
+ );
136
+ expect(secrets).toMatch(/^0x[0-9a-f]+$/);
137
+ });
138
+ test("omits UserSecrets file when includeSecrets is false", async () => {
139
+ const result = await createTestSession({
140
+ appId: "my-app",
141
+ storageDir,
142
+ localMnemonic: LOCAL_MNEMONIC,
143
+ remoteMnemonic: REMOTE_MNEMONIC,
144
+ includeSecrets: false
145
+ });
146
+ await expect(
147
+ readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8")
148
+ ).resolves.toMatch(/^0x/);
149
+ await expect(
150
+ readFile(join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`), "utf-8")
151
+ ).rejects.toThrow(/ENOENT/);
152
+ });
153
+ test("SsoSessions file decodes with the host-papp session codec shape", async () => {
154
+ const result = await createTestSession({
155
+ appId: "my-app",
156
+ storageDir,
157
+ localMnemonic: LOCAL_MNEMONIC,
158
+ remoteMnemonic: REMOTE_MNEMONIC,
159
+ sessionId: "stable-test-id"
160
+ });
161
+ const hex = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
162
+ const decoded = sessionsCodec.dec(fromHex(hex));
163
+ expect(decoded).toHaveLength(1);
164
+ expect(decoded[0].id).toBe("stable-test-id");
165
+ expect(decoded[0].localAccount.accountId).toEqual(result.localAccountId);
166
+ expect(decoded[0].localAccount.pin).toBeUndefined();
167
+ expect(decoded[0].remoteAccount.accountId).toEqual(result.remoteAccountId);
168
+ expect(decoded[0].remoteAccount.publicKey).toHaveLength(32);
169
+ expect(decoded[0].remoteAccount.pin).toBeUndefined();
170
+ });
171
+ test("localAccountId matches the Sr25519 public key derived from the mnemonic", async () => {
172
+ const result = await createTestSession({
173
+ appId: "my-app",
174
+ storageDir,
175
+ localMnemonic: LOCAL_MNEMONIC,
176
+ remoteMnemonic: REMOTE_MNEMONIC
177
+ });
178
+ const expected = deriveSr25519PublicKey(
179
+ createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), "//wallet//sso")
180
+ );
181
+ expect(result.localAccountId).toEqual(expected);
182
+ expect(result.localAccountId).toHaveLength(32);
183
+ });
184
+ test("UserSecrets file decrypts and decodes with the host-papp secret codec shape", async () => {
185
+ const appId = "my-app";
186
+ const result = await createTestSession({
187
+ appId,
188
+ storageDir,
189
+ localMnemonic: LOCAL_MNEMONIC,
190
+ remoteMnemonic: REMOTE_MNEMONIC
191
+ });
192
+ const hex = await readFile(
193
+ join(storageDir, `${appId}_UserSecrets_${result.sessionId}.json`),
194
+ "utf-8"
195
+ );
196
+ const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });
197
+ const nonce = blake2b(new TextEncoder().encode("nonce"), { dkLen: 32 });
198
+ const decrypted = gcm(key, nonce).decrypt(fromHex(hex));
199
+ const decoded = storedUserSecretsCodec.dec(decrypted);
200
+ expect(decoded.entropy).toEqual(mnemonicToEntropy(LOCAL_MNEMONIC));
201
+ expect(decoded.ssSecret).toHaveLength(64);
202
+ expect(decoded.encrSecret).toHaveLength(32);
203
+ });
204
+ test("different appIds produce files under different prefixes", async () => {
205
+ await createTestSession({ appId: "app-a", storageDir, sessionId: "id" });
206
+ await createTestSession({ appId: "app-b", storageDir, sessionId: "id" });
207
+ await expect(
208
+ readFile(join(storageDir, "app-a_SsoSessions.json"), "utf-8")
209
+ ).resolves.toMatch(/^0x/);
210
+ await expect(
211
+ readFile(join(storageDir, "app-b_SsoSessions.json"), "utf-8")
212
+ ).resolves.toMatch(/^0x/);
213
+ });
214
+ test("sanitizes appIds with special characters", async () => {
215
+ await createTestSession({
216
+ appId: "app/with spaces",
217
+ storageDir,
218
+ sessionId: "id"
219
+ });
220
+ await expect(
221
+ readFile(join(storageDir, "app_with_spaces_SsoSessions.json"), "utf-8")
222
+ ).resolves.toMatch(/^0x/);
223
+ });
224
+ test("creates storageDir when it does not yet exist", async () => {
225
+ const nested = join(storageDir, "does", "not", "exist");
226
+ await createTestSession({ appId: "my-app", storageDir: nested });
227
+ await expect(
228
+ readFile(join(nested, "my-app_SsoSessions.json"), "utf-8")
229
+ ).resolves.toMatch(/^0x/);
230
+ });
231
+ test("generates fresh mnemonics when none are supplied", async () => {
232
+ const a = await createTestSession({ appId: "a", storageDir });
233
+ const b = await createTestSession({ appId: "b", storageDir });
234
+ expect(a.localMnemonic).not.toBe(b.localMnemonic);
235
+ expect(a.remoteMnemonic).not.toBe(b.remoteMnemonic);
236
+ expect(a.sessionId).not.toBe(b.sessionId);
237
+ });
238
+ test("respects an explicit sessionId", async () => {
239
+ const result = await createTestSession({
240
+ appId: "my-app",
241
+ storageDir,
242
+ sessionId: "pinned-id"
243
+ });
244
+ expect(result.sessionId).toBe("pinned-id");
245
+ await expect(
246
+ readFile(join(storageDir, "my-app_UserSecrets_pinned-id.json"), "utf-8")
247
+ ).resolves.toMatch(/^0x/);
248
+ });
249
+ test("respects a custom localDerivation", async () => {
250
+ const result = await createTestSession({
251
+ appId: "my-app",
252
+ storageDir,
253
+ localMnemonic: LOCAL_MNEMONIC,
254
+ remoteMnemonic: REMOTE_MNEMONIC,
255
+ localDerivation: "//custom//path"
256
+ });
257
+ expect(result.localDerivation).toBe("//custom//path");
258
+ const expected = deriveSr25519PublicKey(
259
+ createSr25519Secret(mnemonicToEntropy(LOCAL_MNEMONIC), "//custom//path")
260
+ );
261
+ expect(result.localAccountId).toEqual(expected);
262
+ });
263
+ test("repeated calls replace the session list", async () => {
264
+ const first = await createTestSession({
265
+ appId: "my-app",
266
+ storageDir,
267
+ sessionId: "first"
268
+ });
269
+ const second = await createTestSession({
270
+ appId: "my-app",
271
+ storageDir,
272
+ sessionId: "second"
273
+ });
274
+ const hex = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
275
+ const decoded = sessionsCodec.dec(fromHex(hex));
276
+ expect(decoded).toHaveLength(1);
277
+ expect(decoded[0].id).toBe("second");
278
+ await expect(
279
+ readFile(join(storageDir, `my-app_UserSecrets_${first.sessionId}.json`), "utf-8")
280
+ ).resolves.toMatch(/^0x/);
281
+ await expect(
282
+ readFile(join(storageDir, `my-app_UserSecrets_${second.sessionId}.json`), "utf-8")
283
+ ).resolves.toMatch(/^0x/);
284
+ });
285
+ test("AccountIdCodec is Bytes(32) \u2014 keep this invariant if upstream changes", () => {
286
+ const encoded = AccountIdCodec.enc(createAccountId(new Uint8Array(32).fill(7)));
287
+ expect(encoded).toHaveLength(32);
288
+ });
289
+ });
290
+ }
291
+ export {
292
+ createTestSession
293
+ };
294
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@parity/product-sdk-terminal",
3
+ "description": "QR code login and signing for CLI/terminal apps via Polkadot mobile wallet pairing",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "./dist/register.js"
8
+ ],
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./register": {
18
+ "types": "./dist/register.d.ts",
19
+ "import": "./dist/register.js"
20
+ },
21
+ "./testing": {
22
+ "types": "./dist/testing.d.ts",
23
+ "import": "./dist/testing.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "scripts"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@noble/ciphers": "^2.1.0",
35
+ "@noble/curves": "^2.0.1",
36
+ "@noble/hashes": "^2.0.1",
37
+ "@novasamatech/host-papp": "^0.7.7",
38
+ "@novasamatech/statement-store": "^0.7.7",
39
+ "@novasamatech/storage-adapter": "^0.7.7",
40
+ "@polkadot-api/utils": "^0.4.0",
41
+ "@polkadot-api/ws-provider": "^0.9.0",
42
+ "@polkadot-labs/hdkd-helpers": "^0.0.27",
43
+ "nanoid": "^5.1.7",
44
+ "neverthrow": "^8.2.0",
45
+ "polkadot-api": "^2.1.2",
46
+ "qrcode": "^1.5.4",
47
+ "scale-ts": "^1.6.1",
48
+ "@parity/product-sdk-logger": "0.1.1"
49
+ },
50
+ "devDependencies": {
51
+ "@types/qrcode": "^1.5.5",
52
+ "tsup": "^8.4.0",
53
+ "typescript": "^5.9.3",
54
+ "vitest": "^3.1.4"
55
+ },
56
+ "license": "Apache-2.0",
57
+ "scripts": {
58
+ "postinstall": "bash scripts/patch-wasm.sh",
59
+ "build": "tsup",
60
+ "clean": "rm -rf dist",
61
+ "pretest": "tsup",
62
+ "test": "vitest run"
63
+ }
64
+ }
@@ -0,0 +1,27 @@
1
+ #!/bin/bash
2
+ # Patch verifiablejs/bundler to load WASM via base64 embed instead of inline import.
3
+ # Required for bun runtime. Node.js/tsx users should use the ESM loader hook instead:
4
+ # --import @polkadot-apps/terminal/register
5
+
6
+ BUNDLER_JS=$(find node_modules -path "*/verifiablejs/pkg-bundler/verifiablejs.js" 2>/dev/null | head -1)
7
+ [ -z "$BUNDLER_JS" ] && exit 0
8
+
9
+ # Skip if already patched
10
+ head -1 "$BUNDLER_JS" | grep -q "__wbg_set_wasm" && exit 0
11
+
12
+ BUNDLER_DIR=$(dirname "$BUNDLER_JS")
13
+ # -w 0 disables line wrapping on Linux; -i is macOS flag
14
+ WASM_B64=$(base64 -w 0 "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 -i "$BUNDLER_DIR/verifiablejs_bg.wasm" 2>/dev/null || base64 "$BUNDLER_DIR/verifiablejs_bg.wasm")
15
+
16
+ cat > "$BUNDLER_JS" << SHIM
17
+ import { __wbg_set_wasm } from "./verifiablejs_bg.js";
18
+ import * as bg from "./verifiablejs_bg.js";
19
+
20
+ const wasmBytes = Uint8Array.from(atob("$WASM_B64"), c => c.charCodeAt(0));
21
+ const wasmModule = new WebAssembly.Module(wasmBytes);
22
+ const wasmInstance = new WebAssembly.Instance(wasmModule, { "./verifiablejs_bg.js": bg });
23
+ __wbg_set_wasm(wasmInstance.exports);
24
+ wasmInstance.exports.__wbindgen_start();
25
+
26
+ export * from "./verifiablejs_bg.js";
27
+ SHIM