@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
@@ -1,39 +1,48 @@
1
1
  // MajikMessage.ts
2
- import { MajikContact, } from "@majikah/majik-contact";
3
- import { KEY_ALGO } from "./core/crypto/constants";
4
2
  import { MessageEnvelope } from "./core/messages/message-envelope";
5
3
  import { EnvelopeCache, } from "./core/messages/envelope-cache";
6
- import { MajikKeyStore } from "./core/crypto/keystore";
7
- import { arrayBufferToBase64, arrayToBase64, base64ToArrayBuffer, base64ToUint8Array, } from "./core/utils/utilities";
8
- import { autoSaveMajikFileData, loadSavedMajikFileData, } from "./core/utils/majik-file-utils";
4
+ import { arrayToBase64, base64ToUint8Array } from "./core/utils/utilities";
9
5
  import { randomBytes } from "@stablelib/random";
10
- import { clearAllBlobs, idbLoadBlob, idbSaveBlob, } from "./core/utils/idb-majik-system";
11
6
  import { MajikMessageChat } from "./core/database/chat/majik-message-chat";
12
7
  import { MajikKey } from "@majikah/majik-key";
13
8
  import { MajikEnvelope, } from "@majikah/majik-envelope";
14
9
  import { MajikFile, MajikFileError, } from "@majikah/majik-file";
15
10
  import { MajikSignature, } from "@majikah/majik-signature";
16
- import { gzipSync, gunzipSync } from "fflate";
17
- import { MajikContactManager } from "./core/contacts/majik-contact-manager";
11
+ import { MajikContactManager, } from "./core/contacts/majik-contact-manager";
12
+ import { InMemoryClientStateAdapter, InMemoryKeystoreAdapter, } from "./core/storage";
13
+ import { MajikKeyManager } from "./core/crypto/keystore-manager";
14
+ import { ClientStateManager } from "./core/client-state-manager";
18
15
  // ─── MajikMessage ─────────────────────────────────────────────────────────────
