@majikah/majik-message 0.3.6 → 0.3.8

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 (67) 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 +99 -185
  9. package/dist/core/contacts/majik-contact-manager.js +469 -289
  10. package/dist/core/contacts/types.d.ts +1 -0
  11. package/dist/core/crypto/keystore-manager.d.ts +166 -0
  12. package/dist/core/crypto/keystore-manager.js +371 -0
  13. package/dist/core/storage/chats/_types.d.ts +8 -0
  14. package/dist/core/storage/chats/_types.js +1 -0
  15. package/dist/core/storage/chats/adapter-idb.d.ts +3 -0
  16. package/dist/core/storage/chats/adapter-idb.js +5 -0
  17. package/dist/core/storage/chats/adapter-memory.d.ts +23 -0
  18. package/dist/core/storage/chats/adapter-memory.js +44 -0
  19. package/dist/core/storage/chats/adapter-sql.d.ts +17 -0
  20. package/dist/core/storage/chats/adapter-sql.js +85 -0
  21. package/dist/core/storage/client-state/_types.d.ts +37 -0
  22. package/dist/core/storage/client-state/_types.js +16 -0
  23. package/dist/core/storage/client-state/adapter-idb.d.ts +17 -0
  24. package/dist/core/storage/client-state/adapter-idb.js +19 -0
  25. package/dist/core/storage/client-state/adapter-memory.d.ts +20 -0
  26. package/dist/core/storage/client-state/adapter-memory.js +44 -0
  27. package/dist/core/storage/client-state/adapter-sql.d.ts +41 -0
  28. package/dist/core/storage/client-state/adapter-sql.js +104 -0
  29. package/dist/core/storage/contact-directory/contacts/_types.d.ts +3 -0
  30. package/dist/core/storage/contact-directory/contacts/_types.js +1 -0
  31. package/dist/core/storage/contact-directory/contacts/adapter-idb.d.ts +3 -0
  32. package/dist/core/storage/contact-directory/contacts/adapter-idb.js +5 -0
  33. package/dist/core/storage/contact-directory/contacts/adapter-memory.d.ts +14 -0
  34. package/dist/core/storage/contact-directory/contacts/adapter-memory.js +32 -0
  35. package/dist/core/storage/contact-directory/contacts/adapter-sql.d.ts +18 -0
  36. package/dist/core/storage/contact-directory/contacts/adapter-sql.js +97 -0
  37. package/dist/core/storage/contact-directory/groups/_types.d.ts +3 -0
  38. package/dist/core/storage/contact-directory/groups/_types.js +1 -0
  39. package/dist/core/storage/contact-directory/groups/adapter-idb.d.ts +3 -0
  40. package/dist/core/storage/contact-directory/groups/adapter-idb.js +5 -0
  41. package/dist/core/storage/contact-directory/groups/adapter-memory.d.ts +14 -0
  42. package/dist/core/storage/contact-directory/groups/adapter-memory.js +32 -0
  43. package/dist/core/storage/contact-directory/groups/adapter-sql.d.ts +16 -0
  44. package/dist/core/storage/contact-directory/groups/adapter-sql.js +72 -0
  45. package/dist/core/storage/idb-adapter.d.ts +21 -0
  46. package/dist/core/storage/idb-adapter.js +107 -0
  47. package/dist/core/storage/index.d.ts +24 -0
  48. package/dist/core/storage/index.js +19 -0
  49. package/dist/core/storage/keystore/_types.d.ts +3 -0
  50. package/dist/core/storage/keystore/_types.js +1 -0
  51. package/dist/core/storage/keystore/adapter-idb.d.ts +3 -0
  52. package/dist/core/storage/keystore/adapter-idb.js +5 -0
  53. package/dist/core/storage/keystore/adapter-memory.d.ts +14 -0
  54. package/dist/core/storage/keystore/adapter-memory.js +32 -0
  55. package/dist/core/storage/keystore/adapter-sql.d.ts +18 -0
  56. package/dist/core/storage/keystore/adapter-sql.js +93 -0
  57. package/dist/core/storage/sql-db-manager.d.ts +13 -0
  58. package/dist/core/storage/sql-db-manager.js +59 -0
  59. package/dist/core/storage/sql-schema.d.ts +25 -0
  60. package/dist/core/storage/sql-schema.js +122 -0
  61. package/dist/core/storage/storage-adapter.d.ts +22 -0
  62. package/dist/core/storage/storage-adapter.js +1 -0
  63. package/dist/index.d.ts +2 -4
  64. package/dist/index.js +2 -4
  65. package/dist/majik-message.d.ts +114 -174
  66. package/dist/majik-message.js +449 -675
  67. package/package.json +5 -6
