@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.
- package/README.md +3 -3
- package/dist/core/client-state-manager.d.ts +105 -0
- package/dist/core/client-state-manager.js +250 -0
- package/dist/core/contacts/majik-contact-directory.d.ts +0 -5
- package/dist/core/contacts/majik-contact-directory.js +0 -12
- package/dist/core/contacts/majik-contact-groups.d.ts +1 -0
- package/dist/core/contacts/majik-contact-groups.js +5 -0
- package/dist/core/contacts/majik-contact-manager.d.ts +99 -185
- package/dist/core/contacts/majik-contact-manager.js +469 -289
- package/dist/core/contacts/types.d.ts +1 -0
- package/dist/core/crypto/keystore-manager.d.ts +166 -0
- package/dist/core/crypto/keystore-manager.js +371 -0
- package/dist/core/storage/chats/_types.d.ts +8 -0
- package/dist/core/storage/chats/_types.js +1 -0
- package/dist/core/storage/chats/adapter-idb.d.ts +3 -0
- package/dist/core/storage/chats/adapter-idb.js +5 -0
- package/dist/core/storage/chats/adapter-memory.d.ts +23 -0
- package/dist/core/storage/chats/adapter-memory.js +44 -0
- package/dist/core/storage/chats/adapter-sql.d.ts +17 -0
- package/dist/core/storage/chats/adapter-sql.js +85 -0
- package/dist/core/storage/client-state/_types.d.ts +37 -0
- package/dist/core/storage/client-state/_types.js +16 -0
- package/dist/core/storage/client-state/adapter-idb.d.ts +17 -0
- package/dist/core/storage/client-state/adapter-idb.js +19 -0
- package/dist/core/storage/client-state/adapter-memory.d.ts +20 -0
- package/dist/core/storage/client-state/adapter-memory.js +44 -0
- package/dist/core/storage/client-state/adapter-sql.d.ts +41 -0
- package/dist/core/storage/client-state/adapter-sql.js +104 -0
- package/dist/core/storage/contact-directory/contacts/_types.d.ts +3 -0
- package/dist/core/storage/contact-directory/contacts/_types.js +1 -0
- package/dist/core/storage/contact-directory/contacts/adapter-idb.d.ts +3 -0
- package/dist/core/storage/contact-directory/contacts/adapter-idb.js +5 -0
- package/dist/core/storage/contact-directory/contacts/adapter-memory.d.ts +14 -0
- package/dist/core/storage/contact-directory/contacts/adapter-memory.js +32 -0
- package/dist/core/storage/contact-directory/contacts/adapter-sql.d.ts +18 -0
- package/dist/core/storage/contact-directory/contacts/adapter-sql.js +97 -0
- package/dist/core/storage/contact-directory/groups/_types.d.ts +3 -0
- package/dist/core/storage/contact-directory/groups/_types.js +1 -0
- package/dist/core/storage/contact-directory/groups/adapter-idb.d.ts +3 -0
- package/dist/core/storage/contact-directory/groups/adapter-idb.js +5 -0
- package/dist/core/storage/contact-directory/groups/adapter-memory.d.ts +14 -0
- package/dist/core/storage/contact-directory/groups/adapter-memory.js +32 -0
- package/dist/core/storage/contact-directory/groups/adapter-sql.d.ts +16 -0
- package/dist/core/storage/contact-directory/groups/adapter-sql.js +72 -0
- package/dist/core/storage/idb-adapter.d.ts +21 -0
- package/dist/core/storage/idb-adapter.js +107 -0
- package/dist/core/storage/index.d.ts +24 -0
- package/dist/core/storage/index.js +19 -0
- package/dist/core/storage/keystore/_types.d.ts +3 -0
- package/dist/core/storage/keystore/_types.js +1 -0
- package/dist/core/storage/keystore/adapter-idb.d.ts +3 -0
- package/dist/core/storage/keystore/adapter-idb.js +5 -0
- package/dist/core/storage/keystore/adapter-memory.d.ts +14 -0
- package/dist/core/storage/keystore/adapter-memory.js +32 -0
- package/dist/core/storage/keystore/adapter-sql.d.ts +18 -0
- package/dist/core/storage/keystore/adapter-sql.js +93 -0
- package/dist/core/storage/sql-db-manager.d.ts +13 -0
- package/dist/core/storage/sql-db-manager.js +59 -0
- package/dist/core/storage/sql-schema.d.ts +25 -0
- package/dist/core/storage/sql-schema.js +122 -0
- package/dist/core/storage/storage-adapter.d.ts +22 -0
- package/dist/core/storage/storage-adapter.js +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -4
- package/dist/majik-message.d.ts +114 -174
- package/dist/majik-message.js +449 -675
- package/package.json +5 -6
package/dist/majik-message.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
17
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
_id;
|
|
18
|
+
_db;
|
|
19
|
+
_contacts;
|
|
20
|
+
_keys;
|
|
21
|
+
_state;
|
|
23
22
|
envelopeCache;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
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.
|
|
52
|
-
|
|
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
|
|
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.
|
|
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
|
|
80
|
-
const key =
|
|
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
|
|
106
|
-
const key =
|
|
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.
|
|
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
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
249
|
+
// ==========================================================================
|
|
250
|
+
// ── ACCOUNT MANAGEMENT ────────────────────────────────────────────────────
|
|
251
|
+
// ==========================================================================
|
|
252
|
+
generateMnemonic(strength = 128) {
|
|
253
|
+
return MajikKeyManager.generateMnemonic(strength);
|
|
154
254
|
}
|
|
155
|
-
async
|
|
156
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
catch (err) {
|
|
309
|
+
this._emit("error", err, {
|
|
310
|
+
context: "replaceAccountFromMnemonicBackup",
|
|
311
|
+
});
|
|
312
|
+
throw err;
|
|
197
313
|
}
|
|
198
|
-
this.scheduleAutosave();
|
|
199
314
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
366
|
+
const index = this._ownAccountsOrder.indexOf(id);
|
|
225
367
|
if (index > -1)
|
|
226
|
-
this.
|
|
227
|
-
this.
|
|
228
|
-
this.
|
|
368
|
+
this._ownAccountsOrder.splice(index, 1);
|
|
369
|
+
this._ownAccountsOrder.unshift(id);
|
|
370
|
+
this._scheduleOrderSave();
|
|
229
371
|
if (previousActive !== id) {
|
|
230
|
-
this.
|
|
372
|
+
this._emit("active-account-change", this.getActiveAccount(), previousActive);
|
|
231
373
|
}
|
|
232
374
|
return true;
|
|
233
375
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
return
|
|
385
|
+
isContactMajikahRegistered(id) {
|
|
386
|
+
return this._contacts.isMajikahRegistered(id);
|
|
241
387
|
}
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
256
|
-
|
|
391
|
+
setContactMajikahStatus(id, status) {
|
|
392
|
+
this._contacts.setMajikahStatus(id, status);
|
|
257
393
|
}
|
|
258
|
-
async
|
|
259
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
408
|
+
return ((await this._contacts.getContactByPublicKeyBase64(publicKeyBase64)) ??
|
|
409
|
+
null);
|
|
285
410
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
469
|
+
throw new Error("Invalid contact — missing required fields");
|
|
420
470
|
}
|
|
421
|
-
this.
|
|
422
|
-
this.
|
|
423
|
-
this.scheduleAutosave();
|
|
471
|
+
await this._contacts.addContact(contact);
|
|
472
|
+
this._emit("new-contact", contact);
|
|
424
473
|
}
|
|
425
|
-
removeContact(id) {
|
|
426
|
-
const result = this.
|
|
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.
|
|
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
|
-
|
|
441
|
-
this.
|
|
442
|
-
|
|
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(
|
|
484
|
+
const ownIds = new Set(this.listOwnAccounts().map((a) => a.id));
|
|
449
485
|
return contacts.filter((c) => !ownIds.has(c.id));
|
|
450
486
|
}
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
521
|
+
return this._contacts.listGroups(true).filter((g) => g.isSystem);
|
|
533
522
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
561
|
+
return this._contacts.getGroupIdsForContact(contactID);
|
|
622
562
|
}
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
577
|
+
return this._contacts.isContactBlocked(contactID);
|
|
655
578
|
}
|
|
656
|
-
/**
|
|
657
|
-
* Returns the Favorites system group instance.
|
|
658
|
-
*/
|
|
659
579
|
getFavoritesGroup() {
|
|
660
|
-
return this.
|
|
580
|
+
return this._contacts.getFavoritesGroup();
|
|
661
581
|
}
|
|
662
|
-
/**
|
|
663
|
-
* Returns the Blocked system group instance.
|
|
664
|
-
*/
|
|
665
582
|
getBlockedGroup() {
|
|
666
|
-
return this.
|
|
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.
|
|
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.
|
|
589
|
+
return this._contacts.getContactsInGroup(this._contacts.getBlockedGroup().id);
|
|
679
590
|
}
|
|
680
|
-
|
|
681
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
944
|
+
await this.keyManager.ensureUnlocked(id);
|
|
1034
945
|
// get() is safe after ensureUnlocked() — key is in the memory cache.
|
|
1035
|
-
const key =
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1389
|
+
return this.keyManager.isPassphraseValid(target.id, passphrase);
|
|
1479
1390
|
}
|
|
1480
1391
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
1481
1392
|
on(event, callback) {
|
|
1482
|
-
this.
|
|
1393
|
+
this._listeners.get(event)?.push(callback);
|
|
1483
1394
|
}
|
|
1484
1395
|
off(event, callback) {
|
|
1485
|
-
const cbs = this.
|
|
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.
|
|
1405
|
+
this._listeners.set(event, []);
|
|
1495
1406
|
}
|
|
1496
1407
|
}
|
|
1497
|
-
|
|
1498
|
-
this.
|
|
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
|
|
1518
|
-
const key =
|
|
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.
|
|
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
|
|
1555
|
-
const key =
|
|
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.
|
|
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
|
|
1600
|
-
const key =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
-
//
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
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
|
-
|
|
2173
|
+
throw new Error(`Failed to reset data: ${err instanceof Error ? err.message : err}`);
|
|
2363
2174
|
}
|
|
2364
2175
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
2379
|
-
|
|
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
|
-
|
|
2415
|
-
|
|
2188
|
+
if (!this.getActiveAccount()) {
|
|
2189
|
+
void this.setActiveAccount(contact.id, true);
|
|
2416
2190
|
}
|
|
2417
2191
|
}
|
|
2418
2192
|
}
|