19
16
  export class MajikMessage {
20
- userProfile = "default";
21
- id;
22
- contacts;
17
+ _id;
18
+ _db;
19
+ _contacts;
20
+ _keys;
21
+ _state;
23
22
  envelopeCache;
24
- listeners = new Map();
25
- ownAccounts = new Map();
26
- ownAccountsOrder = [];
27
- autosaveTimer = null;
28
- autosaveIntervalId = null;
29
- autosaveIntervalMs = 15_000;
30
- autosaveDebounceMs = 500;
31
- constructor(config, id, userProfile = "default") {
32
- this.userProfile = userProfile || "default";
33
- this.id = id || arrayToBase64(randomBytes(32));
34
- this.contacts = config.contactManager ?? new MajikContactManager();
35
- this.envelopeCache =
36
- config.envelopeCache || new EnvelopeCache(undefined, userProfile);
23
+ _listeners = new Map();
24
+ /** MajikContact instances for accounts this client owns. */
25
+ _ownAccounts = new Map();
26
+ /**
27
+ * Ordered list of own account IDs — head is the active account.
28
+ * Source of truth is ClientStateManager; this array is the in-memory
29
+ * working copy kept in sync on every mutation.
30
+ */
31
+ _ownAccountsOrder = [];
32
+ _autosaveOrderTimer = null;
33
+ constructor(config, id) {
34
+ this._id = id || arrayToBase64(randomBytes(32));
35
+ this._db = config.dbSQL || null;
36
+ this.envelopeCache = config.envelopeCache || new EnvelopeCache(undefined);
37
+ this._contacts =
38
+ config.contactManager ??
39
+ new MajikContactManager(undefined, undefined, config.adapters?.contacts);
40
+ this._keys =
41
+ config.keyManager ??
42
+ new MajikKeyManager(config.adapters?.keys ?? new InMemoryKeystoreAdapter());
43
+ this._state =
44
+ config.clientStateManager ??
45
+ new ClientStateManager(config.adapters?.clientState ?? new InMemoryClientStateAdapter());
37
46
  const events = [
38
47
  "message",
39
48
  "envelope",
@@ -48,16 +57,105 @@ export class MajikMessage {
48
57
  "contact-group-change",
49
58
  "active-account-change",
50
59
  ];
51
- events.forEach((e) => this.listeners.set(e, []));
52
- this.attachAutosaveHandlers();
60
+ events.forEach((e) => this._listeners.set(e, []));
61
+ }
62
+ /** Expose the key manager so callers can share it with other clients. */
63
+ get keyManager() {
64
+ return this._keys;
65
+ }
66
+ /** Expose the client state manager for direct access if needed. */
67
+ get stateManager() {
68
+ return this._state;
69
+ }
70
+ // ── Hydration ─────────────────────────────────────────────────────────────
71
+ /**
72
+ * Load all domains from their adapters and restore client state.
73
+ * Call once on startup.
74
+ *
75
+ * ```ts
76
+ * const client = new MajikBuwizClient({ adapters: { keys: idbAdapter, ... } });
77
+ * await client.hydrate();
78
+ * ```
79
+ */
80
+ async hydrate() {
81
+ // 1. Keys — load into manager cache
82
+ await this._keys.hydrate();
83
+ // 2. Contacts + groups
84
+ await this._contacts.hydrate();
85
+ // 4. Client state — account order, invoice defaults, etc.
86
+ await this._state.hydrate();
87
+ // 5. Own accounts — rebuild from keys loaded in step 1
88
+ await this._hydrateOwnAccounts();
89
+ // 6. Account order — restore from state manager, prune stale IDs
90
+ await this._restoreAccountOrder();
91
+ }
92
+ // ── Private hydration helpers ─────────────────────────────────────────────
93
+ async _hydrateOwnAccounts() {
94
+ const keys = this._keys.list();
95
+ for (const key of keys) {
96
+ if (!this._ownAccounts.has(key.id)) {
97
+ try {
98
+ const contact = key.toContact();
99
+ if (!this._contacts.hasContact(contact.id)) {
100
+ await this._contacts.addContact(contact);
101
+ }
102
+ this._ownAccounts.set(key.id, contact);
103
+ if (!this._ownAccountsOrder.includes(key.id)) {
104
+ this._ownAccountsOrder.push(key.id);
105
+ }
106
+ }
107
+ catch (err) {
108
+ console.warn(`MajikBuwizClient: failed to hydrate own account "${key.id}":`, err);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ async _restoreAccountOrder() {
114
+ try {
115
+ const saved = await this._state.getAccountOrder();
116
+ if (saved) {
117
+ // Prune IDs that no longer exist, then append any new ones at the tail
118
+ const valid = saved.filter((id) => this._ownAccounts.has(id));
119
+ const appended = this._ownAccountsOrder.filter((id) => !valid.includes(id));
120
+ this._ownAccountsOrder = [...valid, ...appended];
121
+ }
122
+ }
123
+ catch {
124
+ // Non-fatal — order defaults to insertion order from _hydrateOwnAccounts
125
+ }
126
+ }
127
+ _scheduleOrderSave() {
128
+ if (this._autosaveOrderTimer !== null) {
129
+ window.clearTimeout(this._autosaveOrderTimer);
130
+ }
131
+ this._autosaveOrderTimer = window.setTimeout(() => {
132
+ void this._persistAccountOrder();
133
+ this._autosaveOrderTimer = null;
134
+ }, 300);
135
+ }
136
+ async _persistAccountOrder() {
137
+ try {
138
+ await this._state.setAccountOrder(this._ownAccountsOrder);
139
+ }
140
+ catch (err) {
141
+ console.warn("MajikBuwizClient: failed to persist account order:", err);
142
+ }
143
+ }
144
+ /**
145
+ * Construct a client and immediately hydrate it.
146
+ */
147
+ static async create(config = {}) {
148
+ const client = new this(config);
149
+ await client.hydrate();
150
+ return client;
53
151
  }
54
152
  /**
55
153
  * Resolve a list of account/contact IDs into MajikRecipient objects.
56
- * Each recipient needs their ML-KEM public key from MajikKeyStore.
154
+ * Each recipient needs their ML-KEM public key from this.keyManager.
57
155
  */
58
156
  async _resolveRecipientsByPublicKey(publicKeys) {
59
157
  return Promise.all(publicKeys.map(async (pkey) => {
60
- const contact = await this.contacts.getContactByPublicKeyBase64(pkey);
158
+ const contact = await this._contacts.getContactByPublicKeyBase64(pkey);
61
159
  if (!contact)
62
160
  throw new Error(`No contact found for public key "${pkey}"`);
63
161
  const mlPubKey = base64ToUint8Array(contact.mlKey);
@@ -76,8 +174,8 @@ export class MajikMessage {
76
174
  * Ensures the account is unlocked and has ML-KEM keys.
77
175
  */
78
176
  async _resolveIdentity(id, promptFn) {
79
- await MajikKeyStore.ensureUnlocked(id, promptFn);
80
- const key = MajikKeyStore.get(id);
177
+ await this.keyManager.ensureUnlocked(id, promptFn);
178
+ const key = this.keyManager.get(id);
81
179
  if (!key)
82
180
  throw new Error(`Account not found: ${id}`);
83
181
  if (!key.hasMlKem) {
@@ -102,8 +200,8 @@ export class MajikMessage {
102
200
  const id = accountId ?? this.getActiveAccount()?.id;
103
201
  if (!id)
104
202
  throw new Error("No active account — call setActiveAccount() first");
105
- await MajikKeyStore.ensureUnlocked(id);
106
- const key = MajikKeyStore.get(id);
203
+ await this.keyManager.ensureUnlocked(id);
204
+ const key = this.keyManager.get(id);
107
205
  if (!key)
108
206
  throw new Error(`Account not found in keystore: "${id}"`);
109
207
  if (!key.hasMlKem) {
@@ -127,7 +225,7 @@ export class MajikMessage {
127
225
  */
128
226
  async _resolveFileRecipientsByPublicKey(publicKeys) {
129
227
  return Promise.all(publicKeys.map(async (pkey) => {
130
- const contact = await this.contacts.getContactByPublicKeyBase64(pkey);
228
+ const contact = await this._contacts.getContactByPublicKeyBase64(pkey);
131
229
  if (!contact)
132
230
  throw new Error(`No contact found for public key "${pkey}"`);
133
231
  const mlPubKey = base64ToUint8Array(contact.mlKey);
@@ -148,69 +246,113 @@ export class MajikMessage {
148
246
  ? window.location.hostname
149
247
  : "extension";
150
248
  }
151
- // ── Account Management ────────────────────────────────────────────────────
152
- generateMnemonic() {
153
- return MajikKeyStore.generateMnemonic();
249
+ // ==========================================================================
250
+ // ── ACCOUNT MANAGEMENT ────────────────────────────────────────────────────
251
+ // ==========================================================================
252
+ generateMnemonic(strength = 128) {
253
+ return MajikKeyManager.generateMnemonic(strength);
154
254
  }
155
- async exportAccountMnemonicBackup(id, mnemonic) {
156
- return MajikKeyStore.exportMnemonicBackup(id, mnemonic);
255
+ async createAccount(mnemonic, passphrase, label) {
256
+ try {
257
+ const key = await MajikKey.create(mnemonic, passphrase, label);
258
+ await this._keys.save(key);
259
+ const contact = key.toContact();
260
+ this._registerOwnAccount(contact);
261
+ this._emit("new-account", contact);
262
+ return { id: key.id, fingerprint: key.fingerprint, backup: key.backup };
263
+ }
264
+ catch (err) {
265
+ this._emit("error", err, { context: "createAccount" });
266
+ throw err;
267
+ }
157
268
  }
158
- /**
159
- * Import an account from a mnemonic-encrypted backup.
160
- * Fully upgrades to Argon2id KDF + ML-KEM keys in one step.
161
- */
162
269
  async importAccountFromMnemonicBackup(backupBase64, mnemonic, passphrase, label) {
163
- const key = await MajikKeyStore.importFromMnemonicBackup(backupBase64, mnemonic, passphrase, label);
164
- if (this.getOwnAccountById(key.id)) {
165
- throw new Error("Account with the same ID already exists");
270
+ try {
271
+ const key = await this._keys.importFromMnemonicBackup(backupBase64, mnemonic, passphrase, label);
272
+ if (this.getOwnAccountById(key.id)) {
273
+ throw new Error("Account with the same ID already exists");
274
+ }
275
+ const contact = key.toContact();
276
+ this._registerOwnAccount(contact);
277
+ this._emit("new-account", contact);
278
+ return { id: key.id, fingerprint: key.fingerprint };
166
279
  }
167
- const keyContact = key.toContact();
168
- this.addOwnAccount(keyContact);
169
- return { id: key.id, fingerprint: key.fingerprint };
170
- }
171
- /**
172
- * Create a new account from a mnemonic, store it encrypted with passphrase.
173
- */
174
- async createAccountFromMnemonic(mnemonic, passphrase, label) {
175
- const key = await MajikKey.create(mnemonic, passphrase, label);
176
- await MajikKeyStore.addMajikKey(key);
177
- const keyContact = key.toContact();
178
- this.addOwnAccount(keyContact);
179
- return { id: key.id, fingerprint: key.fingerprint, backup: key.backup };
180
- }
181
- addOwnAccount(account) {
182
- if (!this.ownAccounts.has(account.id)) {
183
- this.ownAccounts.set(account.id, account);
184
- this.ownAccountsOrder.push(account.id);
280
+ catch (err) {
281
+ this._emit("error", err, { context: "importAccountFromMnemonicBackup" });
282
+ throw err;
185
283
  }
284
+ }
285
+ async replaceAccountFromMnemonicBackup(backupBase64, mnemonic, passphrase, label) {
186
286
  try {
187
- if (!this.contacts.hasContact(account.id)) {
188
- this.contacts.addContact(account);
287
+ const currentAccount = this.getActiveAccountKey();
288
+ const currentContact = this.getActiveAccount();
289
+ const finalLabel = label?.trim() || currentContact?.meta?.label;
290
+ // 1. Import first (no mutation yet)
291
+ const key = await this._keys.importFromMnemonicBackup(backupBase64, mnemonic, passphrase, finalLabel);
292
+ // 2. Prevent duplicate (except self-replace)
293
+ if (this.getOwnAccountById(key.id) && key.id !== currentAccount?.id) {
294
+ throw new Error("Account with the same ID already exists");
189
295
  }
190
- if (!this.getActiveAccount()) {
191
- this.setActiveAccount(account.id);
296
+ const contact = key.toContact();
297
+ // 3. Remove old account if different
298
+ if (currentAccount && currentAccount.id !== key.id) {
299
+ await this.removeOwnAccount(currentAccount.id);
192
300
  }
193
- this.emit("new-account", account);
301
+ // 4. Register new account
302
+ this._registerOwnAccount(contact);
303
+ // 5. Set active
304
+ await this.setActiveAccount(contact.id, true);
305
+ this._emit("new-account", contact);
306
+ return { id: key.id, fingerprint: key.fingerprint };
194
307
  }
195
- catch {
196
- // ignore if contact can't be added
308
+ catch (err) {
309
+ this._emit("error", err, {
310
+ context: "replaceAccountFromMnemonicBackup",
311
+ });
312
+ throw err;
197
313
  }
198
- this.scheduleAutosave();
199
314
  }
200
- listOwnAccounts(majikahOnly = false) {
201
- let accounts = this.ownAccountsOrder
202
- .map((id) => this.ownAccounts.get(id))
203
- .filter((c) => !!c);
204
- if (majikahOnly) {
205
- accounts = accounts.filter((a) => this.isContactMajikahRegistered(a.id));
206
- }
207
- return accounts;
315
+ async exportAccountMnemonicBackup(id, mnemonic) {
316
+ return this._keys.exportMnemonicBackup(id, mnemonic);
317
+ }
318
+ addOwnAccount(account) {
319
+ this._registerOwnAccount(account);
320
+ this._emit("new-account", account);
321
+ }
322
+ async removeOwnAccount(id) {
323
+ if (!this._ownAccounts.has(id))
324
+ return false;
325
+ this._ownAccounts.delete(id);
326
+ const idx = this._ownAccountsOrder.indexOf(id);
327
+ if (idx > -1)
328
+ this._ownAccountsOrder.splice(idx, 1);
329
+ await this._contacts.removeContact(id);
330
+ await this._keys.delete(id);
331
+ this._scheduleOrderSave();
332
+ this._emit("removed-account", id);
333
+ return true;
208
334
  }
209
335
  getOwnAccountById(id) {
210
- return this.ownAccounts.get(id);
336
+ return this._ownAccounts.get(id);
337
+ }
338
+ getActiveAccount() {
339
+ if (!this._ownAccountsOrder.length)
340
+ return null;
341
+ return this._ownAccounts.get(this._ownAccountsOrder[0]) ?? null;
342
+ }
343
+ getActiveAccountKey() {
344
+ if (!this._ownAccountsOrder.length)
345
+ return null;
346
+ const activeKey = this._keys.get(this._ownAccountsOrder[0]);
347
+ if (!activeKey)
348
+ return null;
349
+ return activeKey;
350
+ }
351
+ isAccountActive(id) {
352
+ return this._ownAccounts.has(id) && this._ownAccountsOrder[0] === id;
211
353
  }
212
354
  async setActiveAccount(id, bypassIdentity = false) {
213
- if (!this.ownAccounts.has(id))
355
+ if (!this._ownAccounts.has(id))
214
356
  return false;
215
357
  if (!bypassIdentity) {
216
358
  try {
@@ -221,474 +363,221 @@ export class MajikMessage {
221
363
  }
222
364
  }
223
365
  const previousActive = this.getActiveAccount()?.id;
224
- const index = this.ownAccountsOrder.indexOf(id);
366
+ const index = this._ownAccountsOrder.indexOf(id);
225
367
  if (index > -1)
226
- this.ownAccountsOrder.splice(index, 1);
227
- this.ownAccountsOrder.unshift(id);
228
- this.scheduleAutosave();
368
+ this._ownAccountsOrder.splice(index, 1);
369
+ this._ownAccountsOrder.unshift(id);
370
+ this._scheduleOrderSave();
229
371
  if (previousActive !== id) {
230
- this.emit("active-account-change", this.getActiveAccount(), previousActive);
372
+ this._emit("active-account-change", this.getActiveAccount(), previousActive);
231
373
  }
232
374
  return true;
233
375
  }
234
- getActiveAccount() {
235
- if (!this.ownAccountsOrder.length)
236
- return null;
237
- return this.ownAccounts.get(this.ownAccountsOrder[0]) ?? null;
376
+ listOwnAccounts(majikahOnly = false) {
377
+ let accounts = this._ownAccountsOrder
378
+ .map((id) => this._ownAccounts.get(id))
379
+ .filter((c) => !!c);
380
+ if (majikahOnly) {
381
+ accounts = accounts.filter((a) => this.isContactMajikahRegistered(a.id));
382
+ }
383
+ return accounts;
238
384
  }
239
- isAccountActive(id) {
240
- return !!this.ownAccounts.has(id) && this.ownAccountsOrder[0] === id;
385
+ isContactMajikahRegistered(id) {
386
+ return this._contacts.isMajikahRegistered(id);
241
387
  }
242
- removeOwnAccount(id) {
243
- if (!this.ownAccounts.has(id))
244
- return false;
245
- this.ownAccounts.delete(id);
246
- const idx = this.ownAccountsOrder.indexOf(id);
247
- if (idx > -1)
248
- this.ownAccountsOrder.splice(idx, 1);
249
- this.removeContact(id);
250
- this.envelopeCache.deleteByFingerprint(id).catch(() => { });
251
- this.emit("removed-account", id);
252
- this.scheduleAutosave();
253
- return true;
388
+ isContactMajikahIdentityChecked(id) {
389
+ return this._contacts.isMajikahIdentityChecked(id);
254
390
  }
255
- async hasOwnIdentity(fingerprint) {
256
- return MajikKeyStore.hasIdentity(fingerprint);
391
+ setContactMajikahStatus(id, status) {
392
+ this._contacts.setMajikahStatus(id, status);
257
393
  }
258
- async updatePassphrase(currentPassphrase, newPassphrase, id) {
259
- const target = id ? this.getOwnAccountById(id) : this.getActiveAccount();
260
- if (!target)
261
- throw new Error("No target account specified");
262
- await MajikKeyStore.updatePassphrase(target.id, currentPassphrase, newPassphrase);
263
- this.scheduleAutosave();
394
+ async hasOwnIdentity(fingerprint) {
395
+ return this.keyManager.has(fingerprint);
264
396
  }
265
- // ── Contact Management ────────────────────────────────────────────────────
397
+ // ==========================================================================
398
+ // ── CONTACT MANAGEMENT ────────────────────────────────────────────────────
399
+ // ==========================================================================
266
400
  getContactByID(id) {
267
401
  if (!id?.trim())
268
402
  throw new Error("Invalid contact ID");
269
- return this.contacts.getContact(id) ?? null;
270
- }
271
- hasContact(id) {
272
- if (!id?.trim())
273
- throw new Error("Invalid contact ID");
274
- return this.contacts.hasContact(id);
275
- }
276
- async hasContactByPublicKeyBase64(publicKey) {
277
- if (!publicKey?.trim())
278
- throw new Error("Invalid contact public key");
279
- return await this.contacts.hasContactByPublicKeyBase64(publicKey);
403
+ return this._contacts.getContact(id) ?? null;
280
404
  }
281
405
  async getContactByPublicKey(publicKeyBase64) {
282
406
  if (!publicKeyBase64?.trim())
283
407
  throw new Error("Invalid public key");
284
- return ((await this.contacts.getContactByPublicKeyBase64(publicKeyBase64)) ?? null);
408
+ return ((await this._contacts.getContactByPublicKeyBase64(publicKeyBase64)) ??
409
+ null);
285
410
  }
286
- async exportContactAsJSON(contactID) {
287
- const contact = this.contacts.getContact(contactID);
288
- if (!contact)
289
- return null;
290
- let publicKeyBase64;
291
- const anyPub = contact.publicKey;
292
- if (anyPub?.raw instanceof Uint8Array) {
293
- publicKeyBase64 = arrayBufferToBase64(anyPub.raw.buffer);
294
- }
295
- else {
296
- const raw = await crypto.subtle.exportKey("raw", contact.publicKey);
297
- publicKeyBase64 = arrayBufferToBase64(raw);
298
- }
299
- return JSON.stringify({
300
- id: contact.id,
301
- label: contact.meta?.label || "",
302
- publicKey: publicKeyBase64,
303
- fingerprint: contact.fingerprint,
304
- mlKey: contact.mlKey,
305
- edPublicKeyBase64: contact.edPublicKeyBase64,
306
- mlDsaPublicKeyBase64: contact.mlDsaPublicKeyBase64,
307
- }, null, 2);
308
- }
309
- async exportContactAsString(contactID) {
310
- const contact = this.contacts.getContact(contactID);
311
- if (!contact)
312
- return null;
313
- const compressedString = this.exportContactCompressed(contact);
314
- return compressedString;
411
+ async exportContactAsJSON(id) {
412
+ if (!id?.trim())
413
+ throw new Error("Invalid contact ID");
414
+ return this._contacts.exportContactAsJSON(id);
415
+ }
416
+ async exportContactAsString(id) {
417
+ if (!id?.trim())
418
+ throw new Error("Invalid contact ID");
419
+ return this._contacts.exportContactAsString(id);
315
420
  }
316
421
  async importContactFromJSON(jsonStr) {
317
- try {
318
- const data = JSON.parse(jsonStr);
319
- if (!data.id || !data.publicKey || !data.fingerprint) {
320
- return { success: false, message: "Invalid contact JSON" };
321
- }
322
- const rawBuffer = base64ToArrayBuffer(data.publicKey);
323
- let publicKey;
324
- try {
325
- publicKey = await crypto.subtle.importKey("raw", rawBuffer, KEY_ALGO, true, []);
326
- }
327
- catch {
328
- publicKey = { raw: new Uint8Array(rawBuffer) };
329
- }
330
- this.addContact(new MajikContact({
331
- id: data.id,
332
- publicKey,
333
- fingerprint: data.fingerprint,
334
- meta: { label: data.label },
335
- mlKey: data.mlKey,
336
- edPublicKeyBase64: data.edPublicKeyBase64,
337
- mlDsaPublicKeyBase64: data.mlDsaPublicKeyBase64,
338
- }));
339
- return { success: true, message: "Contact imported successfully" };
340
- }
341
- catch (err) {
342
- return {
343
- success: false,
344
- message: err instanceof Error ? err.message : "Unknown error",
345
- };
346
- }
422
+ if (!jsonStr?.trim())
423
+ throw new Error("Invalid contact JSON");
424
+ return this._contacts.importContactFromJSON(jsonStr);
347
425
  }
348
426
  async importContactFromString(base64Str) {
349
- try {
350
- const parsedContact = await this.importContactCompressed(base64Str);
351
- this.addContact(parsedContact);
352
- return { success: true, message: "Contact imported successfully" };
353
- }
354
- catch (err) {
355
- return {
356
- success: false,
357
- message: err instanceof Error ? err.message : "Unknown error",
358
- };
359
- }
427
+ if (!base64Str?.trim())
428
+ throw new Error("Invalid contact string");
429
+ return this._contacts.importContactFromString(base64Str);
360
430
  }
361
431
  async exportContactCompressed(contact) {
362
- // Prepare JSON with raw keys
363
- let publicKeyBase64;
364
- const anyPub = contact.publicKey;
365
- if (anyPub?.raw instanceof Uint8Array) {
366
- publicKeyBase64 = arrayBufferToBase64(anyPub.raw.buffer);
367
- }
368
- else {
369
- const raw = await crypto.subtle.exportKey("raw", contact.publicKey);
370
- publicKeyBase64 = arrayBufferToBase64(raw);
371
- }
372
- const jsonObj = {
373
- id: contact.id,
374
- label: contact.meta?.label || "",
375
- publicKey: publicKeyBase64,
376
- fingerprint: contact.fingerprint,
377
- mlKey: contact.mlKey,
378
- edPublicKeyBase64: contact.edPublicKeyBase64,
379
- mlDsaPublicKeyBase64: contact.mlDsaPublicKeyBase64,
380
- };
381
- const jsonStr = JSON.stringify(jsonObj);
382
- const utf8 = new TextEncoder().encode(jsonStr);
383
- // Compress with gzip or Brotli
384
- const compressed = gzipSync(utf8);
385
- // Encode for string export
386
- return arrayToBase64(compressed);
432
+ if (!contact?.id?.trim())
433
+ throw new Error("Invalid contact");
434
+ return this._contacts.exportContactCompressed(contact);
387
435
  }
388
436
  async importContactCompressed(base64Str) {
389
- const compressed = base64ToArrayBuffer(base64Str);
390
- const decompressed = gunzipSync(new Uint8Array(compressed));
391
- const jsonStr = new TextDecoder().decode(decompressed);
392
- const data = JSON.parse(jsonStr);
393
- const rawBuffer = base64ToArrayBuffer(data.publicKey);
394
- let publicKey;
395
- try {
396
- publicKey = await crypto.subtle.importKey("raw", rawBuffer, KEY_ALGO, true, []);
397
- }
398
- catch {
399
- publicKey = { raw: new Uint8Array(rawBuffer) };
400
- }
401
- if (!data?.id || !publicKey || !data?.fingerprint || !data?.mlKey) {
402
- throw new Error("Invalid contact JSON");
403
- }
404
- return new MajikContact({
405
- id: data.id,
406
- publicKey,
407
- fingerprint: data.fingerprint,
408
- meta: { label: data.label },
409
- mlKey: data.mlKey,
410
- edPublicKeyBase64: data.edPublicKeyBase64,
411
- mlDsaPublicKeyBase64: data.mlDsaPublicKeyBase64,
412
- });
437
+ if (!base64Str?.trim())
438
+ throw new Error("Invalid contact string");
439
+ return this._contacts.importContactCompressed(base64Str);
413
440
  }
414
- addContact(contact) {
441
+ async addContact(contact) {
415
442
  if (!contact?.id ||
416
443
  !contact?.publicKey ||
417
444
  !contact?.fingerprint ||
418
445
  !contact?.mlKey) {
419
- throw new Error("Invalid contact JSON");
446
+ throw new Error("Invalid contact — missing required fields");
420
447
  }
421
- this.contacts.addContact(contact);
422
- this.emit("new-contact", contact);
423
- this.scheduleAutosave();
448
+ await this._contacts.addContact(contact);
449
+ this._emit("new-contact", contact);
424
450
  }
425
- removeContact(id) {
426
- const result = this.contacts.removeContact(id);
451
+ async removeContact(id) {
452
+ const result = await this._contacts.removeContact(id);
427
453
  if (!result.success)
428
454
  throw new Error(result.message);
429
- this.emit("removed-contact", id);
430
- this.scheduleAutosave();
455
+ this._emit("removed-contact", id);
431
456
  }
432
- updateContactMeta(id, meta) {
433
- this.contacts.updateContactMeta(id, meta);
434
- this.scheduleAutosave();
435
- }
436
- blockContact(id) {
437
- this.contacts.blockContact(id);
438
- this.scheduleAutosave();
439
- }
440
- unblockContact(id) {
441
- this.contacts.unblockContact(id);
442
- this.scheduleAutosave();
443
- }
444
- listContacts(all = true, majikahOnly = false) {
445
- const contacts = this.contacts.listContacts(true, majikahOnly);
446
- if (all)
457
+ listContacts(includeOwnAccounts = false) {
458
+ const contacts = this._contacts.listContacts(true);
459
+ if (includeOwnAccounts)
447
460
  return contacts;
448
- const ownIds = new Set(this.listOwnAccounts(majikahOnly).map((a) => a.id));
461
+ const ownIds = new Set(this.listOwnAccounts().map((a) => a.id));
449
462
  return contacts.filter((c) => !ownIds.has(c.id));
450
463
  }
451
- isContactMajikahRegistered(id) {
452
- return this.contacts.isMajikahRegistered(id);
464
+ async updateContactMeta(id, meta) {
465
+ await this._contacts.updateContactMeta(id, meta);
453
466
  }
454
- isContactMajikahIdentityChecked(id) {
455
- return this.contacts.isMajikahIdentityChecked(id);
456
- }
457
- setContactMajikahStatus(id, status) {
458
- this.contacts.setMajikahStatus(id, status);
459
- this.scheduleAutosave();
460
- }
461
- /* ================================
462
- * Group CRUD Pass-throughs
463
- * ================================ */
464
- /**
465
- * Creates and registers a new user-defined group.
466
- * Throws if a group with the same ID already exists.
467
- */
468
- createGroup(id, name, meta, initialMemberIds) {
469
- const newGroup = this.contacts.createGroup(id, name, meta, initialMemberIds);
470
- this.emit("new-contact-group", newGroup);
471
- this.scheduleAutosave();
467
+ async createGroup(id, name, meta, initialMemberIds) {
468
+ const newGroup = await this._contacts.createGroup(id, name, meta, initialMemberIds);
469
+ this._emit("new-contact-group", newGroup);
472
470
  return this;
473
471
  }
474
- /**
475
- * Registers an already-constructed MajikContactGroup instance.
476
- * Throws if a group with the same ID already exists.
477
- */
478
- addGroup(group) {
479
- this.contacts.addGroup(group);
480
- this.emit("new-contact-group", group);
481
- this.scheduleAutosave();
472
+ async addGroup(group) {
473
+ await this._contacts.addGroup(group);
474
+ this._emit("new-contact-group", group);
482
475
  return this;
483
476
  }
484
- /**
485
- * Removes a user group by ID.
486
- * System groups (Favorites, Blocked) cannot be deleted.
487
- */
488
- removeGroup(id) {
489
- const response = this.contacts.removeGroup(id);
490
- this.emit("removed-contact-group", response.data);
491
- this.scheduleAutosave();
477
+ async removeGroup(id) {
478
+ const response = await this._contacts.removeGroup(id);
479
+ this._emit("removed-contact-group", response.data);
492
480
  return response;
493
481
  }
494
- /**
495
- * Returns a group by ID, or undefined if not found.
496
- */
497
482
  getContactGroup(id) {
498
- return this.contacts.getGroup(id);
483
+ return this._contacts.getGroup(id);
499
484
  }
500
- /**
501
- * Returns a group by ID. Throws if not found.
502
- */
503
485
  getGroupOrThrow(id) {
504
- return this.contacts.getGroupOrThrow(id);
486
+ return this._contacts.getGroupOrThrow(id);
505
487
  }
506
- /**
507
- * Returns true if a group with the given ID exists.
508
- */
509
488
  hasGroup(id) {
510
- return this.contacts.hasGroup(id);
489
+ return this._contacts.hasGroup(id);
511
490
  }
512
- /**
513
- * Returns all groups.
514
- *
515
- * @param includeSystem Include system groups (Favorites, Blocked). Default: true.
516
- * @param sortedByName Sort results alphabetically by group name. Default: false.
517
- */
518
491
  listContactGroups(includeSystem = true, sortedByName = false) {
519
- return this.contacts.listGroups(includeSystem, sortedByName);
492
+ return this._contacts.listGroups(includeSystem, sortedByName);
520
493
  }
521
- /**
522
- * Returns only user-created groups (excludes Favorites and Blocked).
523
- * Sorted alphabetically by name.
524
- */
525
494
  listUserGroups(sortedByName = true) {
526
- return this.contacts.listGroups(false, sortedByName);
495
+ return this._contacts.listGroups(false, sortedByName);
527
496
  }
528
- /**
529
- * Returns only system groups (Favorites and Blocked).
530
- */
531
497
  listSystemGroups() {
532
- return this.contacts.listGroups(true).filter((g) => g.isSystem);
498
+ return this._contacts.listGroups(true).filter((g) => g.isSystem);
533
499
  }
534
- /**
535
- * Updates mutable metadata on a group (name, description).
536
- * Name is locked on system groups — will throw if attempted.
537
- */
538
- updateGroupMeta(id, meta) {
539
- const updatedGroup = this.contacts.updateGroupMeta(id, meta);
540
- this.emit("contact-group-change", updatedGroup);
541
- this.scheduleAutosave();
500
+ async updateGroupMeta(id, meta) {
501
+ const updatedGroup = await this._contacts.updateGroupMeta(id, meta);
502
+ this._emit("contact-group-change", updatedGroup);
542
503
  return this;
543
504
  }
544
- /* ================================
545
- * Group Membership Pass-throughs
546
- * ================================ */
547
- /**
548
- * Adds a contact to a group.
549
- * Validates the contact exists in the directory.
550
- * If the group is the system Blocked group, also calls contact.block().
551
- * Throws if the contact is already a member — use addContactToGroupIfAbsent for idempotent.
552
- */
553
- addContactToGroup(groupID, contactID) {
554
- const updatedGroup = this.contacts.addContactToGroup(groupID, contactID);
555
- this.emit("contact-group-change", updatedGroup);
556
- this.scheduleAutosave();
505
+ async addContactToGroup(groupID, contactID) {
506
+ const updatedGroup = await this._contacts.addContactToGroup(groupID, contactID);
507
+ this._emit("contact-group-change", updatedGroup);
557
508
  return this;
558
509
  }
559
- /**
560
- * Adds multiple contacts to a group in one call (all-or-nothing).
561
- */
562
- addContactsToGroup(groupID, contactIds) {
563
- const updatedGroup = this.contacts.addContactsToGroup(groupID, contactIds);
564
- this.emit("contact-group-change", updatedGroup);
565
- this.scheduleAutosave();
510
+ async addContactsToGroup(groupID, contactIds) {
511
+ const updatedGroup = await this._contacts.addContactsToGroup(groupID, contactIds);
512
+ this._emit("contact-group-change", updatedGroup);
566
513
  return this;
567
514
  }
568
- /**
569
- * Removes a contact from a group.
570
- * If the group is the system Blocked group, also calls contact.unblock().
571
- * Throws if the contact is not a member — use removeContactFromGroupIfPresent for idempotent.
572
- */
573
- removeContactFromGroup(groupID, contactID) {
574
- const updatedGroup = this.contacts.removeContactFromGroup(groupID, contactID);
575
- this.emit("contact-group-change", updatedGroup);
576
- this.scheduleAutosave();
515
+ async removeContactFromGroup(groupID, contactID) {
516
+ const updatedGroup = await this._contacts.removeContactFromGroup(groupID, contactID);
517
+ this._emit("contact-group-change", updatedGroup);
577
518
  return this;
578
519
  }
579
- /**
580
- * Moves a contact from one group to another atomically.
581
- * Throws if the contact is not a member of the source group.
582
- */
583
- moveContactBetweenGroups(contactID, fromGroupId, toGroupId) {
584
- const updatedGroup = this.contacts.moveContactBetweenGroups(contactID, fromGroupId, toGroupId);
585
- this.emit("contact-group-change", updatedGroup);
586
- this.scheduleAutosave();
520
+ async moveContactBetweenGroups(contactID, fromGroupId, toGroupId) {
521
+ const updatedGroup = await this._contacts.moveContactBetweenGroups(contactID, fromGroupId, toGroupId);
522
+ this._emit("contact-group-change", updatedGroup);
587
523
  return this;
588
524
  }
589
- /* ================================
590
- * Group Query Pass-throughs
591
- * ================================ */
592
- /**
593
- * Returns all hydrated MajikContact instances in the given group.
594
- * Contacts removed from the directory since last save are silently skipped.
595
- */
596
525
  getContactsInGroup(groupID) {
597
- return this.contacts.getContactsInGroup(groupID);
526
+ return this._contacts.getContactsInGroup(groupID);
598
527
  }
599
- /**
600
- * Returns hydrated contacts in the group, sorted by label (or ID if no label).
601
- */
602
528
  getContactsInGroupSorted(groupID) {
603
- return this.contacts.getContactsInGroupSorted(groupID);
529
+ return this._contacts.getContactsInGroupSorted(groupID);
604
530
  }
605
- /**
606
- * Returns true if the contact is a member of the given group.
607
- */
608
531
  isContactInGroup(groupID, contactID) {
609
- return this.contacts.isContactInGroup(groupID, contactID);
532
+ return this._contacts.isContactInGroup(groupID, contactID);
610
533
  }
611
- /**
612
- * Returns all groups the contact belongs to.
613
- */
614
534
  getGroupsForContact(contactID) {
615
- return this.contacts.getGroupsForContact(contactID);
535
+ return this._contacts.getGroupsForContact(contactID);
616
536
  }
617
- /**
618
- * Returns all group IDs the contact belongs to.
619
- */
620
537
  getGroupIdsForContact(contactID) {
621
- return this.contacts.getGroupIdsForContact(contactID);
538
+ return this._contacts.getGroupIdsForContact(contactID);
622
539
  }
623
- /* ================================
624
- * System Group Convenience Pass-throughs
625
- * ================================ */
626
- /**
627
- * Adds the contact to the Favorites group (idempotent).
628
- */
629
- addContactToFavorites(contactID) {
630
- const updatedGroup = this.contacts.addToFavorites(contactID);
631
- this.emit("contact-group-change", updatedGroup);
632
- this.scheduleAutosave();
540
+ async addContactToFavorites(contactID) {
541
+ const updatedGroup = await this._contacts.addToFavorites(contactID);
542
+ this._emit("contact-group-change", updatedGroup);
633
543
  return this;
634
544
  }
635
- /**
636
- * Removes the contact from the Favorites group (idempotent).
637
- */
638
- removeContactFromFavorites(contactID) {
639
- const updatedGroup = this.contacts.removeFromFavorites(contactID);
640
- this.emit("contact-group-change", updatedGroup);
641
- this.scheduleAutosave();
545
+ async removeContactFromFavorites(contactID) {
546
+ const updatedGroup = await this._contacts.removeFromFavorites(contactID);
547
+ this._emit("contact-group-change", updatedGroup);
642
548
  return this;
643
549
  }
644
- /**
645
- * Returns true if the contact is in the Favorites group.
646
- */
647
550
  isContactFavorite(contactID) {
648
- return this.contacts.isFavorite(contactID);
551
+ return this._contacts.isFavorite(contactID);
649
552
  }
650
- /**
651
- * Returns true if the contact is in the Blocked group.
652
- */
653
553
  isContactBlocked(contactID) {
654
- return this.contacts.isContactBlocked(contactID);
554
+ return this._contacts.isContactBlocked(contactID);
655
555
  }
656
- /**
657
- * Returns the Favorites system group instance.
658
- */
659
556
  getFavoritesGroup() {
660
- return this.contacts.getFavoritesGroup();
557
+ return this._contacts.getFavoritesGroup();
661
558
  }
662
- /**
663
- * Returns the Blocked system group instance.
664
- */
665
559
  getBlockedGroup() {
666
- return this.contacts.getBlockedGroup();
560
+ return this._contacts.getBlockedGroup();
667
561
  }
668
- /**
669
- * Returns all contacts in the Favorites group as hydrated MajikContact instances.
670
- */
671
562
  getFavoriteContacts() {
672
- return this.contacts.getContactsInGroup(this.contacts.getFavoritesGroup().id);
563
+ return this._contacts.getContactsInGroup(this._contacts.getFavoritesGroup().id);
673
564
  }
674
- /**
675
- * Returns all contacts in the Blocked group as hydrated MajikContact instances.
676
- */
677
565
  getBlockedContacts() {
678
- return this.contacts.getContactsInGroup(this.contacts.getBlockedGroup().id);
566
+ return this._contacts.getContactsInGroup(this._contacts.getBlockedGroup().id);
679
567
  }
680
- /* ================================
681
- * Directory Clear
682
- * ================================ */
683
- /**
684
- * Clears both the directory and all group memberships.
685
- * System groups are preserved (re-bootstrapped by the group manager).
686
- */
687
- clearDirectory() {
688
- this.contacts.clear();
689
- this.scheduleAutosave();
568
+ async clearDirectory() {
569
+ await this._contacts.clear();
690
570
  return this;
691
571
  }
572
+ resolveSignerLabel(signerId) {
573
+ const ownAccount = this._ownAccounts.get(signerId);
574
+ if (ownAccount?.meta?.label)
575
+ return ownAccount.meta.label;
576
+ const contact = this._contacts.getContact(signerId);
577
+ if (contact?.meta?.label)
578
+ return contact.meta.label;
579
+ return `${signerId.slice(0, 16)}…`;
580
+ }
692
581
  // ── Encryption / Decryption ───────────────────────────────────────────────
693
582
  /**
694
583
  * Compose and encrypt a message for one or more recipients.
@@ -710,8 +599,7 @@ export class MajikMessage {
710
599
  if (cache) {
711
600
  await this.envelopeCache.set(new MessageEnvelope(envelope.toBinary()), this._source);
712
601
  }
713
- this.scheduleAutosave();
714
- this.emit("envelope", envelope);
602
+ this._emit("envelope", envelope);
715
603
  return scannerString;
716
604
  }
717
605
  /**
@@ -764,7 +652,7 @@ export class MajikMessage {
764
652
  }
765
653
  catch (err) {
766
654
  console.warn("Error: ", err);
767
- this.emit("error", err, { context: "encryptTextForScanner" });
655
+ this._emit("error", err, { context: "encryptTextForScanner" });
768
656
  return null;
769
657
  }
770
658
  }
@@ -829,7 +717,7 @@ export class MajikMessage {
829
717
  return { messageChat, scannerString };
830
718
  }
831
719
  catch (err) {
832
- this.emit("error", err, { context: "createEncryptedMajikMessageChat" });
720
+ this._emit("error", err, { context: "createEncryptedMajikMessageChat" });
833
721
  throw err;
834
722
  }
835
723
  }
@@ -848,7 +736,7 @@ export class MajikMessage {
848
736
  return await envelope.decrypt(identity);
849
737
  }
850
738
  catch (err) {
851
- this.emit("error", err, { context: "decryptMajikMessageChat" });
739
+ this._emit("error", err, { context: "decryptMajikMessageChat" });
852
740
  throw err;
853
741
  }
854
742
  }
@@ -889,7 +777,7 @@ export class MajikMessage {
889
777
  const activeId = this.getActiveAccount()?.id;
890
778
  if (!activeId)
891
779
  throw new Error("No active account — call setActiveAccount() first");
892
- const signingKey = MajikKeyStore.get(activeId);
780
+ const signingKey = this.keyManager.get(activeId);
893
781
  // ── 4. Build CreateOptions ─────────────────────────────────────────────
894
782
  const createOptions = {
895
783
  data,
@@ -1030,9 +918,9 @@ export class MajikMessage {
1030
918
  if (!id)
1031
919
  throw new Error("No active account — call setActiveAccount() first");
1032
920
  try {
1033
- await MajikKeyStore.ensureUnlocked(id);
921
+ await this.keyManager.ensureUnlocked(id);
1034
922
  // get() is safe after ensureUnlocked() — key is in the memory cache.
1035
- const key = MajikKeyStore.get(id);
923
+ const key = this.keyManager.get(id);
1036
924
  if (!key)
1037
925
  throw new Error(`Account not found in keystore: "${id}"`);
1038
926
  if (!key.hasSigningKeys) {
@@ -1045,7 +933,7 @@ export class MajikMessage {
1045
933
  });
1046
934
  }
1047
935
  catch (err) {
1048
- this.emit("error", err, { context: "signMajikFile" });
936
+ this._emit("error", err, { context: "signMajikFile" });
1049
937
  throw err;
1050
938
  }
1051
939
  }
@@ -1087,7 +975,7 @@ export class MajikMessage {
1087
975
  return file.verify(sig.extractPublicKeys());
1088
976
  }
1089
977
  catch (err) {
1090
- this.emit("error", err, { context: "verifyMajikFile" });
978
+ this._emit("error", err, { context: "verifyMajikFile" });
1091
979
  throw err;
1092
980
  }
1093
981
  }
@@ -1136,7 +1024,7 @@ export class MajikMessage {
1136
1024
  return file.verifyBinary(decryptIdentity, sig.extractPublicKeys());
1137
1025
  }
1138
1026
  catch (err) {
1139
- this.emit("error", err, { context: "verifyMajikFileBinary" });
1027
+ this._emit("error", err, { context: "verifyMajikFileBinary" });
1140
1028
  throw err;
1141
1029
  }
1142
1030
  }
@@ -1170,7 +1058,7 @@ export class MajikMessage {
1170
1058
  return false;
1171
1059
  // get() checks the memory cache — no async needed since the account
1172
1060
  // must already be loaded to be the active account.
1173
- const key = MajikKeyStore.get(id);
1061
+ const key = this.keyManager.get(id);
1174
1062
  if (!key)
1175
1063
  return false;
1176
1064
  return key.fingerprint === sigInfo.signerId;
@@ -1448,7 +1336,7 @@ export class MajikMessage {
1448
1336
  const id = accountId ?? this.getActiveAccount()?.id;
1449
1337
  if (!id)
1450
1338
  return false;
1451
- const key = MajikKeyStore.get(id);
1339
+ const key = this.keyManager.get(id);
1452
1340
  return key?.hasSigningKeys === true;
1453
1341
  }
1454
1342
  // ── Envelope Cache ────────────────────────────────────────────────────────
@@ -1459,30 +1347,30 @@ export class MajikMessage {
1459
1347
  const response = await this.envelopeCache.clear();
1460
1348
  if (!response?.success)
1461
1349
  throw new Error(response.message);
1462
- this.scheduleAutosave();
1350
+ this._scheduleOrderSave();
1463
1351
  return response.success;
1464
1352
  }
1465
1353
  // ── Identity / Passphrase ─────────────────────────────────────────────────
1466
1354
  /**
1467
1355
  * Ensure an identity is unlocked.
1468
- * Delegates entirely to MajikKeyStore.ensureUnlocked() — passphrase prompting
1356
+ * Delegates entirely to this.keyManager.ensureUnlocked() — passphrase prompting
1469
1357
  * is handled there via onUnlockRequested or the optional promptFn.
1470
1358
  */
1471
1359
  async ensureIdentityUnlocked(id, promptFn) {
1472
- return MajikKeyStore.ensureUnlocked(id, promptFn);
1360
+ return this.keyManager.ensureUnlocked(id, promptFn);
1473
1361
  }
1474
1362
  async isPassphraseValid(passphrase, id) {
1475
1363
  const target = id ? this.getOwnAccountById(id) : this.getActiveAccount();
1476
1364
  if (!target)
1477
1365
  return false;
1478
- return MajikKeyStore.isPassphraseValid(target.id, passphrase);
1366
+ return this.keyManager.isPassphraseValid(target.id, passphrase);
1479
1367
  }
1480
1368
  // ── Events ────────────────────────────────────────────────────────────────
1481
1369
  on(event, callback) {
1482
- this.listeners.get(event)?.push(callback);
1370
+ this._listeners.get(event)?.push(callback);
1483
1371
  }
1484
1372
  off(event, callback) {
1485
- const cbs = this.listeners.get(event);
1373
+ const cbs = this._listeners.get(event);
1486
1374
  if (!cbs?.length)
1487
1375
  return;
1488
1376
  if (callback) {
@@ -1491,11 +1379,11 @@ export class MajikMessage {
1491
1379
  cbs.splice(i, 1);
1492
1380
  }
1493
1381
  else {
1494
- this.listeners.set(event, []);
1382
+ this._listeners.set(event, []);
1495
1383
  }
1496
1384
  }
1497
- emit(event, ...args) {
1498
- this.listeners.get(event)?.forEach((cb) => cb(...args));
1385
+ _emit(event, ...args) {
1386
+ this._listeners.get(event)?.forEach((cb) => cb(...args));
1499
1387
  }
1500
1388
  // ── Content & File Signing ────────────────────────────────────────────────
1501
1389
  /**
@@ -1514,8 +1402,8 @@ export class MajikMessage {
1514
1402
  if (!id)
1515
1403
  throw new Error("No active account — call setActiveAccount() first");
1516
1404
  try {
1517
- await MajikKeyStore.ensureUnlocked(id);
1518
- const key = MajikKeyStore.get(id);
1405
+ await this.keyManager.ensureUnlocked(id);
1406
+ const key = this.keyManager.get(id);
1519
1407
  if (!key)
1520
1408
  throw new Error(`Account not found in keystore: "${id}"`);
1521
1409
  if (!key.hasSigningKeys) {
@@ -1528,7 +1416,7 @@ export class MajikMessage {
1528
1416
  });
1529
1417
  }
1530
1418
  catch (err) {
1531
- this.emit("error", err, { context: "signContent" });
1419
+ this._emit("error", err, { context: "signContent" });
1532
1420
  throw err;
1533
1421
  }
1534
1422
  }
@@ -1551,8 +1439,8 @@ export class MajikMessage {
1551
1439
  if (!id)
1552
1440
  throw new Error("No active account — call setActiveAccount() first");
1553
1441
  try {
1554
- await MajikKeyStore.ensureUnlocked(id);
1555
- const key = MajikKeyStore.get(id);
1442
+ await this.keyManager.ensureUnlocked(id);
1443
+ const key = this.keyManager.get(id);
1556
1444
  if (!key)
1557
1445
  throw new Error(`Account not found in keystore: "${id}"`);
1558
1446
  if (!key.hasSigningKeys) {
@@ -1567,7 +1455,7 @@ export class MajikMessage {
1567
1455
  });
1568
1456
  }
1569
1457
  catch (err) {
1570
- this.emit("error", err, { context: "signFile" });
1458
+ this._emit("error", err, { context: "signFile" });
1571
1459
  throw err;
1572
1460
  }
1573
1461
  }
@@ -1596,8 +1484,8 @@ export class MajikMessage {
1596
1484
  const id = options?.accountId ?? this.getActiveAccount()?.id;
1597
1485
  if (!id)
1598
1486
  throw new Error("No active account — call setActiveAccount() first");
1599
- await MajikKeyStore.ensureUnlocked(id);
1600
- const key = MajikKeyStore.get(id);
1487
+ await this.keyManager.ensureUnlocked(id);
1488
+ const key = this.keyManager.get(id);
1601
1489
  if (!key)
1602
1490
  throw new Error(`Account not found in keystore: "${id}"`);
1603
1491
  if (!key.hasSigningKeys) {
@@ -1621,7 +1509,7 @@ export class MajikMessage {
1621
1509
  };
1622
1510
  }
1623
1511
  catch (err) {
1624
- this.emit("error", err, { context: "batchSignFiles" });
1512
+ this._emit("error", err, { context: "batchSignFiles" });
1625
1513
  return {
1626
1514
  blob: null,
1627
1515
  signature: null,
@@ -1670,7 +1558,7 @@ export class MajikMessage {
1670
1558
  return MajikSignature.verify(content, sig, sig.extractPublicKeys());
1671
1559
  }
1672
1560
  catch (err) {
1673
- this.emit("error", err, { context: "verifyContent" });
1561
+ this._emit("error", err, { context: "verifyContent" });
1674
1562
  throw err;
1675
1563
  }
1676
1564
  }
@@ -1726,7 +1614,7 @@ export class MajikMessage {
1726
1614
  return results[0];
1727
1615
  }
1728
1616
  catch (err) {
1729
- this.emit("error", err, { context: "verifyFile" });
1617
+ this._emit("error", err, { context: "verifyFile" });
1730
1618
  throw err;
1731
1619
  }
1732
1620
  }
@@ -1795,7 +1683,7 @@ export class MajikMessage {
1795
1683
  };
1796
1684
  }
1797
1685
  catch (err) {
1798
- this.emit("error", err, { context: "batchVerifyFiles" });
1686
+ this._emit("error", err, { context: "batchVerifyFiles" });
1799
1687
  return {
1800
1688
  valid: false,
1801
1689
  signerId: undefined,
@@ -1824,7 +1712,7 @@ export class MajikMessage {
1824
1712
  return MajikSignature.extractFrom(file, options);
1825
1713
  }
1826
1714
  catch (err) {
1827
- this.emit("error", err, { context: "extractSignature" });
1715
+ this._emit("error", err, { context: "extractSignature" });
1828
1716
  throw err;
1829
1717
  }
1830
1718
  }
@@ -1842,7 +1730,7 @@ export class MajikMessage {
1842
1730
  return MajikSignature.stripFrom(file, options);
1843
1731
  }
1844
1732
  catch (err) {
1845
- this.emit("error", err, { context: "stripSignature" });
1733
+ this._emit("error", err, { context: "stripSignature" });
1846
1734
  throw err;
1847
1735
  }
1848
1736
  }
@@ -1860,7 +1748,7 @@ export class MajikMessage {
1860
1748
  return MajikSignature.isSigned(file, options);
1861
1749
  }
1862
1750
  catch (err) {
1863
- this.emit("error", err, { context: "isFileSigned" });
1751
+ this._emit("error", err, { context: "isFileSigned" });
1864
1752
  throw err;
1865
1753
  }
1866
1754
  }
@@ -1878,7 +1766,7 @@ export class MajikMessage {
1878
1766
  const id = accountId ?? this.getActiveAccount()?.id;
1879
1767
  if (!id)
1880
1768
  throw new Error("No active account — call setActiveAccount() first");
1881
- const key = MajikKeyStore.get(id);
1769
+ const key = this.keyManager.get(id);
1882
1770
  if (!key)
1883
1771
  throw new Error(`Account not found in keystore: "${id}"`);
1884
1772
  if (!key.hasSigningKeys) {
@@ -1936,7 +1824,7 @@ export class MajikMessage {
1936
1824
  return MajikSignature.getAllowlist(file, options);
1937
1825
  }
1938
1826
  catch (err) {
1939
- this.emit("error", err, { context: "getAllowlist" });
1827
+ this._emit("error", err, { context: "getAllowlist" });
1940
1828
  throw err;
1941
1829
  }
1942
1830
  }
@@ -1953,7 +1841,7 @@ export class MajikMessage {
1953
1841
  return MajikSignature.canSign(file, key, options);
1954
1842
  }
1955
1843
  catch (err) {
1956
- this.emit("error", err, { context: "canSign" });
1844
+ this._emit("error", err, { context: "canSign" });
1957
1845
  throw err;
1958
1846
  }
1959
1847
  }
@@ -1967,7 +1855,7 @@ export class MajikMessage {
1967
1855
  return MajikSignature.isMultiSig(file, options);
1968
1856
  }
1969
1857
  catch (err) {
1970
- this.emit("error", err, { context: "isMultiSig" });
1858
+ this._emit("error", err, { context: "isMultiSig" });
1971
1859
  throw err;
1972
1860
  }
1973
1861
  }
@@ -1994,7 +1882,7 @@ export class MajikMessage {
1994
1882
  return MajikSignature.getSignatories(file, options, filter);
1995
1883
  }
1996
1884
  catch (err) {
1997
- this.emit("error", err, { context: "getSignatories" });
1885
+ this._emit("error", err, { context: "getSignatories" });
1998
1886
  throw err;
1999
1887
  }
2000
1888
  }
@@ -2007,7 +1895,7 @@ export class MajikMessage {
2007
1895
  return MajikSignature.getSignedSignatories(file, options);
2008
1896
  }
2009
1897
  catch (err) {
2010
- this.emit("error", err, { context: "getSignedSignatories" });
1898
+ this._emit("error", err, { context: "getSignedSignatories" });
2011
1899
  throw err;
2012
1900
  }
2013
1901
  }
@@ -2020,7 +1908,7 @@ export class MajikMessage {
2020
1908
  return MajikSignature.getPendingSignatories(file, options);
2021
1909
  }
2022
1910
  catch (err) {
2023
- this.emit("error", err, { context: "getPendingSignatories" });
1911
+ this._emit("error", err, { context: "getPendingSignatories" });
2024
1912
  throw err;
2025
1913
  }
2026
1914
  }
@@ -2033,7 +1921,7 @@ export class MajikMessage {
2033
1921
  return MajikSignature.getAllSignatories(file, options);
2034
1922
  }
2035
1923
  catch (err) {
2036
- this.emit("error", err, { context: "getAllSignatories" });
1924
+ this._emit("error", err, { context: "getAllSignatories" });
2037
1925
  throw err;
2038
1926
  }
2039
1927
  }
@@ -2050,7 +1938,7 @@ export class MajikMessage {
2050
1938
  return MajikSignature.getIssuer(file, options);
2051
1939
  }
2052
1940
  catch (err) {
2053
- this.emit("error", err, { context: "getIssuer" });
1941
+ this._emit("error", err, { context: "getIssuer" });
2054
1942
  throw err;
2055
1943
  }
2056
1944
  }
@@ -2077,7 +1965,7 @@ export class MajikMessage {
2077
1965
  return MajikSignature.extractFrom(file, options);
2078
1966
  }
2079
1967
  catch (err) {
2080
- this.emit("error", err, { context: "getFileSignatureInfo" });
1968
+ this._emit("error", err, { context: "getFileSignatureInfo" });
2081
1969
  throw err;
2082
1970
  }
2083
1971
  }
@@ -2098,7 +1986,7 @@ export class MajikMessage {
2098
1986
  return MajikSignature.getEnvelopeInfo(file, options);
2099
1987
  }
2100
1988
  catch (err) {
2101
- this.emit("error", err, { context: "getEnvelopeInfo" });
1989
+ this._emit("error", err, { context: "getEnvelopeInfo" });
2102
1990
  throw err;
2103
1991
  }
2104
1992
  }
@@ -2119,7 +2007,7 @@ export class MajikMessage {
2119
2007
  if (!id)
2120
2008
  throw new Error("No active account — call setActiveAccount() first");
2121
2009
  try {
2122
- const key = MajikKeyStore.get(id);
2010
+ const key = this.keyManager.get(id);
2123
2011
  if (!key)
2124
2012
  throw new Error(`Account not found in keystore: "${id}"`);
2125
2013
  return MajikSignature.seal(file, key, {
@@ -2128,7 +2016,7 @@ export class MajikMessage {
2128
2016
  });
2129
2017
  }
2130
2018
  catch (err) {
2131
- this.emit("error", err, { context: "seal" });
2019
+ this._emit("error", err, { context: "seal" });
2132
2020
  throw err;
2133
2021
  }
2134
2022
  }
@@ -2146,7 +2034,7 @@ export class MajikMessage {
2146
2034
  return MajikSignature.verifySeal(file, options);
2147
2035
  }
2148
2036
  catch (err) {
2149
- this.emit("error", err, { context: "verifySeal" });
2037
+ this._emit("error", err, { context: "verifySeal" });
2150
2038
  throw err;
2151
2039
  }
2152
2040
  }
@@ -2163,7 +2051,7 @@ export class MajikMessage {
2163
2051
  return MajikSignature.getSealInfo(file, options);
2164
2052
  }
2165
2053
  catch (err) {
2166
- this.emit("error", err, { context: "getSealInfo" });
2054
+ this._emit("error", err, { context: "getSealInfo" });
2167
2055
  throw err;
2168
2056
  }
2169
2057
  }
@@ -2176,7 +2064,7 @@ export class MajikMessage {
2176
2064
  return MajikSignature.isSealed(file, options);
2177
2065
  }
2178
2066
  catch (err) {
2179
- this.emit("error", err, { context: "isSealed" });
2067
+ this._emit("error", err, { context: "isSealed" });
2180
2068
  throw err;
2181
2069
  }
2182
2070
  }
@@ -2197,14 +2085,14 @@ export class MajikMessage {
2197
2085
  }
2198
2086
  // Option B: contact ID looked up from the contact directory
2199
2087
  if (options.contactID) {
2200
- const contact = this.contacts.getContact(options.contactID);
2088
+ const contact = this._contacts.getContact(options.contactID);
2201
2089
  if (!contact) {
2202
2090
  throw new Error(`No contact found for id "${options.contactID}"`);
2203
2091
  }
2204
2092
  // Own accounts are in the keystore — get their signing keys directly
2205
2093
  const ownAccount = this.getOwnAccountById(options.contactID);
2206
2094
  if (ownAccount) {
2207
- const key = MajikKeyStore.get(options.contactID);
2095
+ const key = this.keyManager.get(options.contactID);
2208
2096
  if (key?.hasSigningKeys) {
2209
2097
  return MajikSignature.publicKeysFromMajikKey(key);
2210
2098
  }
@@ -2222,7 +2110,7 @@ export class MajikMessage {
2222
2110
  }
2223
2111
  // Option C: raw base64 public key — look up via contact directory
2224
2112
  if (options.publicKeyBase64) {
2225
- const contact = await this.contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
2113
+ const contact = await this._contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
2226
2114
  if (!contact) {
2227
2115
  throw new Error(`No contact found for public key "${options.publicKeyBase64}"`);
2228
2116
  }
@@ -2237,182 +2125,45 @@ export class MajikMessage {
2237
2125
  }
2238
2126
  return null;
2239
2127
  }
2240
- // ── Serialization ─────────────────────────────────────────────────────────
2241
- async toJSON() {
2242
- const json = {
2243
- id: this.id,
2244
- contacts: await this.contacts.toJSON(),
2245
- envelopeCache: this.envelopeCache.toJSON(),
2246
- };
2247
- try {
2248
- const accounts = [];
2249
- for (const id of this.ownAccountsOrder) {
2250
- const acct = this.ownAccounts.get(id);
2251
- if (acct)
2252
- accounts.push(await acct.toJSON());
2253
- }
2254
- json.ownAccounts = { accounts, order: [...this.ownAccountsOrder] };
2255
- }
2256
- catch (e) {
2257
- console.warn("Failed to serialize ownAccounts:", e);
2258
- }
2259
- return json;
2260
- }
2261
- static async fromJSON(json) {
2262
- // const migratedJSON = migrateMajikMessageJSON(json);
2263
- // ── Step 2: restore MajikContactManager (directory + groups together) ─
2264
- const contactManager = await MajikContactManager.fromJSON(json.contacts);
2265
- const envelopeCache = EnvelopeCache.fromJSON(json.envelopeCache);
2266
- const instance = new this({ contactManager, envelopeCache }, json.id);
2128
+ // ==========================================================================
2129
+ // ── RESET ─────────────────────────────────────────────────────────────────
2130
+ // ==========================================================================
2131
+ /**
2132
+ * Wipe all data from every adapter and reset in-memory state.
2133
+ * The client remains usable — call hydrate() or add new accounts after reset.
2134
+ */
2135
+ async resetData() {
2267
2136
  try {
2268
- if (json.ownAccounts && Array.isArray(json.ownAccounts.accounts)) {
2269
- for (const acct of json.ownAccounts.accounts) {
2270
- try {
2271
- const raw = base64ToArrayBuffer(acct.publicKeyBase64);
2272
- const publicKey = await crypto.subtle.importKey("raw", raw, KEY_ALGO, true, []);
2273
- const contact = MajikContact.create(acct.id, publicKey, acct.fingerprint, acct.meta);
2274
- instance.ownAccounts.set(contact.id, contact);
2275
- }
2276
- catch (e) {
2277
- console.info("Fallback restoring own account (raw-key wrapper)", acct.id, e);
2278
- }
2279
- }
2280
- if (Array.isArray(json.ownAccounts.order)) {
2281
- instance.ownAccountsOrder = [...json.ownAccounts.order];
2282
- }
2283
- // Fallback: populate from contacts if accounts array failed
2284
- if (instance.ownAccounts.size === 0) {
2285
- for (const id of instance.ownAccountsOrder) {
2286
- const c = instance.contacts.getContact(id);
2287
- if (c)
2288
- instance.ownAccounts.set(id, c);
2289
- }
2290
- }
2291
- // Ensure own accounts are in contacts
2292
- instance.ownAccountsOrder.forEach((id) => {
2293
- const c = instance.ownAccounts.get(id);
2294
- if (c && !instance.contacts.hasContact(c.id)) {
2295
- instance.contacts.addContact(c);
2296
- }
2297
- });
2137
+ await this._keys.adapter.clear();
2138
+ await this._contacts.clear();
2139
+ await this._state.clear();
2140
+ if (this._db) {
2141
+ await this._db.vacuum();
2142
+ await this._db.optimize();
2298
2143
  }
2299
- }
2300
- catch (e) {
2301
- console.warn("Error restoring ownAccounts:", e);
2302
- }
2303
- return instance;
2304
- }
2305
- // ── Persistence ───────────────────────────────────────────────────────────
2306
- attachAutosaveHandlers() {
2307
- if (typeof window === "undefined")
2308
- return;
2309
- try {
2310
- window.addEventListener("beforeunload", () => void this.saveState());
2311
- }
2312
- catch {
2313
- /* ignore */
2314
- }
2315
- this.startAutosave();
2316
- }
2317
- startAutosave() {
2318
- if (this.autosaveIntervalId || typeof window === "undefined")
2319
- return;
2320
- this.autosaveIntervalId = window.setInterval(() => void this.saveState(), this.autosaveIntervalMs);
2321
- }
2322
- stopAutosave() {
2323
- if (!this.autosaveIntervalId || typeof window === "undefined")
2324
- return;
2325
- window.clearInterval(this.autosaveIntervalId);
2326
- this.autosaveIntervalId = null;
2327
- }
2328
- scheduleAutosave() {
2329
- if (typeof window === "undefined")
2330
- return;
2331
- if (this.autosaveTimer)
2332
- window.clearTimeout(this.autosaveTimer);
2333
- this.autosaveTimer = window.setTimeout(() => {
2334
- void this.saveState();
2335
- this.autosaveTimer = null;
2336
- }, this.autosaveDebounceMs);
2337
- }
2338
- async saveState() {
2339
- try {
2340
- const json = await this.toJSON();
2341
- await idbSaveBlob("majik-message-state", autoSaveMajikFileData(json), this.userProfile);
2342
- }
2343
- catch (err) {
2344
- console.error("Failed to save MajikMessage state:", err);
2345
- }
2346
- }
2347
- async loadState() {
2348
- try {
2349
- const saved = await idbLoadBlob("majik-message-state", this.userProfile);
2350
- if (!saved?.data)
2351
- return;
2352
- const loaded = await loadSavedMajikFileData(saved.data);
2353
- // Pass raw parsed object — fromJSON handles migration internally
2354
- const restored = await MajikMessage.fromJSON(loaded.j);
2355
- this.id = restored.id;
2356
- this.contacts = restored.contacts;
2357
- this.envelopeCache = restored.envelopeCache;
2358
- this.ownAccounts = restored.ownAccounts;
2359
- this.ownAccountsOrder = [...restored.ownAccountsOrder];
2144
+ this._ownAccounts.clear();
2145
+ this._ownAccountsOrder = [];
2146
+ this._keys = new MajikKeyManager(this._keys.adapter);
2147
+ this._emit("active-account-change", null);
2360
2148
  }
2361
2149
  catch (err) {
2362
- console.error("Failed to load MajikMessage state:", err);
2150
+ throw new Error(`Failed to reset data: ${err instanceof Error ? err.message : err}`);
2363
2151
  }
2364
2152
  }
2365
- static async loadOrCreate(config, userProfile = "default") {
2366
- try {
2367
- const saved = await idbLoadBlob("majik-message-state", userProfile);
2368
- if (saved?.data) {
2369
- const loaded = await loadSavedMajikFileData(saved.data);
2370
- const instance = (await this.fromJSON(loaded.j));
2371
- instance.attachAutosaveHandlers();
2372
- return instance;
2373
- }
2374
- }
2375
- catch (err) {
2376
- console.warn("Error loading saved MajikMessage state:", err);
2153
+ // ==========================================================================
2154
+ // ── PRIVATE HELPERS ───────────────────────────────────────────────────────
2155
+ // ==========================================================================
2156
+ _registerOwnAccount(contact) {
2157
+ if (!this._ownAccounts.has(contact.id)) {
2158
+ this._ownAccounts.set(contact.id, contact);
2159
+ this._ownAccountsOrder.push(contact.id);
2160
+ this._scheduleOrderSave();
2377
2161
  }
2378
- const created = new this(config);
2379
- await created.saveState();
2380
- created.attachAutosaveHandlers();
2381
- return created;
2382
- }
2383
- async resetData(userProfile = "default") {
2384
- try {
2385
- await this.clearCachedEnvelopes();
2386
- for (const id of [...this.ownAccountsOrder]) {
2387
- await MajikKeyStore.deleteIdentity(id).catch(() => { });
2388
- }
2389
- this.ownAccounts.clear();
2390
- this.ownAccountsOrder = [];
2391
- try {
2392
- this.contacts.clear();
2393
- }
2394
- catch {
2395
- /* ignore */
2396
- }
2397
- try {
2398
- await MajikKeyStore.deleteAll();
2399
- }
2400
- catch {
2401
- /* ignore */
2402
- }
2403
- this.id = arrayToBase64(randomBytes(32));
2404
- try {
2405
- await clearAllBlobs(userProfile);
2406
- }
2407
- catch {
2408
- /* ignore */
2409
- }
2410
- this.stopAutosave();
2411
- this.startAutosave();
2412
- this.emit("active-account-change", null);
2162
+ if (!this._contacts.hasContact(contact.id)) {
2163
+ this._contacts.addContact(contact);
2413
2164
  }
2414
- catch (err) {
2415
- throw new Error(`Failed to reset data: ${err instanceof Error ? err.message : err}`);
2165
+ if (!this.getActiveAccount()) {
2166
+ void this.setActiveAccount(contact.id, true);
2416
2167
  }
2417
2168
  }
2418
2169
  }