@parity/product-sdk-terminal 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ Creates a terminal adapter backed by the host-papp SDK.
63
63
 
64
64
  **Options:**
65
65
  - `appId` -- unique app identifier (used as storage namespace)
66
- - `endpoints?` -- statement store WebSocket endpoints (defaults to Paseo)
66
+ - `endpoints?` -- statement store WebSocket endpoints (defaults to `StatementStoreNetworks.paseo`)
67
67
  - `hostMetadata?` -- optional host environment info
68
68
  - `storageDir?` -- override the on-disk session directory (defaults to `~/.polkadot-apps/`). Useful in tests and containerised environments.
69
69
 
@@ -73,6 +73,26 @@ Creates a terminal adapter backed by the host-papp SDK.
73
73
  - `sessions` -- session manager (signing, disconnect)
74
74
  - `destroy()` -- disconnect the WebSocket and release resources. Idempotent. Drops `@novasamatech/statement-store`'s benign `Statement subscription error: Client destroyed` line, which it emits synchronously while disconnecting a live subscription — only that line; every other `console.error` passes through.
75
75
 
76
+ ### `StatementStoreNetworks`
77
+
78
+ The statement store endpoints terminal can pair against, keyed by network.
79
+
80
+ | Key | Network | Endpoint |
81
+ | --- | --- | --- |
82
+ | `paseo` | Paseo Next v2 | `wss://paseo-people-next-system-rpc.polkadot.io` |
83
+ | `previewnet` | zombienet, a step ahead of paseo | whatever host-papp's `SS_PREVIEW_STAGE_ENDPOINTS` resolves to, today `wss://previewnet.substrate.dev/people` |
84
+
85
+ Keyed the same way as `BULLETIN_RPCS` in `@parity/product-sdk-host`. There is deliberately no
86
+ `devnet` key: that people chain has a statement store, but no endpoint for it is published here, and
87
+ no phone build pairs against it. Pass `endpoints` explicitly to reach one.
88
+
89
+ Pairing only works when the terminal and the phone are on the **same** people chain, and the phone
90
+ picks its chain per build flavour. The Polkadot app's nightly build uses `paseo`, which is why that
91
+ is the default; a preview-flavour phone needs `endpoints: StatementStoreNetworks.previewnet`.
92
+
93
+ `SS_STABLE_STAGE_ENDPOINTS` is also exported. It is internal to the Parity network and does not
94
+ answer from outside it.
95
+
76
96
  ### `isBenignTeardownError(error): boolean`
77
97
 
78
98
  Whether `error` is the benign teardown noise above (`Client destroyed`, matched on the message so it never swallows another package's `DestroyedError`). `destroy()` already drops it during its own teardown; export it so a consumer with its own `console.error` guard can drop the same line without reinventing the match.
@@ -102,7 +122,7 @@ const subSigner = createSessionSignerForAccount(session, {
102
122
  });
103
123
  ```
104
124
 
105
- > **Wire format note:** `@novasamatech/host-papp` 0.7 expects `productAccountId: [productId, derivationIndex]` in `SigningRawRequest`. Both functions above hide that tuple — pass an adapter for the default case or a named-fields object for the escape hatch.
125
+ > **Wire format note:** `@novasamatech/host-papp` expects `productAccountId: [productId, { tag: "Index", value: derivationIndex }]` in `SigningRawRequest`. Both functions above hide that tuple — pass an adapter for the default case or a named-fields object for the escape hatch.
106
126
 
107
127
  ### `renderQrCode(data, options?): Promise<string>`
108
128
 
@@ -251,9 +271,9 @@ For consumers moving from `@polkadot-apps/terminal` v0.2.0 / v0.3.0. Existing se
251
271
 
252
272
  ### Why the signer API changed
253
273
 
254
- `@novasamatech/host-papp` 0.7 replaced `SigningRawRequest.address` with `productAccountId: [productId, derivationIndex]`. The wire format requires both fields, so a session-only argument is no longer enough — the signer needs to know *which sub-account of which product is asking*. We split that into two functions to keep the common case ergonomic:
274
+ `@novasamatech/host-papp` 0.7 replaced `SigningRawRequest.address` with `productAccountId: [productId, derivationIndex]`, and 0.10 made the index a tagged `Index | Raw` selector. The wire format requires both fields, so a session-only argument is no longer enough — the signer needs to know *which sub-account of which product is asking*. We split that into two functions to keep the common case ergonomic:
255
275
 
256
- - `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, 0]`)
276
+ - `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, { tag: "Index", value: 0 }]`)
257
277
  - `createSessionSignerForAccount(session, { productId, derivationIndex })` for everything else
258
278
 
259
279
  The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal` no longer works against host-papp 0.7 regardless of which package you use.