@@ -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,244 @@ 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
+ getContactsByID(ids, strict = false) {
412
+ if (!ids?.length)
413
+ throw new Error("At least 1 id is required");
414
+ return this._contacts.getContactsByIds(ids, strict);
415
+ }
416
+ async getContactsByPublicKey(publicKeys) {
417
+ if (!publicKeys?.length)
418
+ throw new Error("At least 1 public key is required");
419
+ return await this._contacts.getContactsByPublicKeys(publicKeys);
420
+ }
421
+ async getMajikRecipientsByPublicKey(publicKeys, strict) {
422
+ return await this._contacts.getMajikRecipients("public_key", publicKeys, strict);
423
+ }
424
+ async getExpectedSignersByPublicKey(publicKeys, strict) {
425
+ return await this._contacts.getExpectedSigners("public_key", publicKeys, strict);
426
+ }
427
+ async exportContactAsJSON(id) {
428
+ if (!id?.trim())
429
+ throw new Error("Invalid contact ID");
430
+ return this._contacts.exportContactAsJSON(id);
431
+ }
432
+ async exportContactAsString(id) {
433
+ if (!id?.trim())
434
+ throw new Error("Invalid contact ID");
435
+ return this._contacts.exportContactAsString(id);
315
436
  }
316
437
  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
- }
438
+ if (!jsonStr?.trim())
439
+ throw new Error("Invalid contact JSON");
440
+ return this._contacts.importContactFromJSON(jsonStr);
347
441
  }
348
442
  async importContactFromString(base64Str) {
349
- try {
350
- const parsedContact = await this.importContactCompressed(base64Str);
351
- this.addContact(parsedContact);
352
- return { success: true, message: "Contact imported successfully" };
443
+ if (!base64Str?.trim())
444
+ throw new Error("Invalid contact string");
445
+ const response = await this._contacts.importContactFromString(base64Str);
446
+ if (response.success) {
447
+ this._emit("new-contact", response.data);
353
448
  }
354
- catch (err) {
355
- return {
356
- success: false,
357
- message: err instanceof Error ? err.message : "Unknown error",
358
- };
449
+ else {
450
+ this._emit("error", response.message);
359
451
  }
452
+ return response;
360
453
  }
361
454
  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);
455
+ if (!contact?.id?.trim())
456
+ throw new Error("Invalid contact");
457
+ return this._contacts.exportContactCompressed(contact);
387
458
  }
388
459
  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
- });
460
+ if (!base64Str?.trim())
461
+ throw new Error("Invalid contact string");
462
+ return this._contacts.importContactCompressed(base64Str);
413
463
  }
414
- addContact(contact) {
464
+ async addContact(contact) {
415
465
  if (!contact?.id ||
416
466
  !contact?.publicKey ||
417
467
  !contact?.fingerprint ||
418
468
  !contact?.mlKey) {
419
- throw new Error("Invalid contact JSON");
469
+ throw new Error("Invalid contact — missing required fields");
420
470
  }
421
- this.contacts.addContact(contact);
422
- this.emit("new-contact", contact);
423
- this.scheduleAutosave();
471
+ await this._contacts.addContact(contact);
472
+ this._emit("new-contact", contact);
424
473
  }
425
- removeContact(id) {
426
- const result = this.contacts.removeContact(id);
474
+ async removeContact(id) {
475
+ const result = await this._contacts.removeContact(id);
427
476
  if (!result.success)
428
477
  throw new Error(result.message);
429
- this.emit("removed-contact", id);
430
- this.scheduleAutosave();
431
- }
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();
478
+ this._emit("removed-contact", id);
439
479
  }
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)
480
+ listContacts(includeOwnAccounts = false) {
481
+ const contacts = this._contacts.listContacts(true);
482
+ if (includeOwnAccounts)
447
483
  return contacts;
448
- const ownIds = new Set(this.listOwnAccounts(majikahOnly).map((a) => a.id));
484
+ const ownIds = new Set(this.listOwnAccounts().map((a) => a.id));
449
485
  return contacts.filter((c) => !ownIds.has(c.id));
450
486
  }
