@majikah/majik-message 0.3.6 → 0.3.7

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.
Files changed (66) hide show
  1. package/README.md +3 -3
  2. package/dist/core/client-state-manager.d.ts +105 -0
  3. package/dist/core/client-state-manager.js +250 -0
  4. package/dist/core/contacts/majik-contact-directory.d.ts +0 -5
  5. package/dist/core/contacts/majik-contact-directory.js +0 -12
  6. package/dist/core/contacts/majik-contact-groups.d.ts +1 -0
  7. package/dist/core/contacts/majik-contact-groups.js +5 -0
  8. package/dist/core/contacts/majik-contact-manager.d.ts +92 -184
  9. package/dist/core/contacts/majik-contact-manager.js +368 -288
  10. package/dist/core/crypto/keystore-manager.d.ts +166 -0
  11. package/dist/core/crypto/keystore-manager.js +371 -0
  12. package/dist/core/storage/chats/_types.d.ts +8 -0
  13. package/dist/core/storage/chats/_types.js +1 -0
  14. package/dist/core/storage/chats/adapter-idb.d.ts +3 -0
  15. package/dist/core/storage/chats/adapter-idb.js +5 -0
  16. package/dist/core/storage/chats/adapter-memory.d.ts +23 -0
  17. package/dist/core/storage/chats/adapter-memory.js +44 -0
  18. package/dist/core/storage/chats/adapter-sql.d.ts +17 -0
  19. package/dist/core/storage/chats/adapter-sql.js +84 -0
  20. package/dist/core/storage/client-state/_types.d.ts +37 -0
  21. package/dist/core/storage/client-state/_types.js +16 -0
  22. package/dist/core/storage/client-state/adapter-idb.d.ts +17 -0
  23. package/dist/core/storage/client-state/adapter-idb.js +19 -0
  24. package/dist/core/storage/client-state/adapter-memory.d.ts +20 -0
  25. package/dist/core/storage/client-state/adapter-memory.js +44 -0
  26. package/dist/core/storage/client-state/adapter-sql.d.ts +41 -0
  27. package/dist/core/storage/client-state/adapter-sql.js +104 -0
  28. package/dist/core/storage/contact-directory/contacts/_types.d.ts +3 -0
  29. package/dist/core/storage/contact-directory/contacts/_types.js +1 -0
  30. package/dist/core/storage/contact-directory/contacts/adapter-idb.d.ts +3 -0
  31. package/dist/core/storage/contact-directory/contacts/adapter-idb.js +5 -0
  32. package/dist/core/storage/contact-directory/contacts/adapter-memory.d.ts +14 -0
  33. package/dist/core/storage/contact-directory/contacts/adapter-memory.js +32 -0
  34. package/dist/core/storage/contact-directory/contacts/adapter-sql.d.ts +16 -0
  35. package/dist/core/storage/contact-directory/contacts/adapter-sql.js +73 -0
  36. package/dist/core/storage/contact-directory/groups/_types.d.ts +3 -0
  37. package/dist/core/storage/contact-directory/groups/_types.js +1 -0
  38. package/dist/core/storage/contact-directory/groups/adapter-idb.d.ts +3 -0
  39. package/dist/core/storage/contact-directory/groups/adapter-idb.js +5 -0
  40. package/dist/core/storage/contact-directory/groups/adapter-memory.d.ts +14 -0
  41. package/dist/core/storage/contact-directory/groups/adapter-memory.js +32 -0
  42. package/dist/core/storage/contact-directory/groups/adapter-sql.d.ts +16 -0
  43. package/dist/core/storage/contact-directory/groups/adapter-sql.js +71 -0
  44. package/dist/core/storage/idb-adapter.d.ts +21 -0
  45. package/dist/core/storage/idb-adapter.js +107 -0
  46. package/dist/core/storage/index.d.ts +24 -0
  47. package/dist/core/storage/index.js +19 -0
  48. package/dist/core/storage/keystore/_types.d.ts +3 -0
  49. package/dist/core/storage/keystore/_types.js +1 -0
  50. package/dist/core/storage/keystore/adapter-idb.d.ts +3 -0
  51. package/dist/core/storage/keystore/adapter-idb.js +5 -0
  52. package/dist/core/storage/keystore/adapter-memory.d.ts +14 -0
  53. package/dist/core/storage/keystore/adapter-memory.js +32 -0
  54. package/dist/core/storage/keystore/adapter-sql.d.ts +16 -0
  55. package/dist/core/storage/keystore/adapter-sql.js +69 -0
  56. package/dist/core/storage/sql-db-manager.d.ts +13 -0
  57. package/dist/core/storage/sql-db-manager.js +59 -0
  58. package/dist/core/storage/sql-schema.d.ts +10 -0
  59. package/dist/core/storage/sql-schema.js +108 -0
  60. package/dist/core/storage/storage-adapter.d.ts +14 -0
  61. package/dist/core/storage/storage-adapter.js +1 -0
  62. package/dist/index.d.ts +2 -4
  63. package/dist/index.js +2 -4
  64. package/dist/majik-message.d.ts +109 -174
  65. package/dist/majik-message.js +428 -677
  66. package/package.json +4 -5