@@ -1,9 +1,10 @@
1
+ import { createLogger } from '@parity/product-sdk-logger';
2
+ import { fromPromise } from 'neverthrow';
3
+ import { join } from 'path';
4
+ import { unlink, writeFile, readFile, mkdir } from 'fs/promises';
5
+ import { homedir } from 'os';
6
+
1
7
  // src/node-storage.ts
2
- import { createLogger } from "@parity/product-sdk-logger";
3
- import { fromPromise } from "neverthrow";
4
- import { join } from "path";
5
- import { readFile, writeFile, mkdir, unlink } from "fs/promises";
6
- import { homedir } from "os";
7
8
  var log = createLogger("terminal");
8
9
  var DEFAULT_STORAGE_DIR = join(homedir(), ".polkadot-apps");
9
10
  function sanitizeKey(appId, key) {
@@ -78,133 +79,7 @@ function createNodeStorageAdapter(appId, storageDir) {
78
79
  }
79
80
  };
80
81
  }
81
- if (void 0) {
82
- const { describe, test, expect, beforeEach, afterAll } = void 0;
83
- const { mkdtemp, rm } = await null;
84
- const { tmpdir } = await null;
85
- const { configure } = await null;
86
- let testDir;
87
- beforeEach(async () => {
88
- configure({ handler: () => {
89
- } });
90
- testDir = await mkdtemp(join(tmpdir(), "terminal-storage-test-"));
91
- });
92
- afterAll(async () => {
93
- try {
94
- await rm(testDir, { recursive: true });
95
- } catch {
96
- }
97
- });
98
- describe("createNodeStorageAdapter", () => {
99
- test("read returns null for missing key", async () => {
100
- const store = createNodeStorageAdapter("test", testDir);
101
- const result = await store.read("nonexistent");
102
- expect(result.isOk()).toBe(true);
103
- expect(result._unsafeUnwrap()).toBeNull();
104
- });
105
- test("write and read round-trip", async () => {
106
- const store = createNodeStorageAdapter("test", testDir);
107
- await store.write("key1", "hello");
108
- const result = await store.read("key1");
109
- expect(result._unsafeUnwrap()).toBe("hello");
110
- });
111
- test("write overwrites existing value", async () => {
112
- const store = createNodeStorageAdapter("test", testDir);
113
- await store.write("key1", "first");
114
- await store.write("key1", "second");
115
- const result = await store.read("key1");
116
- expect(result._unsafeUnwrap()).toBe("second");
117
- });
118
- test("clear removes key", async () => {
119
- const store = createNodeStorageAdapter("test", testDir);
120
- await store.write("key1", "value");
121
- await store.clear("key1");
122
- const result = await store.read("key1");
123
- expect(result._unsafeUnwrap()).toBeNull();
124
- });
125
- test("clear is safe for missing key", async () => {
126
- const store = createNodeStorageAdapter("test", testDir);
127
- const result = await store.clear("nonexistent");
128
- expect(result.isOk()).toBe(true);
129
- });
130
- test("different appIds are isolated", async () => {
131
- const storeA = createNodeStorageAdapter("app-a", testDir);
132
- const storeB = createNodeStorageAdapter("app-b", testDir);
133
- await storeA.write("key", "from-a");
134
- await storeB.write("key", "from-b");
135
- expect((await storeA.read("key"))._unsafeUnwrap()).toBe("from-a");
136
- expect((await storeB.read("key"))._unsafeUnwrap()).toBe("from-b");
137
- });
138
- test("subscribe notifies on write", async () => {
139
- const store = createNodeStorageAdapter("test", testDir);
140
- const values = [];
141
- store.subscribe("key1", (v) => values.push(v));
142
- await store.write("key1", "hello");
143
- expect(values).toEqual(["hello"]);
144
- });
145
- test("subscribe notifies on clear", async () => {
146
- const store = createNodeStorageAdapter("test", testDir);
147
- const values = [];
148
- await store.write("key1", "hello");
149
- store.subscribe("key1", (v) => values.push(v));
150
- await store.clear("key1");
151
- expect(values).toEqual([null]);
152
- });
153
- test("unsubscribe stops notifications", async () => {
154
- const store = createNodeStorageAdapter("test", testDir);
155
- const values = [];
156
- const unsub = store.subscribe("key1", (v) => values.push(v));
157
- await store.write("key1", "first");
158
- unsub();
159
- await store.write("key1", "second");
160
- expect(values).toEqual(["first"]);
161
- });
162
- test("subscriber errors do not break other subscribers", async () => {
163
- const store = createNodeStorageAdapter("test", testDir);
164
- const values = [];
165
- store.subscribe("key1", () => {
166
- throw new Error("boom");
167
- });
168
- store.subscribe("key1", (v) => {
169
- if (v) values.push(v);
170
- });
171
- await store.write("key1", "hello");
172
- expect(values).toEqual(["hello"]);
173
- });
174
- test("sanitizes special characters in keys", async () => {
175
- const store = createNodeStorageAdapter("test", testDir);
176
- await store.write("key/with:special chars!", "value");
177
- const result = await store.read("key/with:special chars!");
178
- expect(result._unsafeUnwrap()).toBe("value");
179
- });
180
- test("handles JSON values", async () => {
181
- const store = createNodeStorageAdapter("test", testDir);
182
- const obj = { name: "test", count: 42, nested: { ok: true } };
183
- await store.write("json", JSON.stringify(obj));
184
- const raw = (await store.read("json"))._unsafeUnwrap();
185
- expect(JSON.parse(raw)).toEqual(obj);
186
- });
187
- });
188
- describe("toError", () => {
189
- test("returns Error instances unchanged", () => {
190
- const original = new TypeError("boom");
191
- expect(toError(original)).toBe(original);
192
- });
193
- test("wraps non-Error string values", () => {
194
- const result = toError("primitive failure");
195
- expect(result).toBeInstanceOf(Error);
196
- expect(result.message).toBe("primitive failure");
197
- });
198
- test("wraps non-Error nullish values", () => {
199
- const result = toError(null);
200
- expect(result).toBeInstanceOf(Error);
201
- expect(result.message).toBe("null");
202
- });
203
- });
204
- }
205
82
 