451
- isContactMajikahRegistered(id) {
452
- return this.contacts.isMajikahRegistered(id);
453
- }
454
- isContactMajikahIdentityChecked(id) {
455
- return this.contacts.isMajikahIdentityChecked(id);
456
- }
457
- setContactMajikahStatus(id, status) {
458
- this.contacts.setMajikahStatus(id, status);
459
- this.scheduleAutosave();
487
+ async updateContactMeta(id, meta) {
488
+ await this._contacts.updateContactMeta(id, meta);
460
489
  }
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();
490
+ async createGroup(id, name, meta, initialMemberIds) {
491
+ const newGroup = await this._contacts.createGroup(id, name, meta, initialMemberIds);
492
+ this._emit("new-contact-group", newGroup);
472
493
  return this;
473
494
  }
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();
495
+ async addGroup(group) {
496
+ await this._contacts.addGroup(group);
497
+ this._emit("new-contact-group", group);
482
498
  return this;
483
499
  }
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();
500
+ async removeGroup(id) {
501
+ const response = await this._contacts.removeGroup(id);
502
+ this._emit("removed-contact-group", response.data);
492
503
  return response;
493
504
  }
494
- /**
495
- * Returns a group by ID, or undefined if not found.
496
- */
497
505
  getContactGroup(id) {
498
- return this.contacts.getGroup(id);
506
+ return this._contacts.getGroup(id);
499
507
  }
500
- /**
501
- * Returns a group by ID. Throws if not found.
502
- */
503
508
  getGroupOrThrow(id) {
504
- return this.contacts.getGroupOrThrow(id);
509
+ return this._contacts.getGroupOrThrow(id);
505
510
  }
506
- /**
507
- * Returns true if a group with the given ID exists.
508
- */
509
511
  hasGroup(id) {
510
- return this.contacts.hasGroup(id);
512
+ return this._contacts.hasGroup(id);
511
513
  }
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
514
  listContactGroups(includeSystem = true, sortedByName = false) {
519
- return this.contacts.listGroups(includeSystem, sortedByName);
515
+ return this._contacts.listGroups(includeSystem, sortedByName);
520
516
  }
521
- /**
522
- * Returns only user-created groups (excludes Favorites and Blocked).
523
- * Sorted alphabetically by name.
524
- */
525
517
  listUserGroups(sortedByName = true) {
526
- return this.contacts.listGroups(false, sortedByName);
518
+ return this._contacts.listGroups(false, sortedByName);
527
519
  }
528
- /**
529
- * Returns only system groups (Favorites and Blocked).
530
- */
531
520
  listSystemGroups() {
532
- return this.contacts.listGroups(true).filter((g) => g.isSystem);
521
+ return this._contacts.listGroups(true).filter((g) => g.isSystem);
533
522
  }
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();
523
+ async updateGroupMeta(id, meta) {
524
+ const updatedGroup = await this._contacts.updateGroupMeta(id, meta);
525
+ this._emit("contact-group-change", updatedGroup);
542
526
  return this;
543
527
  }
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();
528
+ async addContactToGroup(groupID, contactID) {
529
+ const updatedGroup = await this._contacts.addContactToGroup(groupID, contactID);
530
+ this._emit("contact-group-change", updatedGroup);
557
531
  return this;
558
532
  }
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();
533
+ async addContactsToGroup(groupID, contactIds) {
534
+ const updatedGroup = await this._contacts.addContactsToGroup(groupID, contactIds);
535
+ this._emit("contact-group-change", updatedGroup);
566
536
  return this;
567
537
  }
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();
538
+ async removeContactFromGroup(groupID, contactID) {
539
+ const updatedGroup = await this._contacts.removeContactFromGroup(groupID, contactID);
540
+ this._emit("contact-group-change", updatedGroup);
577
541
  return this;
578
542
  }
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();
543
+ async moveContactBetweenGroups(contactID, fromGroupId, toGroupId) {
544
+ const updatedGroup = await this._contacts.moveContactBetweenGroups(contactID, fromGroupId, toGroupId);
545
+ this._emit("contact-group-change", updatedGroup);
587
546
  return this;
588
547
  }
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
548
  getContactsInGroup(groupID) {
597
- return this.contacts.getContactsInGroup(groupID);
549
+ return this._contacts.getContactsInGroup(groupID);
598
550
  }
599
- /**
600
- * Returns hydrated contacts in the group, sorted by label (or ID if no label).
601
- */
602
551
  getContactsInGroupSorted(groupID) {
603
- return this.contacts.getContactsInGroupSorted(groupID);
552
+ return this._contacts.getContactsInGroupSorted(groupID);
604
553
  }
605
- /**
606
- * Returns true if the contact is a member of the given group.
607
- */
608
554
  isContactInGroup(groupID, contactID) {
609
- return this.contacts.isContactInGroup(groupID, contactID);
555
+ return this._contacts.isContactInGroup(groupID, contactID);
610
556
  }
611
- /**
612
- * Returns all groups the contact belongs to.
613
- */
614
557
  getGroupsForContact(contactID) {
615
- return this.contacts.getGroupsForContact(contactID);
558
+ return this._contacts.getGroupsForContact(contactID);
616
559
  }
617
- /**
618
- * Returns all group IDs the contact belongs to.
619
- */
620
560
  getGroupIdsForContact(contactID) {
621
- return this.contacts.getGroupIdsForContact(contactID);
561
+ return this._contacts.getGroupIdsForContact(contactID);
622
562
  }
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();
563
+ async addContactToFavorites(contactID) {
564
+ const updatedGroup = await this._contacts.addToFavorites(contactID);
565
+ this._emit("contact-group-change", updatedGroup);
633
566
  return this;
634
567
  }
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();
568
+ async removeContactFromFavorites(contactID) {
569
+ const updatedGroup = await this._contacts.removeFromFavorites(contactID);
570
+ this._emit("contact-group-change", updatedGroup);
642
571
  return this;
643
572
  }
644
- /**
645
- * Returns true if the contact is in the Favorites group.
646
- */
647
573
  isContactFavorite(contactID) {
648
- return this.contacts.isFavorite(contactID);
574
+ return this._contacts.isFavorite(contactID);
649
575
  }
650
- /**
651
- * Returns true if the contact is in the Blocked group.
652
- */
653
576
  isContactBlocked(contactID) {
654
- return this.contacts.isContactBlocked(contactID);
577
+ return this._contacts.isContactBlocked(contactID);
655
578
  }
656
- /**
657
- * Returns the Favorites system group instance.
658
- */
659
579
  getFavoritesGroup() {
660
- return this.contacts.getFavoritesGroup();
580
+ return this._contacts.getFavoritesGroup();
661
581
  }
662
- /**
663
- * Returns the Blocked system group instance.
664
- */
665
582
  getBlockedGroup() {
666
- return this.contacts.getBlockedGroup();
583
+ return this._contacts.getBlockedGroup();
667
584
  }
668
- /**
669
- * Returns all contacts in the Favorites group as hydrated MajikContact instances.
670
- */
671
585
  getFavoriteContacts() {
672
- return this.contacts.getContactsInGroup(this.contacts.getFavoritesGroup().id);
586
+ return this._contacts.getContactsInGroup(this._contacts.getFavoritesGroup().id);
673
587
  }
674
- /**
675
- * Returns all contacts in the Blocked group as hydrated MajikContact instances.
676
- */
677
588
  getBlockedContacts() {
678
- return this.contacts.getContactsInGroup(this.contacts.getBlockedGroup().id);
589
+ return this._contacts.getContactsInGroup(this._contacts.getBlockedGroup().id);
679
590
  }
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();
591
+ async clearDirectory() {
592
+ await this._contacts.clear();
690
593
  return this;
691
594
  }
595
+ resolveSignerLabel(signerId) {
596
+ const ownAccount = this._ownAccounts.get(signerId);
597
+ if (ownAccount?.meta?.label)
598
+ return ownAccount.meta.label;
599
+ const contact = this._contacts.getContact(signerId);
600
+ if (contact?.meta?.label)
601
+ return contact.meta.label;
602
+ return `${signerId.slice(0, 16)}…`;
603
+ }
692
604
  // ── Encryption / Decryption ───────────────────────────────────────────────
693
605
  /**
694
606
  * Compose and encrypt a message for one or more recipients.
@@ -710,8 +622,7 @@ export class MajikMessage {
710
622
  if (cache) {
711
623
  await this.envelopeCache.set(new MessageEnvelope(envelope.toBinary()), this._source);
712
624
  }
713
- this.scheduleAutosave();
714
- this.emit("envelope", envelope);
625
+ this._emit("envelope", envelope);
715
626
  return scannerString;
716
627
  }
717
628
  /**
@@ -764,7 +675,7 @@ export class MajikMessage {
764
675
  }
765
676
  catch (err) {
766
677
  console.warn("Error: ", err);
767
- this.emit("error", err, { context: "encryptTextForScanner" });
678
+ this._emit("error", err, { context: "encryptTextForScanner" });
768
679
  return null;
769
680
  }
770
681
  }
@@ -829,7 +740,7 @@ export class MajikMessage {
829
740
  return { messageChat, scannerString };
830
741
  }
831
742
  catch (err) {
832
- this.emit("error", err, { context: "createEncryptedMajikMessageChat" });
743
+ this._emit("error", err, { context: "createEncryptedMajikMessageChat" });
833
744
  throw err;
834
745
  }
835
746
  }
@@ -848,7 +759,7 @@ export class MajikMessage {
848
759
  return await envelope.decrypt(identity);
849
760
  }
850
761
  catch (err) {
851
- this.emit("error", err, { context: "decryptMajikMessageChat" });
762
+ this._emit("error", err, { context: "decryptMajikMessageChat" });
852
763
  throw err;
853
764
  }
854
765
  }
@@ -889,7 +800,7 @@ export class MajikMessage {
889
800
  const activeId = this.getActiveAccount()?.id;
890
801
  if (!activeId)
891
802
  throw new Error("No active account — call setActiveAccount() first");
892
- const signingKey = MajikKeyStore.get(activeId);
803
+ const signingKey = this.keyManager.get(activeId);
893
804
  // ── 4. Build CreateOptions ─────────────────────────────────────────────
894
805
  const createOptions = {
895
806
  data,
@@ -1030,9 +941,9 @@ export class MajikMessage {
1030
941
  if (!id)
1031
942
  throw new Error("No active account — call setActiveAccount() first");
1032
943
  try {
1033
- await MajikKeyStore.ensureUnlocked(id);
944
+ await this.keyManager.ensureUnlocked(id);
1034
945
  // get() is safe after ensureUnlocked() — key is in the memory cache.
1035
- const key = MajikKeyStore.get(id);
946
+ const key = this.keyManager.get(id);
1036
947
  if (!key)
1037
948
  throw new Error(`Account not found in keystore: "${id}"`);
1038
949
  if (!key.hasSigningKeys) {
@@ -1045,7 +956,7 @@ export class MajikMessage {
1045
956
  });
1046
957
  }
1047
958
  catch (err) {
1048
- this.emit("error", err, { context: "signMajikFile" });
959
+ this._emit("error", err, { context: "signMajikFile" });
1049
960
  throw err;
1050
961
  }
1051
962
  }
@@ -1087,7 +998,7 @@ export class MajikMessage {
1087
998
  return file.verify(sig.extractPublicKeys());
1088
999
  }
1089
1000
  catch (err) {
1090
- this.emit("error", err, { context: "verifyMajikFile" });
1001
+ this._emit("error", err, { context: "verifyMajikFile" });
1091
1002
  throw err;
1092
1003
  }
1093
1004
  }
@@ -1136,7 +1047,7 @@ export class MajikMessage {
1136
1047
  return file.verifyBinary(decryptIdentity, sig.extractPublicKeys());
1137
1048
  }
1138
1049
  catch (err) {
1139
- this.emit("error", err, { context: "verifyMajikFileBinary" });
1050
+ this._emit("error", err, { context: "verifyMajikFileBinary" });
1140
1051
  throw err;
1141
1052
  }
1142
1053
  }
@@ -1170,7 +1081,7 @@ export class MajikMessage {
1170
1081
  return false;
1171
1082
  // get() checks the memory cache — no async needed since the account
1172
1083
  // must already be loaded to be the active account.
1173
- const key = MajikKeyStore.get(id);
1084
+ const key = this.keyManager.get(id);
1174
1085
  if (!key)
1175
1086
  return false;
1176
1087
  return key.fingerprint === sigInfo.signerId;
@@ -1448,7 +1359,7 @@ export class MajikMessage {
1448
1359
  const id = accountId ?? this.getActiveAccount()?.id;
1449
1360
  if (!id)
1450
1361
  return false;
1451
- const key = MajikKeyStore.get(id);
1362
+ const key = this.keyManager.get(id);
1452
1363
  return key?.hasSigningKeys === true;
1453
1364
  }
1454
1365
  // ── Envelope Cache ────────────────────────────────────────────────────────
@@ -1459,30 +1370,30 @@ export class MajikMessage {
1459
1370
  const response = await this.envelopeCache.clear();
1460
1371
  if (!response?.success)
1461
1372
  throw new Error(response.message);
1462
- this.scheduleAutosave();
1373
+ this._scheduleOrderSave();
1463
1374
  return response.success;
1464
1375
  }
1465
1376
  // ── Identity / Passphrase ─────────────────────────────────────────────────
1466
1377
  /**
1467
1378
  * Ensure an identity is unlocked.
1468
- * Delegates entirely to MajikKeyStore.ensureUnlocked() — passphrase prompting
1379
+ * Delegates entirely to this.keyManager.ensureUnlocked() — passphrase prompting
1469
1380
  * is handled there via onUnlockRequested or the optional promptFn.
1470
1381
  */
1471
1382
  async ensureIdentityUnlocked(id, promptFn) {
1472
- return MajikKeyStore.ensureUnlocked(id, promptFn);
1383
+ return this.keyManager.ensureUnlocked(id, promptFn);
1473
1384
  }
1474
1385
  async isPassphraseValid(passphrase, id) {
1475
1386
  const target = id ? this.getOwnAccountById(id) : this.getActiveAccount();
1476
1387
  if (!target)
1477
1388
  return false;
1478
- return MajikKeyStore.isPassphraseValid(target.id, passphrase);
1389
+ return this.keyManager.isPassphraseValid(target.id, passphrase);
1479
1390
  }
1480
1391
  // ── Events ────────────────────────────────────────────────────────────────
1481
1392
  on(event, callback) {
1482
- this.listeners.get(event)?.push(callback);
1393
+ this._listeners.get(event)?.push(callback);
1483
1394
  }
1484
1395
  off(event, callback) {
1485
- const cbs = this.listeners.get(event);
1396
+ const cbs = this._listeners.get(event);
1486
1397
  if (!cbs?.length)
1487
1398
  return;
1488
1399
  if (callback) {
@@ -1491,11 +1402,11 @@ export class MajikMessage {
1491
1402
  cbs.splice(i, 1);
1492
1403
  }
1493
1404
  else {
1494
- this.listeners.set(event, []);
1405
+ this._listeners.set(event, []);
1495
1406
  }
1496
1407
  }
1497
- emit(event, ...args) {
1498
- this.listeners.get(event)?.forEach((cb) => cb(...args));
1408
+ _emit(event, ...args) {
1409
+ this._listeners.get(event)?.forEach((cb) => cb(...args));
1499
1410
  }
1500
1411
  // ── Content & File Signing ────────────────────────────────────────────────
1501
1412
  /**
@@ -1514,8 +1425,8 @@ export class MajikMessage {
1514
1425
  if (!id)
1515
1426
  throw new Error("No active account — call setActiveAccount() first");
1516
1427
  try {
1517
- await MajikKeyStore.ensureUnlocked(id);
1518
- const key = MajikKeyStore.get(id);
1428
+ await this.keyManager.ensureUnlocked(id);
1429
+ const key = this.keyManager.get(id);
1519
1430
  if (!key)
1520
1431
  throw new Error(`Account not found in keystore: "${id}"`);
1521
1432
  if (!key.hasSigningKeys) {
@@ -1528,7 +1439,7 @@ export class MajikMessage {
1528
1439
  });
1529
1440
  }
1530
1441
  catch (err) {
1531
- this.emit("error", err, { context: "signContent" });
1442
+ this._emit("error", err, { context: "signContent" });
1532
1443
  throw err;
1533
1444
  }
1534
1445
  }
@@ -1551,8 +1462,8 @@ export class MajikMessage {
1551
1462
  if (!id)
1552
1463
  throw new Error("No active account — call setActiveAccount() first");
1553
1464
  try {
1554
- await MajikKeyStore.ensureUnlocked(id);
1555
- const key = MajikKeyStore.get(id);
1465
+ await this.keyManager.ensureUnlocked(id);
1466
+ const key = this.keyManager.get(id);
1556
1467
  if (!key)
1557
1468
  throw new Error(`Account not found in keystore: "${id}"`);
1558
1469
  if (!key.hasSigningKeys) {
@@ -1567,7 +1478,7 @@ export class MajikMessage {
1567
1478
  });
1568
1479
  }
1569
1480
  catch (err) {
1570
- this.emit("error", err, { context: "signFile" });
1481
+ this._emit("error", err, { context: "signFile" });
1571
1482
  throw err;
1572
1483
  }
1573
1484
  }
@@ -1596,8 +1507,8 @@ export class MajikMessage {
1596
1507
  const id = options?.accountId ?? this.getActiveAccount()?.id;
1597
1508
  if (!id)
1598
1509
  throw new Error("No active account — call setActiveAccount() first");
1599
- await MajikKeyStore.ensureUnlocked(id);
1600
- const key = MajikKeyStore.get(id);
1510
+ await this.keyManager.ensureUnlocked(id);
1511
+ const key = this.keyManager.get(id);
1601
1512
  if (!key)
1602
1513
  throw new Error(`Account not found in keystore: "${id}"`);
1603
1514
  if (!key.hasSigningKeys) {
@@ -1621,7 +1532,7 @@ export class MajikMessage {
1621
1532
  };
1622
1533
  }
1623
1534
  catch (err) {
1624
- this.emit("error", err, { context: "batchSignFiles" });
1535
+ this._emit("error", err, { context: "batchSignFiles" });
1625
1536
  return {
1626
1537
  blob: null,
1627
1538
  signature: null,
@@ -1670,7 +1581,7 @@ export class MajikMessage {
1670
1581
  return MajikSignature.verify(content, sig, sig.extractPublicKeys());
1671
1582
  }
1672
1583
  catch (err) {
1673
- this.emit("error", err, { context: "verifyContent" });
1584
+ this._emit("error", err, { context: "verifyContent" });
1674
1585
  throw err;
1675
1586
  }
1676
1587
  }
@@ -1726,7 +1637,7 @@ export class MajikMessage {
1726
1637
  return results[0];
1727
1638
  }
1728
1639
  catch (err) {
1729
- this.emit("error", err, { context: "verifyFile" });
1640
+ this._emit("error", err, { context: "verifyFile" });
1730
1641
  throw err;
1731
1642
  }
1732
1643
  }
@@ -1795,7 +1706,7 @@ export class MajikMessage {
1795
1706
  };
1796
1707
  }
1797
1708
  catch (err) {
1798
- this.emit("error", err, { context: "batchVerifyFiles" });
1709
+ this._emit("error", err, { context: "batchVerifyFiles" });
1799
1710
  return {
1800
1711
  valid: false,
1801
1712
  signerId: undefined,
@@ -1824,7 +1735,7 @@ export class MajikMessage {
1824
1735
  return MajikSignature.extractFrom(file, options);
1825
1736
  }
1826
1737
  catch (err) {
1827
- this.emit("error", err, { context: "extractSignature" });
1738
+ this._emit("error", err, { context: "extractSignature" });
1828
1739
  throw err;
1829
1740
  }
1830
1741
  }
@@ -1842,7 +1753,7 @@ export class MajikMessage {
1842
1753
  return MajikSignature.stripFrom(file, options);
1843
1754
  }
1844
1755
  catch (err) {
1845
- this.emit("error", err, { context: "stripSignature" });
1756
+ this._emit("error", err, { context: "stripSignature" });
1846
1757
  throw err;
1847
1758
  }
1848
1759
  }
@@ -1860,7 +1771,7 @@ export class MajikMessage {
1860
1771
  return MajikSignature.isSigned(file, options);
1861
1772
  }
1862
1773
  catch (err) {
1863
- this.emit("error", err, { context: "isFileSigned" });
1774
+ this._emit("error", err, { context: "isFileSigned" });
1864
1775
  throw err;
1865
1776
  }
1866
1777
  }
@@ -1878,7 +1789,7 @@ export class MajikMessage {
1878
1789
  const id = accountId ?? this.getActiveAccount()?.id;
1879
1790
  if (!id)
1880
1791
  throw new Error("No active account — call setActiveAccount() first");
1881
- const key = MajikKeyStore.get(id);
1792
+ const key = this.keyManager.get(id);
1882
1793
  if (!key)
1883
1794
  throw new Error(`Account not found in keystore: "${id}"`);
1884
1795
  if (!key.hasSigningKeys) {
@@ -1936,7 +1847,7 @@ export class MajikMessage {
1936
1847
  return MajikSignature.getAllowlist(file, options);
1937
1848
  }
1938
1849
  catch (err) {
1939
- this.emit("error", err, { context: "getAllowlist" });
1850
+ this._emit("error", err, { context: "getAllowlist" });
1940
1851
  throw err;
1941
1852
  }
1942
1853
  }
@@ -1953,7 +1864,7 @@ export class MajikMessage {
1953
1864
  return MajikSignature.canSign(file, key, options);
1954
1865
  }
1955
1866
  catch (err) {
1956
- this.emit("error", err, { context: "canSign" });
1867
+ this._emit("error", err, { context: "canSign" });
1957
1868
  throw err;
1958
1869
  }
1959
1870
  }
@@ -1967,7 +1878,7 @@ export class MajikMessage {
1967
1878
  return MajikSignature.isMultiSig(file, options);
1968
1879
  }
1969
1880
  catch (err) {
1970
- this.emit("error", err, { context: "isMultiSig" });
1881
+ this._emit("error", err, { context: "isMultiSig" });
1971
1882
  throw err;
1972
1883
  }
1973
1884
  }
@@ -1994,7 +1905,7 @@ export class MajikMessage {
1994
1905
  return MajikSignature.getSignatories(file, options, filter);
1995
1906
  }
1996
1907
  catch (err) {
1997
- this.emit("error", err, { context: "getSignatories" });
1908
+ this._emit("error", err, { context: "getSignatories" });
1998
1909
  throw err;
1999
1910
  }
2000
1911
  }
@@ -2007,7 +1918,7 @@ export class MajikMessage {
2007
1918
  return MajikSignature.getSignedSignatories(file, options);
2008
1919
  }
2009
1920
  catch (err) {
2010
- this.emit("error", err, { context: "getSignedSignatories" });
1921
+ this._emit("error", err, { context: "getSignedSignatories" });
2011
1922
  throw err;
2012
1923
  }
2013
1924
  }
@@ -2020,7 +1931,7 @@ export class MajikMessage {
2020
1931
  return MajikSignature.getPendingSignatories(file, options);
2021
1932
  }
2022
1933
  catch (err) {
2023
- this.emit("error", err, { context: "getPendingSignatories" });
1934
+ this._emit("error", err, { context: "getPendingSignatories" });
2024
1935
  throw err;
2025
1936
  }
2026
1937
  }
@@ -2033,7 +1944,7 @@ export class MajikMessage {
2033
1944
  return MajikSignature.getAllSignatories(file, options);
2034
1945
  }
2035
1946
  catch (err) {
2036
- this.emit("error", err, { context: "getAllSignatories" });
1947
+ this._emit("error", err, { context: "getAllSignatories" });
2037
1948
  throw err;
2038
1949
  }
2039
1950
  }
@@ -2050,7 +1961,7 @@ export class MajikMessage {
2050
1961
  return MajikSignature.getIssuer(file, options);
2051
1962
  }
2052
1963
  catch (err) {
2053
- this.emit("error", err, { context: "getIssuer" });
1964
+ this._emit("error", err, { context: "getIssuer" });
2054
1965
  throw err;
2055
1966
  }
2056
1967
  }
@@ -2077,7 +1988,7 @@ export class MajikMessage {
2077
1988
  return MajikSignature.extractFrom(file, options);
2078
1989
  }
2079
1990
  catch (err) {
2080
- this.emit("error", err, { context: "getFileSignatureInfo" });
1991
+ this._emit("error", err, { context: "getFileSignatureInfo" });
2081
1992
  throw err;
2082
1993
  }
2083
1994
  }
@@ -2098,7 +2009,7 @@ export class MajikMessage {
2098
2009
  return MajikSignature.getEnvelopeInfo(file, options);
2099
2010
  }
2100
2011
  catch (err) {
2101
- this.emit("error", err, { context: "getEnvelopeInfo" });
2012
+ this._emit("error", err, { context: "getEnvelopeInfo" });
2102
2013
  throw err;
2103
2014
  }
2104
2015
  }
@@ -2119,7 +2030,7 @@ export class MajikMessage {
2119
2030
  if (!id)
2120
2031
  throw new Error("No active account — call setActiveAccount() first");
2121
2032
  try {
2122
- const key = MajikKeyStore.get(id);
2033
+ const key = this.keyManager.get(id);
2123
2034
  if (!key)
2124
2035
  throw new Error(`Account not found in keystore: "${id}"`);
2125
2036
  return MajikSignature.seal(file, key, {
@@ -2128,7 +2039,7 @@ export class MajikMessage {
2128
2039
  });
2129
2040
  }
2130
2041
  catch (err) {
2131
- this.emit("error", err, { context: "seal" });
2042
+ this._emit("error", err, { context: "seal" });
2132
2043
  throw err;
2133
2044
  }
2134
2045
  }
@@ -2146,7 +2057,7 @@ export class MajikMessage {
2146
2057
  return MajikSignature.verifySeal(file, options);
2147
2058
  }
2148
2059
  catch (err) {
2149
- this.emit("error", err, { context: "verifySeal" });
2060
+ this._emit("error", err, { context: "verifySeal" });
2150
2061
  throw err;
2151
2062
  }
2152
2063
  }
@@ -2163,7 +2074,7 @@ export class MajikMessage {
2163
2074
  return MajikSignature.getSealInfo(file, options);
2164
2075
  }
2165
2076
  catch (err) {
2166
- this.emit("error", err, { context: "getSealInfo" });
2077
+ this._emit("error", err, { context: "getSealInfo" });
2167
2078
  throw err;
2168
2079
  }
2169
2080
  }
@@ -2176,7 +2087,7 @@ export class MajikMessage {
2176
2087
  return MajikSignature.isSealed(file, options);
2177
2088
  }
2178
2089
  catch (err) {
2179
- this.emit("error", err, { context: "isSealed" });
2090
+ this._emit("error", err, { context: "isSealed" });
2180
2091
  throw err;
2181
2092
  }
2182
2093
  }
@@ -2197,14 +2108,14 @@ export class MajikMessage {
2197
2108
  }
2198
2109
  // Option B: contact ID looked up from the contact directory
2199
2110
  if (options.contactID) {
2200
- const contact = this.contacts.getContact(options.contactID);
2111
+ const contact = this._contacts.getContact(options.contactID);
2201
2112
  if (!contact) {
2202
2113
  throw new Error(`No contact found for id "${options.contactID}"`);
2203
2114
  }
2204
2115
  // Own accounts are in the keystore — get their signing keys directly
2205
2116
  const ownAccount = this.getOwnAccountById(options.contactID);
2206
2117
  if (ownAccount) {
2207
- const key = MajikKeyStore.get(options.contactID);
2118
+ const key = this.keyManager.get(options.contactID);
2208
2119
  if (key?.hasSigningKeys) {
2209
2120
  return MajikSignature.publicKeysFromMajikKey(key);
2210
2121
  }
@@ -2222,7 +2133,7 @@ export class MajikMessage {
2222
2133
  }
2223
2134
  // Option C: raw base64 public key — look up via contact directory
2224
2135
  if (options.publicKeyBase64) {
2225
- const contact = await this.contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
2136
+ const contact = await this._contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
2226
2137
  if (!contact) {
2227
2138
  throw new Error(`No contact found for public key "${options.publicKeyBase64}"`);
2228
2139
  }
@@ -2237,182 +2148,45 @@ export class MajikMessage {
2237
2148
  }
2238
2149
  return null;
2239
2150
  }
2240
- // ── Serialization ─────────────────────────────────────────────────────────
2241
- async toJSON() {
2242
- const json = {
2243
- id: this.id,
2244
- contacts: await this.contacts.toJSON(),
2245
- envelopeCache: this.envelopeCache.toJSON(),
2246
- };
2151
+ // ==========================================================================
2152
+ // ── RESET ─────────────────────────────────────────────────────────────────
2153
+ // ==========================================================================
2154
+ /**
2155
+ * Wipe all data from every adapter and reset in-memory state.
2156
+ * The client remains usable — call hydrate() or add new accounts after reset.
2157
+ */
2158
+ async resetData() {
2247
2159
  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());
2160
+ await this._keys.adapter.clear();
2161
+ await this._contacts.clear();
2162
+ await this._state.clear();
2163
+ if (this._db) {
2164
+ await this._db.vacuum();
2165
+ await this._db.optimize();
2253
2166
  }
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);
2267
- 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
- });
2298
- }
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];
2167
+ this._ownAccounts.clear();
2168
+ this._ownAccountsOrder = [];
2169
+ this._keys = new MajikKeyManager(this._keys.adapter);
2170
+ this._emit("active-account-change", null);
2360
2171
  }
2361
2172
  catch (err) {
2362
- console.error("Failed to load MajikMessage state:", err);
2173
+ throw new Error(`Failed to reset data: ${err instanceof Error ? err.message : err}`);
2363
2174
  }
2364
2175
  }
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);
2176
+ // ==========================================================================
2177
+ // ── PRIVATE HELPERS ───────────────────────────────────────────────────────
2178
+ // ==========================================================================
2179
+ _registerOwnAccount(contact) {
2180
+ if (!this._ownAccounts.has(contact.id)) {
2181
+ this._ownAccounts.set(contact.id, contact);
2182
+ this._ownAccountsOrder.push(contact.id);
2183
+ this._scheduleOrderSave();
2377
2184
  }
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);
2185
+ if (!this._contacts.hasContact(contact.id)) {
2186
+ this._contacts.addContact(contact);
2413
2187
  }
2414
- catch (err) {
2415
- throw new Error(`Failed to reset data: ${err instanceof Error ? err.message : err}`);
2188
+ if (!this.getActiveAccount()) {
2189
+ void this.setActiveAccount(contact.id, true);
2416
2190
  }
2417
2191
  }
2418
2192
  }