@parity/product-sdk-keys 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,175 @@
1
+ import { generateMnemonic } from "@polkadot-labs/hdkd-helpers";
2
+ import type { KvStore } from "@parity/product-sdk-storage";
3
+
4
+ import { seedToAccount } from "./seed-to-account.js";
5
+ import type { SessionKeyInfo } from "./types.js";
6
+
7
+ /**
8
+ * Manages an sr25519 account derived from a BIP39 mnemonic.
9
+ *
10
+ * @param options.store - KvStore instance (from `@parity/product-sdk-storage`).
11
+ * Create with `createKvStore({ prefix: "session-key" })` for namespaced persistence.
12
+ * @param options.name - Identifies this session key. Defaults to `"default"`.
13
+ * Use different names to manage multiple independent session keys.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const store = await createKvStore({ prefix: "session-key" });
18
+ * const skm = new SessionKeyManager({ store });
19
+ * const key = await skm.getOrCreate();
20
+ * ```
21
+ */
22
+ export class SessionKeyManager {
23
+ private readonly name: string;
24
+ private readonly store: KvStore;
25
+
26
+ constructor(options: { store: KvStore; name?: string }) {
27
+ this.name = options.name ?? "default";
28
+ this.store = options.store;
29
+ }
30
+
31
+ /**
32
+ * Create a new session key from a fresh mnemonic.
33
+ * Persists the mnemonic to the store.
34
+ */
35
+ async create(): Promise<SessionKeyInfo> {
36
+ const mnemonic = generateMnemonic();
37
+ await this.store.set(this.name, mnemonic);
38
+ return { mnemonic, account: seedToAccount(mnemonic) };
39
+ }
40
+
41
+ /**
42
+ * Load an existing session key from the store.
43
+ * Returns null if no mnemonic is stored.
44
+ */
45
+ async get(): Promise<SessionKeyInfo | null> {
46
+ const mnemonic = await this.store.get(this.name);
47
+ if (!mnemonic) return null;
48
+ return { mnemonic, account: seedToAccount(mnemonic) };
49
+ }
50
+
51
+ /**
52
+ * Load existing or create a new session key.
53
+ */
54
+ async getOrCreate(): Promise<SessionKeyInfo> {
55
+ const existing = await this.get();
56
+ if (existing) return existing;
57
+ return this.create();
58
+ }
59
+
60
+ /**
61
+ * Derive a session key from an explicit mnemonic (no storage interaction).
62
+ */
63
+ fromMnemonic(mnemonic: string): SessionKeyInfo {
64
+ return { mnemonic, account: seedToAccount(mnemonic) };
65
+ }
66
+
67
+ /**
68
+ * Clear the stored mnemonic from the store.
69
+ */
70
+ async clear(): Promise<void> {
71
+ await this.store.remove(this.name);
72
+ }
73
+ }
74
+
75
+ if (import.meta.vitest) {
76
+ const { test, expect, describe } = import.meta.vitest;
77
+
78
+ const TEST_MNEMONIC =
79
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
80
+
81
+ function mockKvStore(): KvStore & { data: Map<string, string> } {
82
+ const data = new Map<string, string>();
83
+ return {
84
+ data,
85
+ async get(key) {
86
+ return data.get(key) ?? null;
87
+ },
88
+ async set(key, value) {
89
+ data.set(key, value);
90
+ },
91
+ async remove(key) {
92
+ data.delete(key);
93
+ },
94
+ async getJSON() {
95
+ return null;
96
+ },
97
+ async setJSON() {},
98
+ };
99
+ }
100
+
101
+ describe("SessionKeyManager", () => {
102
+ test("fromMnemonic produces deterministic results", () => {
103
+ const store = mockKvStore();
104
+ const skm = new SessionKeyManager({ store });
105
+ const a = skm.fromMnemonic(TEST_MNEMONIC);
106
+ const b = skm.fromMnemonic(TEST_MNEMONIC);
107
+ expect(a.mnemonic).toBe(TEST_MNEMONIC);
108
+ expect(a.account.ss58Address).toBe(b.account.ss58Address);
109
+ expect(a.account.h160Address).toBe(b.account.h160Address);
110
+ });
111
+
112
+ test("fromMnemonic throws on invalid mnemonic", () => {
113
+ const store = mockKvStore();
114
+ const skm = new SessionKeyManager({ store });
115
+ expect(() => skm.fromMnemonic("invalid words here")).toThrow("Invalid mnemonic phrase");
116
+ });
117
+
118
+ test("create and get round-trip", async () => {
119
+ const store = mockKvStore();
120
+ const skm = new SessionKeyManager({ store });
121
+ const info = await skm.create();
122
+ expect(info.mnemonic).toBeTruthy();
123
+ expect(info.account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);
124
+ expect(store.data.get("default")).toBe(info.mnemonic);
125
+
126
+ const loaded = await skm.get();
127
+ expect(loaded?.mnemonic).toBe(info.mnemonic);
128
+ });
129
+
130
+ test("get returns null when no key stored", async () => {
131
+ const store = mockKvStore();
132
+ const skm = new SessionKeyManager({ store });
133
+ expect(await skm.get()).toBeNull();
134
+ });
135
+
136
+ test("getOrCreate creates then returns cached", async () => {
137
+ const store = mockKvStore();
138
+ const skm = new SessionKeyManager({ store });
139
+ const created = await skm.getOrCreate();
140
+ expect(store.data.size).toBe(1);
141
+
142
+ const loaded = await skm.getOrCreate();
143
+ expect(loaded.mnemonic).toBe(created.mnemonic);
144
+ expect(loaded.account.ss58Address).toBe(created.account.ss58Address);
145
+ });
146
+
147
+ test("clear removes mnemonic from store", async () => {
148
+ const store = mockKvStore();
149
+ const skm = new SessionKeyManager({ store });
150
+ await skm.create();
151
+ expect(store.data.size).toBe(1);
152
+
153
+ await skm.clear();
154
+ expect(store.data.size).toBe(0);
155
+ expect(await skm.get()).toBeNull();
156
+ });
157
+
158
+ test("name separates storage keys", async () => {
159
+ const store = mockKvStore();
160
+ const main = new SessionKeyManager({ name: "main", store });
161
+ const burner = new SessionKeyManager({ name: "burner", store });
162
+
163
+ const mainInfo = await main.create();
164
+ const burnerInfo = await burner.create();
165
+
166
+ expect(store.data.get("main")).toBe(mainInfo.mnemonic);
167
+ expect(store.data.get("burner")).toBe(burnerInfo.mnemonic);
168
+ expect(mainInfo.account.ss58Address).not.toBe(burnerInfo.account.ss58Address);
169
+
170
+ await main.clear();
171
+ expect(store.data.has("main")).toBe(false);
172
+ expect(store.data.get("burner")).toBe(burnerInfo.mnemonic);
173
+ });
174
+ });
175
+ }
package/src/types.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { PolkadotSigner } from "polkadot-api";
2
+
3
+ import type { SS58String } from "@parity/product-sdk-address";
4
+
5
+ /** Derivation result for a Substrate/EVM account from seed material. */
6
+ export interface DerivedAccount {
7
+ /** Public key (32 bytes). Sr25519 or Ed25519 depending on key type. */
8
+ publicKey: Uint8Array;
9
+ /** SS58 address (generic prefix 42 by default) */
10
+ ss58Address: SS58String;
11
+ /** H160 EVM address derived via keccak256(publicKey) */
12
+ h160Address: `0x${string}`;
13
+ /** PolkadotSigner for signing extrinsics */
14
+ signer: PolkadotSigner;
15
+ }
16
+
17
+ /** NaCl encryption + signing keypairs derived from a master key. */
18
+ export interface DerivedKeypairs {
19
+ /** Curve25519 keypair for NaCl Box (asymmetric encryption) */
20
+ encryption: { publicKey: Uint8Array; secretKey: Uint8Array };
21
+ /** Ed25519 keypair for NaCl Sign (digital signatures) */
22
+ signing: { publicKey: Uint8Array; secretKey: Uint8Array };
23
+ }
24
+
25
+ /** Session key info returned by SessionKeyManager. */
26
+ export interface SessionKeyInfo {
27
+ /** The BIP39 mnemonic (the only thing that needs persisting) */
28
+ mnemonic: string;
29
+ /** The derived account info */
30
+ account: DerivedAccount;
31
+ }