206
- export {
207
- sanitizeKey,
208
- createNodeStorageAdapter
209
- };
83
+ export { createNodeStorageAdapter, sanitizeKey };
84
+ //# sourceMappingURL=chunk-GHOFKVUG.js.map
210
85
  //# sourceMappingURL=chunk-GHOFKVUG.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/node-storage.ts"],"sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\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":[]}
1
+ {"version":3,"sources":["../src/node-storage.ts"],"names":[],"mappings":"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","file":"chunk-GHOFKVUG.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\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"]}
@@ -0,0 +1,251 @@
1
+ import { createLogger } from '@parity/product-sdk-logger';
2
+ import { fromHex, toHex } from '@polkadot-api/utils';
3
+ import { readFile, mkdir, writeFile, rename } from 'fs/promises';
4
+ import { homedir } from 'os';
5
+ import { join, dirname } from 'path';
6
+ import { numberToBytesLE, bytesToNumberLE } from '@noble/curves/utils.js';
7
+ import { createDerive, sr25519Derive, sr25519 } from '@polkadot-labs/hdkd-helpers';
8
+ import { getPolkadotSigner } from 'polkadot-api/signer';
9
+
10
+ // src/host.ts
11
+ var log = createLogger("terminal");
12
+ var DEFAULT_STORAGE_DIR = join(homedir(), ".polkadot-apps");
13
+ var CACHE_FILE_MODE = 384;
14
+ function sanitizeAppId(appId) {
15
+ return appId.replace(/[^a-zA-Z0-9_.-]/g, "_");
16
+ }
17
+ function cachePath(appId, storageDir) {
18
+ return join(storageDir ?? DEFAULT_STORAGE_DIR, `${sanitizeAppId(appId)}_AllowanceKeys.json`);
19
+ }
20
+ function emptyCache() {
21
+ return { version: 2, entries: {} };
22
+ }
23
+ async function loadCache(appId, storageDir) {
24
+ const path = cachePath(appId, storageDir);
25
+ let raw;
26
+ try {
27
+ raw = await readFile(path, "utf-8");
28
+ } catch (e) {
29
+ if (e?.code === "ENOENT") return emptyCache();
30
+ throw e;
31
+ }
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ if (parsed?.version !== 2 || typeof parsed.entries !== "object" || parsed.entries === null) {
35
+ log.warn("allowance cache schema mismatch; starting fresh", { appId, path });
36
+ return emptyCache();
37
+ }
38
+ return parsed;
39
+ } catch (e) {
40
+ log.warn("allowance cache parse failed; starting fresh", { appId, path, error: String(e) });
41
+ return emptyCache();
42
+ }
43
+ }
44
+ async function saveCache(appId, cache, storageDir) {
45
+ const path = cachePath(appId, storageDir);
46
+ await mkdir(dirname(path), { recursive: true });
47
+ const tmp = `${path}.tmp`;
48
+ await writeFile(tmp, JSON.stringify(cache, null, 2), { mode: CACHE_FILE_MODE });
49
+ await rename(tmp, path);
50
+ }
51
+ function smartContractDest(dest) {
52
+ return dest.tag === "Index" ? `Index::${dest.value}` : `Raw::${toHex(dest.value)}`;
53
+ }
54
+ function cacheKey(resource) {
55
+ if (resource.tag === "SmartContractAllowance") {
56
+ return `${resource.tag}::${smartContractDest(resource.value)}`;
57
+ }
58
+ return resource.tag;
59
+ }
60
+ function pickOnExistingPolicy(cache, resources) {
61
+ if (resources.length === 0) return "Ignore";
62
+ const allSlotTableAndCached = resources.every((r) => {
63
+ if (r.tag !== "BulletInAllowance" && r.tag !== "StatementStoreAllowance") return false;
64
+ return Object.hasOwn(cache.entries, cacheKey(r));
65
+ });
66
+ return allSlotTableAndCached ? "Increase" : "Ignore";
67
+ }
68
+ function mergeOutcomes(cache, requested, outcomes) {
69
+ if (requested.length !== outcomes.length) {
70
+ throw new Error(
71
+ `mergeOutcomes: length mismatch \u2014 requested ${requested.length}, got ${outcomes.length}`
72
+ );
73
+ }
74
+ const entries = { ...cache.entries };
75
+ let mutated = false;
76
+ for (let i = 0; i < outcomes.length; i++) {
77
+ const req = requested[i];
78
+ const out = outcomes[i];
79
+ if (out.tag !== "Allocated") continue;
80
+ const inner = out.value;
81
+ if (inner.tag !== req.tag) {
82
+ throw new Error(
83
+ `mergeOutcomes: variant mismatch at index ${i} \u2014 requested ${req.tag}, got Allocated<${inner.tag}>`
84
+ );
85
+ }
86
+ const key = cacheKey(req);
87
+ switch (inner.tag) {
88
+ case "BulletInAllowance":
89
+ case "StatementStoreAllowance":
90
+ entries[key] = {
91
+ tag: inner.tag,
92
+ slotAccountKey: toHex(inner.value.slotAccountKey)
93
+ };
94
+ mutated = true;
95
+ break;
96
+ case "SmartContractAllowance":
97
+ if (req.tag === "SmartContractAllowance") {
98
+ entries[key] = {
99
+ tag: "SmartContractAllowance",
100
+ dest: smartContractDest(req.value)
101
+ };
102
+ mutated = true;
103
+ }
104
+ break;
105
+ case "AutoSigning":
106
+ entries[key] = {
107
+ tag: "AutoSigning",
108
+ ringVrfDomainEntropy: toHex(inner.value.ringVrfDomainEntropy),
109
+ productRootPrivateKey: toHex(inner.value.productRootPrivateKey)
110
+ };
111
+ mutated = true;
112
+ break;
113
+ }
114
+ }
115
+ return mutated ? { version: 2, entries } : cache;
116
+ }
117
+ function readCacheEntry(cache, resource) {
118
+ return cache.entries[cacheKey(resource)] ?? null;
119
+ }
120
+ var cacheLocks = /* @__PURE__ */ new Map();
121
+ function withCacheLock(appId, storageDir, fn) {
122
+ const key = cachePath(appId, storageDir);
123
+ const prev = cacheLocks.get(key) ?? Promise.resolve();
124
+ const next = prev.catch(() => {
125
+ }).then(fn);
126
+ cacheLocks.set(
127
+ key,
128
+ next.catch(() => {
129
+ })
130
+ );
131
+ return next;
132
+ }
133
+ function canonicalSr25519SecretToEd25519Bytes(secret) {
134
+ const ed25519Scalar = numberToBytesLE(bytesToNumberLE(secret.subarray(0, 32)) << 3n, 32);
135
+ const out = new Uint8Array(64);
136
+ out.set(ed25519Scalar, 0);
137
+ out.set(secret.subarray(32, 64), 32);
138
+ return out;
139
+ }
140
+ function buildKeypair(secret) {
141
+ if (secret.length === 32) {
142
+ const derive = createDerive({ seed: secret, curve: sr25519, derive: sr25519Derive });
143
+ const keypair = derive("");
144
+ return { publicKey: keypair.publicKey, sign: (data) => keypair.sign(data) };
145
+ }
146
+ if (secret.length === 64) {
147
+ const ed25519Secret = canonicalSr25519SecretToEd25519Bytes(secret);
148
+ return {
149
+ publicKey: sr25519.getPublicKey(ed25519Secret),
150
+ sign: (data) => sr25519.sign(data, ed25519Secret)
151
+ };
152
+ }
153
+ throw new Error(
154
+ `createSlotAccountSigner: unexpected slotAccountKey length ${secret.length}. Expected 32 (mini-secret) or 64 (expanded secret).`
155
+ );
156
+ }
157
+ function buildSignerFromEntry(entry) {
158
+ if (entry.tag !== "BulletInAllowance" && entry.tag !== "StatementStoreAllowance") {
159
+ throw new Error(
160
+ `createSlotAccountSigner: ${entry.tag} does not carry a slot account key. Slot-table signing is defined for BulletInAllowance and StatementStoreAllowance only.`
161
+ );
162
+ }
163
+ const secret = fromHex(entry.slotAccountKey);
164
+ const { publicKey, sign } = buildKeypair(secret);
165
+ return getPolkadotSigner(publicKey, "Sr25519", async (data) => sign(data));
166
+ }
167
+ async function createSlotAccountSigner(adapter, resource, productId) {
168
+ const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);
169
+ const entry = readCacheEntry(cache, resource);
170
+ if (!entry) return null;
171
+ return buildSignerFromEntry(entry);
172
+ }
173
+
174
+ // src/host.ts
175
+ var log2 = createLogger("terminal");
176
+ async function requestResourceAllocation(session, adapter, resources, options = {}) {
177
+ const productId = options.productId ?? adapter.appId;
178
+ return withCacheLock(productId, adapter.storageDir, async () => {
179
+ const cache = await loadCache(productId, adapter.storageDir);
180
+ const onExisting = options.onExisting ?? pickOnExistingPolicy(cache, resources);
181
+ log2.debug("requestResourceAllocation", {
182
+ productId,
183
+ resources: resources.map((r) => r.tag),
184
+ onExisting,
185
+ autoPolicy: options.onExisting === void 0
186
+ });
187
+ const outcomes = await sendResourceAllocation(session, productId, resources, onExisting);
188
+ const next = mergeOutcomes(cache, resources, outcomes);
189
+ if (next !== cache) {
190
+ await saveCache(productId, next, adapter.storageDir);
191
+ }
192
+ return outcomes;
193
+ });
194
+ }
195
+ async function sendResourceAllocation(session, productId, resources, onExisting) {
196
+ const result = await session.requestResourceAllocation({
197
+ callingProductId: productId,
198
+ resources,
199
+ onExisting
200
+ });
201
+ if (result.isErr()) {
202
+ throw new Error(`requestResourceAllocation failed: ${result.error.message}`);
203
+ }
204
+ return result.value;
205
+ }
206
+ async function getCachedAllocation(adapter, resource, productId) {
207
+ const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);
208
+ return readCacheEntry(cache, resource);
209
+ }
210
+ async function ensureSlotAccountSigner(session, adapter, resource, productId) {
211
+ const resolvedProductId = productId ?? adapter.appId;
212
+ return withCacheLock(resolvedProductId, adapter.storageDir, async () => {
213
+ const cache = await loadCache(resolvedProductId, adapter.storageDir);
214
+ const hit = readCacheEntry(cache, resource);
215
+ if (hit) return buildSignerFromEntry(hit);
216
+ log2.debug("ensureSlotAccountSigner: cache miss, allocating", { resource: resource.tag });
217
+ const onExisting = pickOnExistingPolicy(cache, [resource]);
218
+ const outcomes = await sendResourceAllocation(
219
+ session,
220
+ resolvedProductId,
221
+ [resource],
222
+ onExisting
223
+ );
224
+ const next = mergeOutcomes(cache, [resource], outcomes);
225
+ if (next !== cache) {
226
+ await saveCache(resolvedProductId, next, adapter.storageDir);
227
+ }
228
+ const outcome = outcomes[0];
229
+ if (!outcome) {
230
+ throw new Error(
231
+ `ensureSlotAccountSigner: protocol violation \u2014 empty outcomes for ${resource.tag}`
232
+ );
233
+ }
234
+ if (outcome.tag !== "Allocated") {
235
+ throw new Error(
236
+ `ensureSlotAccountSigner: allocation ${outcome.tag} for ${resource.tag}`
237
+ );
238
+ }
239
+ const freshEntry = readCacheEntry(next, resource);
240
+ if (!freshEntry) {
241
+ throw new Error(
242
+ `ensureSlotAccountSigner: allocation succeeded but cache lookup returned null for ${resource.tag}`
243
+ );
244
+ }
245
+ return buildSignerFromEntry(freshEntry);
246
+ });
247
+ }
248
+
249
+ export { createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, requestResourceAllocation, sendResourceAllocation };
250
+ //# sourceMappingURL=chunk-OKJQEB6P.js.map
251
+ //# sourceMappingURL=chunk-OKJQEB6P.js.map