@@ -0,0 +1,166 @@
1
+ /**
2
+ * MajikKeyManager.ts
3
+ *
4
+ * Adapter-backed manager for MajikKey accounts.
5
+ * Replaces the static MajikKeyStore class with an instanced manager that
6
+ * follows the same storage-adapter pattern as MajikContactManager.
7
+ *
8
+ * Adapters available:
9
+ * - InMemoryKeystoreAdapter (tests / SSR)
10
+ * - IDB_ADAPTER_KEYSTORE (browser default)
11
+ * - SQLiteKeystoreAdapter (Tauri / desktop)
12
+ */
13
+ import { MajikKey, MajikKeyJSON, SerializedIdentity } from "@majikah/majik-key";
14
+ import { MajikKeyStorageAdapter } from "../storage/keystore/_types";
15
+ export declare class MajikKeyManagerError extends Error {
16
+ cause?: unknown;
17
+ constructor(message: string, cause?: unknown);
18
+ }
19
+ interface LegacySerializedIdentity {
20
+ id: string;
21
+ publicKey: string;
22
+ fingerprint: string;
23
+ encryptedPrivateKey?: string;
24
+ salt?: string;
25
+ }
26
+ export declare class MajikKeyManager {
27
+ /**
28
+ * In-memory cache of all loaded MajikKey instances (locked or unlocked).
29
+ * Keyed by account ID. Unlocked state lives inside each MajikKey instance.
30
+ */
31
+ private readonly _cache;
32
+ private _adapter;
33
+ /**
34
+ * Optional callback invoked when UI needs to prompt for a passphrase.
35
+ */
36
+ onUnlockRequested?: (id: string) => string | Promise<string>;
37
+ constructor(adapter?: MajikKeyStorageAdapter);
38
+ get adapter(): MajikKeyStorageAdapter;
39
+ /**
40
+ * Swap the adapter at runtime. Does NOT migrate data automatically.
41
+ *
42
+ * Migration pattern:
43
+ * ```ts
44
+ * const snap = await manager.toJSON();
45
+ * manager.setAdapter(new SQLiteKeystoreAdapter(worker));
46
+ * await manager.hydrate(); // warms from new (empty) adapter
47
+ * await manager.bulkRestoreFromJSON(snap); // writes old data into new adapter
48
+ * ```
49
+ */
50
+ setAdapter(adapter: MajikKeyStorageAdapter): void;
51
+ /**
52
+ * Load all keys from the adapter into the in-memory cache.
53
+ * Call once after construction (or after swapping adapters).
54
+ */
55
+ hydrate(): Promise<void>;
56
+ toJSON(): Promise<MajikKeyJSON[]>;
57
+ /**
58
+ * Restore from a JSON snapshot into the current adapter.
59
+ * Writes all keys through to the adapter, then rehydrates the cache.
60
+ * Used after setAdapter() to migrate data into a new store.
61
+ */
62
+ bulkRestoreFromJSON(data: MajikKeyJSON[]): Promise<void>;
63
+ static fromJSON(data: MajikKeyJSON[], adapter?: MajikKeyStorageAdapter): Promise<MajikKeyManager>;
64
+ private _persist;
65
+ /**
66
+ * Store a MajikKey in the adapter and cache it in memory.
67
+ */
68
+ save(key: MajikKey): Promise<void>;
69
+ /**
70
+ * Load a MajikKey by ID. Checks memory cache first, then the adapter.
71
+ * Returns null if not found anywhere.
72
+ *
73
+ * Loaded keys are LOCKED. Call unlock(id, passphrase) to unlock.
74
+ */
75
+ load(id: string): Promise<MajikKey | null>;
76
+ getAccount(id: string): Promise<MajikKey>;
77
+ /**
78
+ * Load all MajikKeys (cache + adapter merged).
79
+ */
80
+ loadAll(): Promise<MajikKey[]>;
81
+ /**
82
+ * Delete an account from the adapter and memory cache.
83
+ */
84
+ delete(id: string): Promise<void>;
85
+ /**
86
+ * Check whether an account exists by ID or fingerprint.
87
+ * Checks memory cache first, then the adapter.
88
+ */
89
+ has(idOrFingerprint: string): Promise<boolean>;
90
+ /**
91
+ * Get a loaded MajikKey by ID or fingerprint (cache only — call load() first).
92
+ */
93
+ get(idOrFingerprint: string): MajikKey | undefined;
94
+ /**
95
+ * List all currently cached MajikKey instances (locked + unlocked).
96
+ */
97
+ list(): MajikKey[];
98
+ /**
99
+ * Unlock a stored MajikKey with the given passphrase.
100
+ * Dispatches to the correct KDF (PBKDF2 for legacy, Argon2id for new).
101
+ */
102
+ unlock(id: string, passphrase: string): Promise<MajikKey>;
103
+ /**
104
+ * Lock a MajikKey — clears private keys from memory.
105
+ */
106
+ lock(id: string): void;
107
+ /**
108
+ * Lock all loaded accounts.
109
+ */
110
+ lockAll(): void;
111
+ /**
112
+ * Get the private key of an unlocked account (by ID or fingerprint).
113
+ * Throws if not found or not unlocked — caller must call unlock() first.
114
+ */
115
+ getPrivateKey(idOrFingerprint: string): CryptoKey | {
116
+ raw: Uint8Array;
117
+ };
118
+ /**
119
+ * Get the ML-KEM secret key of an unlocked account.
120
+ * Returns undefined if the account has no ML-KEM keys (pre-migration).
121
+ */
122
+ getMlKemSecretKey(idOrFingerprint: string): Uint8Array | undefined;
123
+ /**
124
+ * Validate whether a passphrase can decrypt the stored account.
125
+ * Does NOT unlock or mutate any state.
126
+ */
127
+ isPassphraseValid(id: string, passphrase: string): Promise<boolean>;
128
+ /**
129
+ * Update passphrase — correctly upgrades KDF to Argon2id on re-encryption.
130
+ */
131
+ updatePassphrase(id: string, currentPassphrase: string, newPassphrase: string): Promise<void>;
132
+ /**
133
+ * Ensure an account is unlocked, prompting for passphrase if needed.
134
+ *
135
+ * Priority: promptFn arg → this.onUnlockRequested → window.prompt
136
+ */
137
+ ensureUnlocked(id: string, promptFn?: (id: string) => string | Promise<string>): Promise<CryptoKey | {
138
+ raw: Uint8Array;
139
+ }>;
140
+ static generateMnemonic(strength?: 128 | 256): string;
141
+ exportMnemonicBackup(id: string, mnemonic: string): Promise<string>;
142
+ importFromMnemonicBackup(backupBase64: string, mnemonic: string, passphrase: string, label?: string): Promise<MajikKey>;
143
+ /**
144
+ * Reconstruct a locked MajikKey from a legacy SerializedIdentity.
145
+ * The resulting key will have kdfVersion: PBKDF2 and hasMlKem: false.
146
+ */
147
+ static fromLegacySerializedIdentity(si: LegacySerializedIdentity): MajikKey;
148
+ /**
149
+ * Migrate a single legacy SerializedIdentity into the current adapter.
150
+ * Skips if a record with the same ID already exists.
151
+ */
152
+ migrate(identity: SerializedIdentity): Promise<{
153
+ success: boolean;
154
+ message: string;
155
+ }>;
156
+ /**
157
+ * Migrate all legacy SerializedIdentity records into the current adapter.
158
+ * Already-migrated accounts are skipped.
159
+ */
160
+ migrateAll(legacyIdentities: SerializedIdentity[]): Promise<{
161
+ migrated: number;
162
+ skipped: number;
163
+ }>;
164
+ private _findKey;
165
+ }
166
+ export {};
@@ -0,0 +1,371 @@
1
+ /**
2
+ * MajikKeyManager.ts
3
+ *
4
+ * Adapter-backed manager for MajikKey accounts.
5
+ * Replaces the static MajikKeyStore class with an instanced manager that
6
+ * follows the same storage-adapter pattern as MajikContactManager.
7
+ *
8
+ * Adapters available:
9
+ * - InMemoryKeystoreAdapter (tests / SSR)
10
+ * - IDB_ADAPTER_KEYSTORE (browser default)
11
+ * - SQLiteKeystoreAdapter (Tauri / desktop)
12
+ */
13
+ import { MajikKey } from "@majikah/majik-key";
14
+ import { KDF_VERSION } from "./constants";
15
+ import { InMemoryKeystoreAdapter } from "../storage/keystore/adapter-memory";
16
+ // ─── Error ────────────────────────────────────────────────────────────────────
17
+ export class MajikKeyManagerError extends Error {
18
+ cause;
19
+ constructor(message, cause) {
20
+ super(message);
21
+ this.name = "MajikKeyManagerError";
22
+ this.cause = cause;
23
+ }
24
+ }
25
+ // ─── MajikKeyManager ──────────────────────────────────────────────────────────
26
+ export class MajikKeyManager {
27
+ /**
28
+ * In-memory cache of all loaded MajikKey instances (locked or unlocked).
29
+ * Keyed by account ID. Unlocked state lives inside each MajikKey instance.
30
+ */
31
+ _cache = new Map();
32
+ _adapter;
33
+ /**
34
+ * Optional callback invoked when UI needs to prompt for a passphrase.
35
+ */
36
+ onUnlockRequested;
37
+ constructor(adapter) {
38
+ this._adapter = adapter ?? new InMemoryKeystoreAdapter();
39
+ }
40
+ // ── Adapter management ────────────────────────────────────────────────────
41
+ get adapter() {
42
+ return this._adapter;
43
+ }
44
+ /**
45
+ * Swap the adapter at runtime. Does NOT migrate data automatically.
46
+ *
47
+ * Migration pattern:
48
+ * ```ts
49
+ * const snap = await manager.toJSON();
50
+ * manager.setAdapter(new SQLiteKeystoreAdapter(worker));
51
+ * await manager.hydrate(); // warms from new (empty) adapter
52
+ * await manager.bulkRestoreFromJSON(snap); // writes old data into new adapter
53
+ * ```
54
+ */
55
+ setAdapter(adapter) {
56
+ this._adapter = adapter;
57
+ }
58
+ // ── Hydration ─────────────────────────────────────────────────────────────
59
+ /**
60
+ * Load all keys from the adapter into the in-memory cache.
61
+ * Call once after construction (or after swapping adapters).
62
+ */
63
+ async hydrate() {
64
+ this._cache.clear();
65
+ const all = await this._adapter.list();
66
+ for (const json of all) {
67
+ try {
68
+ const key = MajikKey.fromJSON(json);
69
+ this._cache.set(key.id, key);
70
+ }
71
+ catch (err) {
72
+ console.warn(`MajikKeyManager.hydrate: skipping malformed key "${json?.id}":`, err);
73
+ }
74
+ }
75
+ }
76
+ // ── Serialization ─────────────────────────────────────────────────────────
77
+ async toJSON() {
78
+ return this._adapter.list();
79
+ }
80
+ /**
81
+ * Restore from a JSON snapshot into the current adapter.
82
+ * Writes all keys through to the adapter, then rehydrates the cache.
83
+ * Used after setAdapter() to migrate data into a new store.
84
+ */
85
+ async bulkRestoreFromJSON(data) {
86
+ if (!Array.isArray(data)) {
87
+ throw new MajikKeyManagerError("bulkRestoreFromJSON: expected MajikKeyJSON[]");
88
+ }
89
+ await this._adapter.bulkSave(data);
90
+ await this.hydrate();
91
+ }
92
+ static async fromJSON(data, adapter) {
93
+ const manager = new MajikKeyManager(adapter);
94
+ await manager.bulkRestoreFromJSON(data);
95
+ return manager;
96
+ }
97
+ // ── Write-through helper ──────────────────────────────────────────────────
98
+ async _persist(key) {
99
+ await this._adapter.save(key.toJSON());
100
+ }
101
+ // ── Core CRUD ─────────────────────────────────────────────────────────────
102
+ /**
103
+ * Store a MajikKey in the adapter and cache it in memory.
104
+ */
105
+ async save(key) {
106
+ await this._persist(key);
107
+ this._cache.set(key.id, key);
108
+ }
109
+ /**
110
+ * Load a MajikKey by ID. Checks memory cache first, then the adapter.
111
+ * Returns null if not found anywhere.
112
+ *
113
+ * Loaded keys are LOCKED. Call unlock(id, passphrase) to unlock.
114
+ */
115
+ async load(id) {
116
+ const cached = this._cache.get(id);
117
+ if (cached)
118
+ return cached;
119
+ const json = await this._adapter.getById(id);
120
+ if (!json)
121
+ return null;
122
+ const key = MajikKey.fromJSON(json);
123
+ this._cache.set(key.id, key);
124
+ return key;
125
+ }
126
+ async getAccount(id) {
127
+ const key = await this.load(id);
128
+ if (!key)
129
+ throw new MajikKeyManagerError(`Account not found: ${id}`);
130
+ return key;
131
+ }
132
+ /**
133
+ * Load all MajikKeys (cache + adapter merged).
134
+ */
135
+ async loadAll() {
136
+ const all = await this._adapter.list();
137
+ for (const json of all) {
138
+ if (!this._cache.has(json.id)) {
139
+ try {
140
+ const key = MajikKey.fromJSON(json);
141
+ this._cache.set(key.id, key);
142
+ }
143
+ catch (err) {
144
+ console.warn(`MajikKeyManager.loadAll: skipping malformed key "${json?.id}":`, err);
145
+ }
146
+ }
147
+ }
148
+ return [...this._cache.values()];
149
+ }
150
+ /**
151
+ * Delete an account from the adapter and memory cache.
152
+ */
153
+ async delete(id) {
154
+ await this._adapter.remove(id);
155
+ this._cache.delete(id);
156
+ }
157
+ /**
158
+ * Check whether an account exists by ID or fingerprint.
159
+ * Checks memory cache first, then the adapter.
160
+ */
161
+ async has(idOrFingerprint) {
162
+ if (this._findKey(idOrFingerprint))
163
+ return true;
164
+ const all = await this._adapter.list();
165
+ return all.some((j) => j.id === idOrFingerprint || j.fingerprint === idOrFingerprint);
166
+ }
167
+ /**
168
+ * Get a loaded MajikKey by ID or fingerprint (cache only — call load() first).
169
+ */
170
+ get(idOrFingerprint) {
171
+ return this._findKey(idOrFingerprint);
172
+ }
173
+ /**
174
+ * List all currently cached MajikKey instances (locked + unlocked).
175
+ */
176
+ list() {
177
+ return [...this._cache.values()];
178
+ }
179
+ // ── Unlock / Lock ─────────────────────────────────────────────────────────
180
+ /**
181
+ * Unlock a stored MajikKey with the given passphrase.
182
+ * Dispatches to the correct KDF (PBKDF2 for legacy, Argon2id for new).
183
+ */
184
+ async unlock(id, passphrase) {
185
+ const key = await this.load(id);
186
+ if (!key)
187
+ throw new MajikKeyManagerError(`Account not found: ${id}`);
188
+ if (key.isUnlocked)
189
+ return key;
190
+ await key.unlock(passphrase);
191
+ this._cache.set(id, key);
192
+ return key;
193
+ }
194
+ /**
195
+ * Lock a MajikKey — clears private keys from memory.
196
+ */
197
+ lock(id) {
198
+ this._cache.get(id)?.lock();
199
+ }
200
+ /**
201
+ * Lock all loaded accounts.
202
+ */
203
+ lockAll() {
204
+ for (const key of this._cache.values())
205
+ key.lock();
206
+ }
207
+ // ── Key material access ───────────────────────────────────────────────────
208
+ /**
209
+ * Get the private key of an unlocked account (by ID or fingerprint).
210
+ * Throws if not found or not unlocked — caller must call unlock() first.
211
+ */
212
+ getPrivateKey(idOrFingerprint) {
213
+ const key = this._findKey(idOrFingerprint);
214
+ if (key?.isUnlocked)
215
+ return key.getPrivateKey();
216
+ throw new MajikKeyManagerError(`Account "${idOrFingerprint}" must be unlocked first via unlock()`);
217
+ }
218
+ /**
219
+ * Get the ML-KEM secret key of an unlocked account.
220
+ * Returns undefined if the account has no ML-KEM keys (pre-migration).
221
+ */
222
+ getMlKemSecretKey(idOrFingerprint) {
223
+ const key = this._findKey(idOrFingerprint);
224
+ if (!key?.isUnlocked)
225
+ return undefined;
226
+ try {
227
+ return key.getMlKemSecretKey();
228
+ }
229
+ catch {
230
+ return undefined;
231
+ }
232
+ }
233
+ // ── Passphrase management ─────────────────────────────────────────────────
234
+ /**
235
+ * Validate whether a passphrase can decrypt the stored account.
236
+ * Does NOT unlock or mutate any state.
237
+ */
238
+ async isPassphraseValid(id, passphrase) {
239
+ const key = await this.load(id);
240
+ if (!key)
241
+ return false;
242
+ return key.verify(passphrase);
243
+ }
244
+ /**
245
+ * Update passphrase — correctly upgrades KDF to Argon2id on re-encryption.
246
+ */
247
+ async updatePassphrase(id, currentPassphrase, newPassphrase) {
248
+ const key = await this.load(id);
249
+ if (!key)
250
+ throw new MajikKeyManagerError(`Account not found: ${id}`);
251
+ await key.updatePassphrase(currentPassphrase, newPassphrase);
252
+ await this._persist(key);
253
+ }
254
+ // ── ensureUnlocked ────────────────────────────────────────────────────────
255
+ /**
256
+ * Ensure an account is unlocked, prompting for passphrase if needed.
257
+ *
258
+ * Priority: promptFn arg → this.onUnlockRequested → window.prompt
259
+ */
260
+ async ensureUnlocked(id, promptFn) {
261
+ try {
262
+ return this.getPrivateKey(id);
263
+ }
264
+ catch {
265
+ /* not yet unlocked */
266
+ }
267
+ let passphrase = null;
268
+ if (promptFn) {
269
+ const res = promptFn(id);
270
+ passphrase = typeof res === "string" ? res : await res;
271
+ }
272
+ else if (this.onUnlockRequested) {
273
+ const res = this.onUnlockRequested(id);
274
+ passphrase = typeof res === "string" ? res : await res;
275
+ }
276
+ else if (typeof window !== "undefined" && window.prompt) {
277
+ passphrase = window.prompt("Enter passphrase to unlock account:", "");
278
+ }
279
+ if (!passphrase)
280
+ throw new MajikKeyManagerError("Unlock cancelled");
281
+ await this.unlock(id, passphrase);
282
+ return this.getPrivateKey(id);
283
+ }
284
+ // ── Mnemonic / backup helpers ─────────────────────────────────────────────
285
+ static generateMnemonic(strength = 128) {
286
+ return MajikKey.generateMnemonic(strength);
287
+ }
288
+ async exportMnemonicBackup(id, mnemonic) {
289
+ const key = this._cache.get(id);
290
+ if (!key)
291
+ throw new MajikKeyManagerError(`Account not found: ${id}`);
292
+ if (key.isLocked)
293
+ throw new MajikKeyManagerError("Account must be unlocked to export backup");
294
+ return key.exportMnemonicBackup(mnemonic);
295
+ }
296
+ async importFromMnemonicBackup(backupBase64, mnemonic, passphrase, label) {
297
+ const key = await MajikKey.importFromMnemonicBackup(backupBase64, mnemonic, passphrase, label);
298
+ await this.save(key);
299
+ return key;
300
+ }
301
+ // ── Legacy migration ──────────────────────────────────────────────────────
302
+ /**
303
+ * Reconstruct a locked MajikKey from a legacy SerializedIdentity.
304
+ * The resulting key will have kdfVersion: PBKDF2 and hasMlKem: false.
305
+ */
306
+ static fromLegacySerializedIdentity(si) {
307
+ if (!si.id || !si.publicKey || !si.fingerprint) {
308
+ throw new MajikKeyManagerError("Invalid legacy SerializedIdentity: missing required fields");
309
+ }
310
+ const json = {
311
+ id: si.id,
312
+ label: "",
313
+ publicKey: si.publicKey,
314
+ fingerprint: si.fingerprint,
315
+ encryptedPrivateKey: si.encryptedPrivateKey || "",
316
+ salt: si.salt || "",
317
+ backup: "_LEGACY",
318
+ timestamp: new Date().toISOString(),
319
+ kdfVersion: KDF_VERSION.PBKDF2,
320
+ };
321
+ return MajikKey.fromJSON(json);
322
+ }
323
+ /**
324
+ * Migrate a single legacy SerializedIdentity into the current adapter.
325
+ * Skips if a record with the same ID already exists.
326
+ */
327
+ async migrate(identity) {
328
+ try {
329
+ if (await this._adapter.exists(identity.id)) {
330
+ return { success: true, message: `Already migrated: ${identity.id}` };
331
+ }
332
+ const key = MajikKeyManager.fromLegacySerializedIdentity(identity);
333
+ await this.save(key);
334
+ return { success: true, message: `Successfully migrated ${identity.id}` };
335
+ }
336
+ catch (err) {
337
+ console.warn(`Failed to migrate legacy account ${identity.id}:`, err);
338
+ return {
339
+ success: false,
340
+ message: `Failed to migrate legacy account ${identity.id}: ${err}`,
341
+ };
342
+ }
343
+ }
344
+ /**
345
+ * Migrate all legacy SerializedIdentity records into the current adapter.
346
+ * Already-migrated accounts are skipped.
347
+ */
348
+ async migrateAll(legacyIdentities) {
349
+ let migrated = 0;
350
+ let skipped = 0;
351
+ for (const identity of legacyIdentities) {
352
+ const result = await this.migrate(identity);
353
+ if (result.success)
354
+ migrated++;
355
+ else
356
+ skipped++;
357
+ }
358
+ return { migrated, skipped };
359
+ }
360
+ // ── Private helpers ───────────────────────────────────────────────────────
361
+ _findKey(idOrFingerprint) {
362
+ const byId = this._cache.get(idOrFingerprint);
363
+ if (byId)
364
+ return byId;
365
+ for (const key of this._cache.values()) {
366
+ if (key.fingerprint === idOrFingerprint)
367
+ return key;
368
+ }
369
+ return undefined;
370
+ }
371
+ }
@@ -0,0 +1,8 @@
1
+ import { MajikMessageChatJSON } from "../../database/chat/types";
2
+ import { MajikStorageAdapter } from "../storage-adapter";
3
+ /**
4
+ * All methods are async — consistent regardless of the backing store.
5
+ * The adapter works only with serialized JSON; it never sees MajikMessageChat
6
+ * instances directly. Deserialization happens in MajikInvoiceManager.
7
+ */
8
+ export type MajikMessageChatStorageAdapter = MajikStorageAdapter<MajikMessageChatJSON>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { MajikMessageChatJSON } from "../../database/chat/types";
2
+ import { IDBGenericAdapter } from "../idb-adapter";
3
+ export declare const IDB_ADAPTER_INVOICE: IDBGenericAdapter<MajikMessageChatJSON>;
@@ -0,0 +1,5 @@
1
+ import { IDBGenericAdapter } from "../idb-adapter";
2
+ const IDB_DB_NAME = "majik-message-chats";
3
+ const IDB_STORE_NAME = "chats";
4
+ const IDB_VERSION = 1;
5
+ export const IDB_ADAPTER_INVOICE = new IDBGenericAdapter(IDB_DB_NAME, IDB_STORE_NAME, IDB_VERSION);
@@ -0,0 +1,23 @@
1
+ import { MajikMessageChatJSON } from "../../database/chat/types";
2
+ import { MajikMessageChatStorageAdapter } from "./_types";
3
+ /**
4
+ * In-memory adapter backed by a plain Map.
5
+ * Default adapter when no other is provided.
6
+ * Does not persist across page loads or restarts.
7
+ *
8
+ * @example
9
+ * const manager = new MajikInvoiceManager();
10
+ * // InMemoryInvoiceAdapter is used automatically
11
+ */
12
+ export declare class InMemoryInvoiceAdapter implements MajikMessageChatStorageAdapter {
13
+ private _store;
14
+ save(invoice: MajikMessageChatJSON): Promise<void>;
15
+ getById(id: string): Promise<MajikMessageChatJSON | null>;
16
+ list(): Promise<MajikMessageChatJSON[]>;
17
+ remove(id: string): Promise<boolean>;
18
+ clear(): Promise<void>;
19
+ count(): Promise<number>;
20
+ exists(id: string): Promise<boolean>;
21
+ bulkSave(invoices: MajikMessageChatJSON[]): Promise<void>;
22
+ bulkRemove(ids: string[]): Promise<void>;
23
+ }
@@ -0,0 +1,44 @@
1
+ // ---------------------------------------------------------------------------
2
+ // InMemoryInvoiceAdapter — default, zero-config, non-persistent
3
+ // ---------------------------------------------------------------------------
4
+ /**
5
+ * In-memory adapter backed by a plain Map.
6
+ * Default adapter when no other is provided.
7
+ * Does not persist across page loads or restarts.
8
+ *
9
+ * @example
10
+ * const manager = new MajikInvoiceManager();
11
+ * // InMemoryInvoiceAdapter is used automatically
12
+ */
13
+ export class InMemoryInvoiceAdapter {
14
+ _store = new Map();
15
+ async save(invoice) {
16
+ this._store.set(invoice.id, invoice);
17
+ }
18
+ async getById(id) {
19
+ return this._store.get(id) ?? null;
20
+ }
21
+ async list() {
22
+ return Array.from(this._store.values());
23
+ }
24
+ async remove(id) {
25
+ return this._store.delete(id);
26
+ }
27
+ async clear() {
28
+ this._store.clear();
29
+ }
30
+ async count() {
31
+ return this._store.size;
32
+ }
33
+ async exists(id) {
34
+ return this._store.has(id);
35
+ }
36
+ async bulkSave(invoices) {
37
+ for (const inv of invoices)
38
+ this._store.set(inv.id, inv);
39
+ }
40
+ async bulkRemove(ids) {
41
+ for (const id of ids)
42
+ this._store.delete(id);
43
+ }
44
+ }
@@ -0,0 +1,17 @@
1
+ import { MajikMessageChatJSON } from "../../database/chat/types";
2
+ import { SQLiteDatabase } from "../sql-db-manager";
3
+ import { StorageSource } from "../storage-adapter";
4
+ import { MajikMessageChatStorageAdapter } from "./_types";
5
+ export declare class SQLiteInvoiceAdapter implements MajikMessageChatStorageAdapter {
6
+ private db;
7
+ constructor(db: SQLiteDatabase);
8
+ save(message: MajikMessageChatJSON, source?: StorageSource): Promise<void>;
9
+ getById(id: string, source?: StorageSource): Promise<MajikMessageChatJSON | null>;
10
+ list(source?: StorageSource): Promise<MajikMessageChatJSON[]>;
11
+ remove(id: string, source?: StorageSource): Promise<boolean>;
12
+ clear(source?: StorageSource): Promise<void>;
13
+ count(source?: StorageSource): Promise<number>;
14
+ exists(id: string, source?: StorageSource): Promise<boolean>;
15
+ bulkSave(messages: MajikMessageChatJSON[], source?: StorageSource): Promise<void>;
16
+ bulkRemove(ids: string[], source?: StorageSource): Promise<void>;
17
+ }