@majikah/majik-message 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +92 -184
- package/dist/core/contacts/majik-contact-manager.js +368 -288
- 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 +84 -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 +16 -0
- package/dist/core/storage/contact-directory/contacts/adapter-sql.js +73 -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 +71 -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 +16 -0
- package/dist/core/storage/keystore/adapter-sql.js +69 -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 +10 -0
- package/dist/core/storage/sql-schema.js +108 -0
- package/dist/core/storage/storage-adapter.d.ts +14 -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 +109 -174
- package/dist/majik-message.js +428 -677
- package/package.json +4 -5
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,221 @@ 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
|
-
async exportContactAsJSON(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
else {
|
|
296
|
-
const raw = await crypto.subtle.exportKey("raw", contact.publicKey);
|
|
297
|
-
publicKeyBase64 = arrayBufferToBase64(raw);
|
|
298
|
-
}
|
|
299
|
-
return JSON.stringify({
|
|
300
|
-
id: contact.id,
|
|
301
|
-
label: contact.meta?.label || "",
|
|
302
|
-
publicKey: publicKeyBase64,
|
|
303
|
-
fingerprint: contact.fingerprint,
|
|
304
|
-
mlKey: contact.mlKey,
|
|
305
|
-
edPublicKeyBase64: contact.edPublicKeyBase64,
|
|
306
|
-
mlDsaPublicKeyBase64: contact.mlDsaPublicKeyBase64,
|
|
307
|
-
}, null, 2);
|
|
308
|
-
}
|
|
309
|
-
async exportContactAsString(contactID) {
|
|
310
|
-
const contact = this.contacts.getContact(contactID);
|
|
311
|
-
if (!contact)
|
|
312
|
-
return null;
|
|
313
|
-
const compressedString = this.exportContactCompressed(contact);
|
|
314
|
-
return compressedString;
|
|
411
|
+
async exportContactAsJSON(id) {
|
|
412
|
+
if (!id?.trim())
|
|
413
|
+
throw new Error("Invalid contact ID");
|
|
414
|
+
return this._contacts.exportContactAsJSON(id);
|
|
415
|
+
}
|
|
416
|
+
async exportContactAsString(id) {
|
|
417
|
+
if (!id?.trim())
|
|
418
|
+
throw new Error("Invalid contact ID");
|
|
419
|
+
return this._contacts.exportContactAsString(id);
|
|
315
420
|
}
|
|
316
421
|
async importContactFromJSON(jsonStr) {
|
|
317
|
-
|
|
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
|
-
}
|
|
422
|
+
if (!jsonStr?.trim())
|
|
423
|
+
throw new Error("Invalid contact JSON");
|
|
424
|
+
return this._contacts.importContactFromJSON(jsonStr);
|
|
347
425
|
}
|
|
348
426
|
async importContactFromString(base64Str) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return { success: true, message: "Contact imported successfully" };
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
return {
|
|
356
|
-
success: false,
|
|
357
|
-
message: err instanceof Error ? err.message : "Unknown error",
|
|
358
|
-
};
|
|
359
|
-
}
|
|
427
|
+
if (!base64Str?.trim())
|
|
428
|
+
throw new Error("Invalid contact string");
|
|
429
|
+
return this._contacts.importContactFromString(base64Str);
|
|
360
430
|
}
|
|
361
431
|
async exportContactCompressed(contact) {
|
|
362
|
-
|
|
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);
|
|
432
|
+
if (!contact?.id?.trim())
|
|
433
|
+
throw new Error("Invalid contact");
|
|
434
|
+
return this._contacts.exportContactCompressed(contact);
|
|
387
435
|
}
|
|
388
436
|
async importContactCompressed(base64Str) {
|
|
389
|
-
|
|
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
|
-
});
|
|
437
|
+
if (!base64Str?.trim())
|
|
438
|
+
throw new Error("Invalid contact string");
|
|
439
|
+
return this._contacts.importContactCompressed(base64Str);
|
|
413
440
|
}
|
|
414
|
-
addContact(contact) {
|
|
441
|
+
async addContact(contact) {
|
|
415
442
|
if (!contact?.id ||
|
|
416
443
|
!contact?.publicKey ||
|
|
417
444
|
!contact?.fingerprint ||
|
|
418
445
|
!contact?.mlKey) {
|
|
419
|
-
throw new Error("Invalid contact
|
|
446
|
+
throw new Error("Invalid contact — missing required fields");
|
|
420
447
|
}
|
|
421
|
-
this.
|
|
422
|
-
this.
|
|
423
|
-
this.scheduleAutosave();
|
|
448
|
+
await this._contacts.addContact(contact);
|
|
449
|
+
this._emit("new-contact", contact);
|
|
424
450
|
}
|
|
425
|
-
removeContact(id) {
|
|
426
|
-
const result = this.
|
|
451
|
+
async removeContact(id) {
|
|
452
|
+
const result = await this._contacts.removeContact(id);
|
|
427
453
|
if (!result.success)
|
|
428
454
|
throw new Error(result.message);
|
|
429
|
-
this.
|
|
430
|
-
this.scheduleAutosave();
|
|
455
|
+
this._emit("removed-contact", id);
|
|
431
456
|
}
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
blockContact(id) {
|
|
437
|
-
this.contacts.blockContact(id);
|
|
438
|
-
this.scheduleAutosave();
|
|
439
|
-
}
|
|
440
|
-
unblockContact(id) {
|
|
441
|
-
this.contacts.unblockContact(id);
|
|
442
|
-
this.scheduleAutosave();
|
|
443
|
-
}
|
|
444
|
-
listContacts(all = true, majikahOnly = false) {
|
|
445
|
-
const contacts = this.contacts.listContacts(true, majikahOnly);
|
|
446
|
-
if (all)
|
|
457
|
+
listContacts(includeOwnAccounts = false) {
|
|
458
|
+
const contacts = this._contacts.listContacts(true);
|
|
459
|
+
if (includeOwnAccounts)
|
|
447
460
|
return contacts;
|
|
448
|
-
const ownIds = new Set(this.listOwnAccounts(
|
|
461
|
+
const ownIds = new Set(this.listOwnAccounts().map((a) => a.id));
|
|
449
462
|
return contacts.filter((c) => !ownIds.has(c.id));
|
|
450
463
|
}
|
|
451
|
-
|
|
452
|
-
|
|
464
|
+
async updateContactMeta(id, meta) {
|
|
465
|
+
await this._contacts.updateContactMeta(id, meta);
|
|
453
466
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
setContactMajikahStatus(id, status) {
|
|
458
|
-
this.contacts.setMajikahStatus(id, status);
|
|
459
|
-
this.scheduleAutosave();
|
|
460
|
-
}
|
|
461
|
-
/* ================================
|
|
462
|
-
* Group CRUD Pass-throughs
|
|
463
|
-
* ================================ */
|
|
464
|
-
/**
|
|
465
|
-
* Creates and registers a new user-defined group.
|
|
466
|
-
* Throws if a group with the same ID already exists.
|
|
467
|
-
*/
|
|
468
|
-
createGroup(id, name, meta, initialMemberIds) {
|
|
469
|
-
const newGroup = this.contacts.createGroup(id, name, meta, initialMemberIds);
|
|
470
|
-
this.emit("new-contact-group", newGroup);
|
|
471
|
-
this.scheduleAutosave();
|
|
467
|
+
async createGroup(id, name, meta, initialMemberIds) {
|
|
468
|
+
const newGroup = await this._contacts.createGroup(id, name, meta, initialMemberIds);
|
|
469
|
+
this._emit("new-contact-group", newGroup);
|
|
472
470
|
return this;
|
|
473
471
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
*/
|
|
478
|
-
addGroup(group) {
|
|
479
|
-
this.contacts.addGroup(group);
|
|
480
|
-
this.emit("new-contact-group", group);
|
|
481
|
-
this.scheduleAutosave();
|
|
472
|
+
async addGroup(group) {
|
|
473
|
+
await this._contacts.addGroup(group);
|
|
474
|
+
this._emit("new-contact-group", group);
|
|
482
475
|
return this;
|
|
483
476
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
*/
|
|
488
|
-
removeGroup(id) {
|
|
489
|
-
const response = this.contacts.removeGroup(id);
|
|
490
|
-
this.emit("removed-contact-group", response.data);
|
|
491
|
-
this.scheduleAutosave();
|
|
477
|
+
async removeGroup(id) {
|
|
478
|
+
const response = await this._contacts.removeGroup(id);
|
|
479
|
+
this._emit("removed-contact-group", response.data);
|
|
492
480
|
return response;
|
|
493
481
|
}
|
|
494
|
-
/**
|
|
495
|
-
* Returns a group by ID, or undefined if not found.
|
|
496
|
-
*/
|
|
497
482
|
getContactGroup(id) {
|
|
498
|
-
return this.
|
|
483
|
+
return this._contacts.getGroup(id);
|
|
499
484
|
}
|
|
500
|
-
/**
|
|
501
|
-
* Returns a group by ID. Throws if not found.
|
|
502
|
-
*/
|
|
503
485
|
getGroupOrThrow(id) {
|
|
504
|
-
return this.
|
|
486
|
+
return this._contacts.getGroupOrThrow(id);
|
|
505
487
|
}
|
|
506
|
-
/**
|
|
507
|
-
* Returns true if a group with the given ID exists.
|
|
508
|
-
*/
|
|
509
488
|
hasGroup(id) {
|
|
510
|
-
return this.
|
|
489
|
+
return this._contacts.hasGroup(id);
|
|
511
490
|
}
|
|
512
|
-
/**
|
|
513
|
-
* Returns all groups.
|
|
514
|
-
*
|
|
515
|
-
* @param includeSystem Include system groups (Favorites, Blocked). Default: true.
|
|
516
|
-
* @param sortedByName Sort results alphabetically by group name. Default: false.
|
|
517
|
-
*/
|
|
518
491
|
listContactGroups(includeSystem = true, sortedByName = false) {
|
|
519
|
-
return this.
|
|
492
|
+
return this._contacts.listGroups(includeSystem, sortedByName);
|
|
520
493
|
}
|
|
521
|
-
/**
|
|
522
|
-
* Returns only user-created groups (excludes Favorites and Blocked).
|
|
523
|
-
* Sorted alphabetically by name.
|
|
524
|
-
*/
|
|
525
494
|
listUserGroups(sortedByName = true) {
|
|
526
|
-
return this.
|
|
495
|
+
return this._contacts.listGroups(false, sortedByName);
|
|
527
496
|
}
|
|
528
|
-
/**
|
|
529
|
-
* Returns only system groups (Favorites and Blocked).
|
|
530
|
-
*/
|
|
531
497
|
listSystemGroups() {
|
|
532
|
-
return this.
|
|
498
|
+
return this._contacts.listGroups(true).filter((g) => g.isSystem);
|
|
533
499
|
}
|
|
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();
|
|
500
|
+
async updateGroupMeta(id, meta) {
|
|
501
|
+
const updatedGroup = await this._contacts.updateGroupMeta(id, meta);
|
|
502
|
+
this._emit("contact-group-change", updatedGroup);
|
|
542
503
|
return this;
|
|
543
504
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Adds a contact to a group.
|
|
549
|
-
* Validates the contact exists in the directory.
|
|
550
|
-
* If the group is the system Blocked group, also calls contact.block().
|
|
551
|
-
* Throws if the contact is already a member — use addContactToGroupIfAbsent for idempotent.
|
|
552
|
-
*/
|
|
553
|
-
addContactToGroup(groupID, contactID) {
|
|
554
|
-
const updatedGroup = this.contacts.addContactToGroup(groupID, contactID);
|
|
555
|
-
this.emit("contact-group-change", updatedGroup);
|
|
556
|
-
this.scheduleAutosave();
|
|
505
|
+
async addContactToGroup(groupID, contactID) {
|
|
506
|
+
const updatedGroup = await this._contacts.addContactToGroup(groupID, contactID);
|
|
507
|
+
this._emit("contact-group-change", updatedGroup);
|
|
557
508
|
return this;
|
|
558
509
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
addContactsToGroup(groupID, contactIds) {
|
|
563
|
-
const updatedGroup = this.contacts.addContactsToGroup(groupID, contactIds);
|
|
564
|
-
this.emit("contact-group-change", updatedGroup);
|
|
565
|
-
this.scheduleAutosave();
|
|
510
|
+
async addContactsToGroup(groupID, contactIds) {
|
|
511
|
+
const updatedGroup = await this._contacts.addContactsToGroup(groupID, contactIds);
|
|
512
|
+
this._emit("contact-group-change", updatedGroup);
|
|
566
513
|
return this;
|
|
567
514
|
}
|
|
568
|
-
|
|
569
|
-
|
|
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();
|
|
515
|
+
async removeContactFromGroup(groupID, contactID) {
|
|
516
|
+
const updatedGroup = await this._contacts.removeContactFromGroup(groupID, contactID);
|
|
517
|
+
this._emit("contact-group-change", updatedGroup);
|
|
577
518
|
return this;
|
|
578
519
|
}
|
|
579
|
-
|
|
580
|
-
|
|
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();
|
|
520
|
+
async moveContactBetweenGroups(contactID, fromGroupId, toGroupId) {
|
|
521
|
+
const updatedGroup = await this._contacts.moveContactBetweenGroups(contactID, fromGroupId, toGroupId);
|
|
522
|
+
this._emit("contact-group-change", updatedGroup);
|
|
587
523
|
return this;
|
|
588
524
|
}
|
|
589
|
-
/* ================================
|
|
590
|
-
* Group Query Pass-throughs
|
|
591
|
-
* ================================ */
|
|
592
|
-
/**
|
|
593
|
-
* Returns all hydrated MajikContact instances in the given group.
|
|
594
|
-
* Contacts removed from the directory since last save are silently skipped.
|
|
595
|
-
*/
|
|
596
525
|
getContactsInGroup(groupID) {
|
|
597
|
-
return this.
|
|
526
|
+
return this._contacts.getContactsInGroup(groupID);
|
|
598
527
|
}
|
|
599
|
-
/**
|
|
600
|
-
* Returns hydrated contacts in the group, sorted by label (or ID if no label).
|
|
601
|
-
*/
|
|
602
528
|
getContactsInGroupSorted(groupID) {
|
|
603
|
-
return this.
|
|
529
|
+
return this._contacts.getContactsInGroupSorted(groupID);
|
|
604
530
|
}
|
|
605
|
-
/**
|
|
606
|
-
* Returns true if the contact is a member of the given group.
|
|
607
|
-
*/
|
|
608
531
|
isContactInGroup(groupID, contactID) {
|
|
609
|
-
return this.
|
|
532
|
+
return this._contacts.isContactInGroup(groupID, contactID);
|
|
610
533
|
}
|
|
611
|
-
/**
|
|
612
|
-
* Returns all groups the contact belongs to.
|
|
613
|
-
*/
|
|
614
534
|
getGroupsForContact(contactID) {
|
|
615
|
-
return this.
|
|
535
|
+
return this._contacts.getGroupsForContact(contactID);
|
|
616
536
|
}
|
|
617
|
-
/**
|
|
618
|
-
* Returns all group IDs the contact belongs to.
|
|
619
|
-
*/
|
|
620
537
|
getGroupIdsForContact(contactID) {
|
|
621
|
-
return this.
|
|
538
|
+
return this._contacts.getGroupIdsForContact(contactID);
|
|
622
539
|
}
|
|
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();
|
|
540
|
+
async addContactToFavorites(contactID) {
|
|
541
|
+
const updatedGroup = await this._contacts.addToFavorites(contactID);
|
|
542
|
+
this._emit("contact-group-change", updatedGroup);
|
|
633
543
|
return this;
|
|
634
544
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
removeContactFromFavorites(contactID) {
|
|
639
|
-
const updatedGroup = this.contacts.removeFromFavorites(contactID);
|
|
640
|
-
this.emit("contact-group-change", updatedGroup);
|
|
641
|
-
this.scheduleAutosave();
|
|
545
|
+
async removeContactFromFavorites(contactID) {
|
|
546
|
+
const updatedGroup = await this._contacts.removeFromFavorites(contactID);
|
|
547
|
+
this._emit("contact-group-change", updatedGroup);
|
|
642
548
|
return this;
|
|
643
549
|
}
|
|
644
|
-
/**
|
|
645
|
-
* Returns true if the contact is in the Favorites group.
|
|
646
|
-
*/
|
|
647
550
|
isContactFavorite(contactID) {
|
|
648
|
-
return this.
|
|
551
|
+
return this._contacts.isFavorite(contactID);
|
|
649
552
|
}
|
|
650
|
-
/**
|
|
651
|
-
* Returns true if the contact is in the Blocked group.
|
|
652
|
-
*/
|
|
653
553
|
isContactBlocked(contactID) {
|
|
654
|
-
return this.
|
|
554
|
+
return this._contacts.isContactBlocked(contactID);
|
|
655
555
|
}
|
|
656
|
-
/**
|
|
657
|
-
* Returns the Favorites system group instance.
|
|
658
|
-
*/
|
|
659
556
|
getFavoritesGroup() {
|
|
660
|
-
return this.
|
|
557
|
+
return this._contacts.getFavoritesGroup();
|
|
661
558
|
}
|
|
662
|
-
/**
|
|
663
|
-
* Returns the Blocked system group instance.
|
|
664
|
-
*/
|
|
665
559
|
getBlockedGroup() {
|
|
666
|
-
return this.
|
|
560
|
+
return this._contacts.getBlockedGroup();
|
|
667
561
|
}
|
|
668
|
-
/**
|
|
669
|
-
* Returns all contacts in the Favorites group as hydrated MajikContact instances.
|
|
670
|
-
*/
|
|
671
562
|
getFavoriteContacts() {
|
|
672
|
-
return this.
|
|
563
|
+
return this._contacts.getContactsInGroup(this._contacts.getFavoritesGroup().id);
|
|
673
564
|
}
|
|
674
|
-
/**
|
|
675
|
-
* Returns all contacts in the Blocked group as hydrated MajikContact instances.
|
|
676
|
-
*/
|
|
677
565
|
getBlockedContacts() {
|
|
678
|
-
return this.
|
|
566
|
+
return this._contacts.getContactsInGroup(this._contacts.getBlockedGroup().id);
|
|
679
567
|
}
|
|
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();
|
|
568
|
+
async clearDirectory() {
|
|
569
|
+
await this._contacts.clear();
|
|
690
570
|
return this;
|
|
691
571
|
}
|
|
572
|
+
resolveSignerLabel(signerId) {
|
|
573
|
+
const ownAccount = this._ownAccounts.get(signerId);
|
|
574
|
+
if (ownAccount?.meta?.label)
|
|
575
|
+
return ownAccount.meta.label;
|
|
576
|
+
const contact = this._contacts.getContact(signerId);
|
|
577
|
+
if (contact?.meta?.label)
|
|
578
|
+
return contact.meta.label;
|
|
579
|
+
return `${signerId.slice(0, 16)}…`;
|
|
580
|
+
}
|
|
692
581
|
// ── Encryption / Decryption ───────────────────────────────────────────────
|
|
693
582
|
/**
|
|
694
583
|
* Compose and encrypt a message for one or more recipients.
|
|
@@ -710,8 +599,7 @@ export class MajikMessage {
|
|
|
710
599
|
if (cache) {
|
|
711
600
|
await this.envelopeCache.set(new MessageEnvelope(envelope.toBinary()), this._source);
|
|
712
601
|
}
|
|
713
|
-
this.
|
|
714
|
-
this.emit("envelope", envelope);
|
|
602
|
+
this._emit("envelope", envelope);
|
|
715
603
|
return scannerString;
|
|
716
604
|
}
|
|
717
605
|
/**
|
|
@@ -764,7 +652,7 @@ export class MajikMessage {
|
|
|
764
652
|
}
|
|
765
653
|
catch (err) {
|
|
766
654
|
console.warn("Error: ", err);
|
|
767
|
-
this.
|
|
655
|
+
this._emit("error", err, { context: "encryptTextForScanner" });
|
|
768
656
|
return null;
|
|
769
657
|
}
|
|
770
658
|
}
|
|
@@ -829,7 +717,7 @@ export class MajikMessage {
|
|
|
829
717
|
return { messageChat, scannerString };
|
|
830
718
|
}
|
|
831
719
|
catch (err) {
|
|
832
|
-
this.
|
|
720
|
+
this._emit("error", err, { context: "createEncryptedMajikMessageChat" });
|
|
833
721
|
throw err;
|
|
834
722
|
}
|
|
835
723
|
}
|
|
@@ -848,7 +736,7 @@ export class MajikMessage {
|
|
|
848
736
|
return await envelope.decrypt(identity);
|
|
849
737
|
}
|
|
850
738
|
catch (err) {
|
|
851
|
-
this.
|
|
739
|
+
this._emit("error", err, { context: "decryptMajikMessageChat" });
|
|
852
740
|
throw err;
|
|
853
741
|
}
|
|
854
742
|
}
|
|
@@ -889,7 +777,7 @@ export class MajikMessage {
|
|
|
889
777
|
const activeId = this.getActiveAccount()?.id;
|
|
890
778
|
if (!activeId)
|
|
891
779
|
throw new Error("No active account — call setActiveAccount() first");
|
|
892
|
-
const signingKey =
|
|
780
|
+
const signingKey = this.keyManager.get(activeId);
|
|
893
781
|
// ── 4. Build CreateOptions ─────────────────────────────────────────────
|
|
894
782
|
const createOptions = {
|
|
895
783
|
data,
|
|
@@ -1030,9 +918,9 @@ export class MajikMessage {
|
|
|
1030
918
|
if (!id)
|
|
1031
919
|
throw new Error("No active account — call setActiveAccount() first");
|
|
1032
920
|
try {
|
|
1033
|
-
await
|
|
921
|
+
await this.keyManager.ensureUnlocked(id);
|
|
1034
922
|
// get() is safe after ensureUnlocked() — key is in the memory cache.
|
|
1035
|
-
const key =
|
|
923
|
+
const key = this.keyManager.get(id);
|
|
1036
924
|
if (!key)
|
|
1037
925
|
throw new Error(`Account not found in keystore: "${id}"`);
|
|
1038
926
|
if (!key.hasSigningKeys) {
|
|
@@ -1045,7 +933,7 @@ export class MajikMessage {
|
|
|
1045
933
|
});
|
|
1046
934
|
}
|
|
1047
935
|
catch (err) {
|
|
1048
|
-
this.
|
|
936
|
+
this._emit("error", err, { context: "signMajikFile" });
|
|
1049
937
|
throw err;
|
|
1050
938
|
}
|
|
1051
939
|
}
|
|
@@ -1087,7 +975,7 @@ export class MajikMessage {
|
|
|
1087
975
|
return file.verify(sig.extractPublicKeys());
|
|
1088
976
|
}
|
|
1089
977
|
catch (err) {
|
|
1090
|
-
this.
|
|
978
|
+
this._emit("error", err, { context: "verifyMajikFile" });
|
|
1091
979
|
throw err;
|
|
1092
980
|
}
|
|
1093
981
|
}
|
|
@@ -1136,7 +1024,7 @@ export class MajikMessage {
|
|
|
1136
1024
|
return file.verifyBinary(decryptIdentity, sig.extractPublicKeys());
|
|
1137
1025
|
}
|
|
1138
1026
|
catch (err) {
|
|
1139
|
-
this.
|
|
1027
|
+
this._emit("error", err, { context: "verifyMajikFileBinary" });
|
|
1140
1028
|
throw err;
|
|
1141
1029
|
}
|
|
1142
1030
|
}
|
|
@@ -1170,7 +1058,7 @@ export class MajikMessage {
|
|
|
1170
1058
|
return false;
|
|
1171
1059
|
// get() checks the memory cache — no async needed since the account
|
|
1172
1060
|
// must already be loaded to be the active account.
|
|
1173
|
-
const key =
|
|
1061
|
+
const key = this.keyManager.get(id);
|
|
1174
1062
|
if (!key)
|
|
1175
1063
|
return false;
|
|
1176
1064
|
return key.fingerprint === sigInfo.signerId;
|
|
@@ -1448,7 +1336,7 @@ export class MajikMessage {
|
|
|
1448
1336
|
const id = accountId ?? this.getActiveAccount()?.id;
|
|
1449
1337
|
if (!id)
|
|
1450
1338
|
return false;
|
|
1451
|
-
const key =
|
|
1339
|
+
const key = this.keyManager.get(id);
|
|
1452
1340
|
return key?.hasSigningKeys === true;
|
|
1453
1341
|
}
|
|
1454
1342
|
// ── Envelope Cache ────────────────────────────────────────────────────────
|
|
@@ -1459,30 +1347,30 @@ export class MajikMessage {
|
|
|
1459
1347
|
const response = await this.envelopeCache.clear();
|
|
1460
1348
|
if (!response?.success)
|
|
1461
1349
|
throw new Error(response.message);
|
|
1462
|
-
this.
|
|
1350
|
+
this._scheduleOrderSave();
|
|
1463
1351
|
return response.success;
|
|
1464
1352
|
}
|
|
1465
1353
|
// ── Identity / Passphrase ─────────────────────────────────────────────────
|
|
1466
1354
|
/**
|
|
1467
1355
|
* Ensure an identity is unlocked.
|
|
1468
|
-
* Delegates entirely to
|
|
1356
|
+
* Delegates entirely to this.keyManager.ensureUnlocked() — passphrase prompting
|
|
1469
1357
|
* is handled there via onUnlockRequested or the optional promptFn.
|
|
1470
1358
|
*/
|
|
1471
1359
|
async ensureIdentityUnlocked(id, promptFn) {
|
|
1472
|
-
return
|
|
1360
|
+
return this.keyManager.ensureUnlocked(id, promptFn);
|
|
1473
1361
|
}
|
|
1474
1362
|
async isPassphraseValid(passphrase, id) {
|
|
1475
1363
|
const target = id ? this.getOwnAccountById(id) : this.getActiveAccount();
|
|
1476
1364
|
if (!target)
|
|
1477
1365
|
return false;
|
|
1478
|
-
return
|
|
1366
|
+
return this.keyManager.isPassphraseValid(target.id, passphrase);
|
|
1479
1367
|
}
|
|
1480
1368
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
1481
1369
|
on(event, callback) {
|
|
1482
|
-
this.
|
|
1370
|
+
this._listeners.get(event)?.push(callback);
|
|
1483
1371
|
}
|
|
1484
1372
|
off(event, callback) {
|
|
1485
|
-
const cbs = this.
|
|
1373
|
+
const cbs = this._listeners.get(event);
|
|
1486
1374
|
if (!cbs?.length)
|
|
1487
1375
|
return;
|
|
1488
1376
|
if (callback) {
|
|
@@ -1491,11 +1379,11 @@ export class MajikMessage {
|
|
|
1491
1379
|
cbs.splice(i, 1);
|
|
1492
1380
|
}
|
|
1493
1381
|
else {
|
|
1494
|
-
this.
|
|
1382
|
+
this._listeners.set(event, []);
|
|
1495
1383
|
}
|
|
1496
1384
|
}
|
|
1497
|
-
|
|
1498
|
-
this.
|
|
1385
|
+
_emit(event, ...args) {
|
|
1386
|
+
this._listeners.get(event)?.forEach((cb) => cb(...args));
|
|
1499
1387
|
}
|
|
1500
1388
|
// ── Content & File Signing ────────────────────────────────────────────────
|
|
1501
1389
|
/**
|
|
@@ -1514,8 +1402,8 @@ export class MajikMessage {
|
|
|
1514
1402
|
if (!id)
|
|
1515
1403
|
throw new Error("No active account — call setActiveAccount() first");
|
|
1516
1404
|
try {
|
|
1517
|
-
await
|
|
1518
|
-
const key =
|
|
1405
|
+
await this.keyManager.ensureUnlocked(id);
|
|
1406
|
+
const key = this.keyManager.get(id);
|
|
1519
1407
|
if (!key)
|
|
1520
1408
|
throw new Error(`Account not found in keystore: "${id}"`);
|
|
1521
1409
|
if (!key.hasSigningKeys) {
|
|
@@ -1528,7 +1416,7 @@ export class MajikMessage {
|
|
|
1528
1416
|
});
|
|
1529
1417
|
}
|
|
1530
1418
|
catch (err) {
|
|
1531
|
-
this.
|
|
1419
|
+
this._emit("error", err, { context: "signContent" });
|
|
1532
1420
|
throw err;
|
|
1533
1421
|
}
|
|
1534
1422
|
}
|
|
@@ -1551,8 +1439,8 @@ export class MajikMessage {
|
|
|
1551
1439
|
if (!id)
|
|
1552
1440
|
throw new Error("No active account — call setActiveAccount() first");
|
|
1553
1441
|
try {
|
|
1554
|
-
await
|
|
1555
|
-
const key =
|
|
1442
|
+
await this.keyManager.ensureUnlocked(id);
|
|
1443
|
+
const key = this.keyManager.get(id);
|
|
1556
1444
|
if (!key)
|
|
1557
1445
|
throw new Error(`Account not found in keystore: "${id}"`);
|
|
1558
1446
|
if (!key.hasSigningKeys) {
|
|
@@ -1567,7 +1455,7 @@ export class MajikMessage {
|
|
|
1567
1455
|
});
|
|
1568
1456
|
}
|
|
1569
1457
|
catch (err) {
|
|
1570
|
-
this.
|
|
1458
|
+
this._emit("error", err, { context: "signFile" });
|
|
1571
1459
|
throw err;
|
|
1572
1460
|
}
|
|
1573
1461
|
}
|
|
@@ -1596,8 +1484,8 @@ export class MajikMessage {
|
|
|
1596
1484
|
const id = options?.accountId ?? this.getActiveAccount()?.id;
|
|
1597
1485
|
if (!id)
|
|
1598
1486
|
throw new Error("No active account — call setActiveAccount() first");
|
|
1599
|
-
await
|
|
1600
|
-
const key =
|
|
1487
|
+
await this.keyManager.ensureUnlocked(id);
|
|
1488
|
+
const key = this.keyManager.get(id);
|
|
1601
1489
|
if (!key)
|
|
1602
1490
|
throw new Error(`Account not found in keystore: "${id}"`);
|
|
1603
1491
|
if (!key.hasSigningKeys) {
|
|
@@ -1621,7 +1509,7 @@ export class MajikMessage {
|
|
|
1621
1509
|
};
|
|
1622
1510
|
}
|
|
1623
1511
|
catch (err) {
|
|
1624
|
-
this.
|
|
1512
|
+
this._emit("error", err, { context: "batchSignFiles" });
|
|
1625
1513
|
return {
|
|
1626
1514
|
blob: null,
|
|
1627
1515
|
signature: null,
|
|
@@ -1670,7 +1558,7 @@ export class MajikMessage {
|
|
|
1670
1558
|
return MajikSignature.verify(content, sig, sig.extractPublicKeys());
|
|
1671
1559
|
}
|
|
1672
1560
|
catch (err) {
|
|
1673
|
-
this.
|
|
1561
|
+
this._emit("error", err, { context: "verifyContent" });
|
|
1674
1562
|
throw err;
|
|
1675
1563
|
}
|
|
1676
1564
|
}
|
|
@@ -1726,7 +1614,7 @@ export class MajikMessage {
|
|
|
1726
1614
|
return results[0];
|
|
1727
1615
|
}
|
|
1728
1616
|
catch (err) {
|
|
1729
|
-
this.
|
|
1617
|
+
this._emit("error", err, { context: "verifyFile" });
|
|
1730
1618
|
throw err;
|
|
1731
1619
|
}
|
|
1732
1620
|
}
|
|
@@ -1795,7 +1683,7 @@ export class MajikMessage {
|
|
|
1795
1683
|
};
|
|
1796
1684
|
}
|
|
1797
1685
|
catch (err) {
|
|
1798
|
-
this.
|
|
1686
|
+
this._emit("error", err, { context: "batchVerifyFiles" });
|
|
1799
1687
|
return {
|
|
1800
1688
|
valid: false,
|
|
1801
1689
|
signerId: undefined,
|
|
@@ -1824,7 +1712,7 @@ export class MajikMessage {
|
|
|
1824
1712
|
return MajikSignature.extractFrom(file, options);
|
|
1825
1713
|
}
|
|
1826
1714
|
catch (err) {
|
|
1827
|
-
this.
|
|
1715
|
+
this._emit("error", err, { context: "extractSignature" });
|
|
1828
1716
|
throw err;
|
|
1829
1717
|
}
|
|
1830
1718
|
}
|
|
@@ -1842,7 +1730,7 @@ export class MajikMessage {
|
|
|
1842
1730
|
return MajikSignature.stripFrom(file, options);
|
|
1843
1731
|
}
|
|
1844
1732
|
catch (err) {
|
|
1845
|
-
this.
|
|
1733
|
+
this._emit("error", err, { context: "stripSignature" });
|
|
1846
1734
|
throw err;
|
|
1847
1735
|
}
|
|
1848
1736
|
}
|
|
@@ -1860,7 +1748,7 @@ export class MajikMessage {
|
|
|
1860
1748
|
return MajikSignature.isSigned(file, options);
|
|
1861
1749
|
}
|
|
1862
1750
|
catch (err) {
|
|
1863
|
-
this.
|
|
1751
|
+
this._emit("error", err, { context: "isFileSigned" });
|
|
1864
1752
|
throw err;
|
|
1865
1753
|
}
|
|
1866
1754
|
}
|
|
@@ -1878,7 +1766,7 @@ export class MajikMessage {
|
|
|
1878
1766
|
const id = accountId ?? this.getActiveAccount()?.id;
|
|
1879
1767
|
if (!id)
|
|
1880
1768
|
throw new Error("No active account — call setActiveAccount() first");
|
|
1881
|
-
const key =
|
|
1769
|
+
const key = this.keyManager.get(id);
|
|
1882
1770
|
if (!key)
|
|
1883
1771
|
throw new Error(`Account not found in keystore: "${id}"`);
|
|
1884
1772
|
if (!key.hasSigningKeys) {
|
|
@@ -1936,7 +1824,7 @@ export class MajikMessage {
|
|
|
1936
1824
|
return MajikSignature.getAllowlist(file, options);
|
|
1937
1825
|
}
|
|
1938
1826
|
catch (err) {
|
|
1939
|
-
this.
|
|
1827
|
+
this._emit("error", err, { context: "getAllowlist" });
|
|
1940
1828
|
throw err;
|
|
1941
1829
|
}
|
|
1942
1830
|
}
|
|
@@ -1953,7 +1841,7 @@ export class MajikMessage {
|
|
|
1953
1841
|
return MajikSignature.canSign(file, key, options);
|
|
1954
1842
|
}
|
|
1955
1843
|
catch (err) {
|
|
1956
|
-
this.
|
|
1844
|
+
this._emit("error", err, { context: "canSign" });
|
|
1957
1845
|
throw err;
|
|
1958
1846
|
}
|
|
1959
1847
|
}
|
|
@@ -1967,7 +1855,7 @@ export class MajikMessage {
|
|
|
1967
1855
|
return MajikSignature.isMultiSig(file, options);
|
|
1968
1856
|
}
|
|
1969
1857
|
catch (err) {
|
|
1970
|
-
this.
|
|
1858
|
+
this._emit("error", err, { context: "isMultiSig" });
|
|
1971
1859
|
throw err;
|
|
1972
1860
|
}
|
|
1973
1861
|
}
|
|
@@ -1994,7 +1882,7 @@ export class MajikMessage {
|
|
|
1994
1882
|
return MajikSignature.getSignatories(file, options, filter);
|
|
1995
1883
|
}
|
|
1996
1884
|
catch (err) {
|
|
1997
|
-
this.
|
|
1885
|
+
this._emit("error", err, { context: "getSignatories" });
|
|
1998
1886
|
throw err;
|
|
1999
1887
|
}
|
|
2000
1888
|
}
|
|
@@ -2007,7 +1895,7 @@ export class MajikMessage {
|
|
|
2007
1895
|
return MajikSignature.getSignedSignatories(file, options);
|
|
2008
1896
|
}
|
|
2009
1897
|
catch (err) {
|
|
2010
|
-
this.
|
|
1898
|
+
this._emit("error", err, { context: "getSignedSignatories" });
|
|
2011
1899
|
throw err;
|
|
2012
1900
|
}
|
|
2013
1901
|
}
|
|
@@ -2020,7 +1908,7 @@ export class MajikMessage {
|
|
|
2020
1908
|
return MajikSignature.getPendingSignatories(file, options);
|
|
2021
1909
|
}
|
|
2022
1910
|
catch (err) {
|
|
2023
|
-
this.
|
|
1911
|
+
this._emit("error", err, { context: "getPendingSignatories" });
|
|
2024
1912
|
throw err;
|
|
2025
1913
|
}
|
|
2026
1914
|
}
|
|
@@ -2033,7 +1921,7 @@ export class MajikMessage {
|
|
|
2033
1921
|
return MajikSignature.getAllSignatories(file, options);
|
|
2034
1922
|
}
|
|
2035
1923
|
catch (err) {
|
|
2036
|
-
this.
|
|
1924
|
+
this._emit("error", err, { context: "getAllSignatories" });
|
|
2037
1925
|
throw err;
|
|
2038
1926
|
}
|
|
2039
1927
|
}
|
|
@@ -2050,7 +1938,7 @@ export class MajikMessage {
|
|
|
2050
1938
|
return MajikSignature.getIssuer(file, options);
|
|
2051
1939
|
}
|
|
2052
1940
|
catch (err) {
|
|
2053
|
-
this.
|
|
1941
|
+
this._emit("error", err, { context: "getIssuer" });
|
|
2054
1942
|
throw err;
|
|
2055
1943
|
}
|
|
2056
1944
|
}
|
|
@@ -2077,7 +1965,7 @@ export class MajikMessage {
|
|
|
2077
1965
|
return MajikSignature.extractFrom(file, options);
|
|
2078
1966
|
}
|
|
2079
1967
|
catch (err) {
|
|
2080
|
-
this.
|
|
1968
|
+
this._emit("error", err, { context: "getFileSignatureInfo" });
|
|
2081
1969
|
throw err;
|
|
2082
1970
|
}
|
|
2083
1971
|
}
|
|
@@ -2098,7 +1986,7 @@ export class MajikMessage {
|
|
|
2098
1986
|
return MajikSignature.getEnvelopeInfo(file, options);
|
|
2099
1987
|
}
|
|
2100
1988
|
catch (err) {
|
|
2101
|
-
this.
|
|
1989
|
+
this._emit("error", err, { context: "getEnvelopeInfo" });
|
|
2102
1990
|
throw err;
|
|
2103
1991
|
}
|
|
2104
1992
|
}
|
|
@@ -2119,7 +2007,7 @@ export class MajikMessage {
|
|
|
2119
2007
|
if (!id)
|
|
2120
2008
|
throw new Error("No active account — call setActiveAccount() first");
|
|
2121
2009
|
try {
|
|
2122
|
-
const key =
|
|
2010
|
+
const key = this.keyManager.get(id);
|
|
2123
2011
|
if (!key)
|
|
2124
2012
|
throw new Error(`Account not found in keystore: "${id}"`);
|
|
2125
2013
|
return MajikSignature.seal(file, key, {
|
|
@@ -2128,7 +2016,7 @@ export class MajikMessage {
|
|
|
2128
2016
|
});
|
|
2129
2017
|
}
|
|
2130
2018
|
catch (err) {
|
|
2131
|
-
this.
|
|
2019
|
+
this._emit("error", err, { context: "seal" });
|
|
2132
2020
|
throw err;
|
|
2133
2021
|
}
|
|
2134
2022
|
}
|
|
@@ -2146,7 +2034,7 @@ export class MajikMessage {
|
|
|
2146
2034
|
return MajikSignature.verifySeal(file, options);
|
|
2147
2035
|
}
|
|
2148
2036
|
catch (err) {
|
|
2149
|
-
this.
|
|
2037
|
+
this._emit("error", err, { context: "verifySeal" });
|
|
2150
2038
|
throw err;
|
|
2151
2039
|
}
|
|
2152
2040
|
}
|
|
@@ -2163,7 +2051,7 @@ export class MajikMessage {
|
|
|
2163
2051
|
return MajikSignature.getSealInfo(file, options);
|
|
2164
2052
|
}
|
|
2165
2053
|
catch (err) {
|
|
2166
|
-
this.
|
|
2054
|
+
this._emit("error", err, { context: "getSealInfo" });
|
|
2167
2055
|
throw err;
|
|
2168
2056
|
}
|
|
2169
2057
|
}
|
|
@@ -2176,7 +2064,7 @@ export class MajikMessage {
|
|
|
2176
2064
|
return MajikSignature.isSealed(file, options);
|
|
2177
2065
|
}
|
|
2178
2066
|
catch (err) {
|
|
2179
|
-
this.
|
|
2067
|
+
this._emit("error", err, { context: "isSealed" });
|
|
2180
2068
|
throw err;
|
|
2181
2069
|
}
|
|
2182
2070
|
}
|
|
@@ -2197,14 +2085,14 @@ export class MajikMessage {
|
|
|
2197
2085
|
}
|
|
2198
2086
|
// Option B: contact ID looked up from the contact directory
|
|
2199
2087
|
if (options.contactID) {
|
|
2200
|
-
const contact = this.
|
|
2088
|
+
const contact = this._contacts.getContact(options.contactID);
|
|
2201
2089
|
if (!contact) {
|
|
2202
2090
|
throw new Error(`No contact found for id "${options.contactID}"`);
|
|
2203
2091
|
}
|
|
2204
2092
|
// Own accounts are in the keystore — get their signing keys directly
|
|
2205
2093
|
const ownAccount = this.getOwnAccountById(options.contactID);
|
|
2206
2094
|
if (ownAccount) {
|
|
2207
|
-
const key =
|
|
2095
|
+
const key = this.keyManager.get(options.contactID);
|
|
2208
2096
|
if (key?.hasSigningKeys) {
|
|
2209
2097
|
return MajikSignature.publicKeysFromMajikKey(key);
|
|
2210
2098
|
}
|
|
@@ -2222,7 +2110,7 @@ export class MajikMessage {
|
|
|
2222
2110
|
}
|
|
2223
2111
|
// Option C: raw base64 public key — look up via contact directory
|
|
2224
2112
|
if (options.publicKeyBase64) {
|
|
2225
|
-
const contact = await this.
|
|
2113
|
+
const contact = await this._contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
|
|
2226
2114
|
if (!contact) {
|
|
2227
2115
|
throw new Error(`No contact found for public key "${options.publicKeyBase64}"`);
|
|
2228
2116
|
}
|
|
@@ -2237,182 +2125,45 @@ export class MajikMessage {
|
|
|
2237
2125
|
}
|
|
2238
2126
|
return null;
|
|
2239
2127
|
}
|
|
2240
|
-
//
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
const accounts = [];
|
|
2249
|
-
for (const id of this.ownAccountsOrder) {
|
|
2250
|
-
const acct = this.ownAccounts.get(id);
|
|
2251
|
-
if (acct)
|
|
2252
|
-
accounts.push(await acct.toJSON());
|
|
2253
|
-
}
|
|
2254
|
-
json.ownAccounts = { accounts, order: [...this.ownAccountsOrder] };
|
|
2255
|
-
}
|
|
2256
|
-
catch (e) {
|
|
2257
|
-
console.warn("Failed to serialize ownAccounts:", e);
|
|
2258
|
-
}
|
|
2259
|
-
return json;
|
|
2260
|
-
}
|
|
2261
|
-
static async fromJSON(json) {
|
|
2262
|
-
// const migratedJSON = migrateMajikMessageJSON(json);
|
|
2263
|
-
// ── Step 2: restore MajikContactManager (directory + groups together) ─
|
|
2264
|
-
const contactManager = await MajikContactManager.fromJSON(json.contacts);
|
|
2265
|
-
const envelopeCache = EnvelopeCache.fromJSON(json.envelopeCache);
|
|
2266
|
-
const instance = new this({ contactManager, envelopeCache }, json.id);
|
|
2128
|
+
// ==========================================================================
|
|
2129
|
+
// ── RESET ─────────────────────────────────────────────────────────────────
|
|
2130
|
+
// ==========================================================================
|
|
2131
|
+
/**
|
|
2132
|
+
* Wipe all data from every adapter and reset in-memory state.
|
|
2133
|
+
* The client remains usable — call hydrate() or add new accounts after reset.
|
|
2134
|
+
*/
|
|
2135
|
+
async resetData() {
|
|
2267
2136
|
try {
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
instance.ownAccounts.set(contact.id, contact);
|
|
2275
|
-
}
|
|
2276
|
-
catch (e) {
|
|
2277
|
-
console.info("Fallback restoring own account (raw-key wrapper)", acct.id, e);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
if (Array.isArray(json.ownAccounts.order)) {
|
|
2281
|
-
instance.ownAccountsOrder = [...json.ownAccounts.order];
|
|
2282
|
-
}
|
|
2283
|
-
// Fallback: populate from contacts if accounts array failed
|
|
2284
|
-
if (instance.ownAccounts.size === 0) {
|
|
2285
|
-
for (const id of instance.ownAccountsOrder) {
|
|
2286
|
-
const c = instance.contacts.getContact(id);
|
|
2287
|
-
if (c)
|
|
2288
|
-
instance.ownAccounts.set(id, c);
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
// Ensure own accounts are in contacts
|
|
2292
|
-
instance.ownAccountsOrder.forEach((id) => {
|
|
2293
|
-
const c = instance.ownAccounts.get(id);
|
|
2294
|
-
if (c && !instance.contacts.hasContact(c.id)) {
|
|
2295
|
-
instance.contacts.addContact(c);
|
|
2296
|
-
}
|
|
2297
|
-
});
|
|
2137
|
+
await this._keys.adapter.clear();
|
|
2138
|
+
await this._contacts.clear();
|
|
2139
|
+
await this._state.clear();
|
|
2140
|
+
if (this._db) {
|
|
2141
|
+
await this._db.vacuum();
|
|
2142
|
+
await this._db.optimize();
|
|
2298
2143
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
return instance;
|
|
2304
|
-
}
|
|
2305
|
-
// ── Persistence ───────────────────────────────────────────────────────────
|
|
2306
|
-
attachAutosaveHandlers() {
|
|
2307
|
-
if (typeof window === "undefined")
|
|
2308
|
-
return;
|
|
2309
|
-
try {
|
|
2310
|
-
window.addEventListener("beforeunload", () => void this.saveState());
|
|
2311
|
-
}
|
|
2312
|
-
catch {
|
|
2313
|
-
/* ignore */
|
|
2314
|
-
}
|
|
2315
|
-
this.startAutosave();
|
|
2316
|
-
}
|
|
2317
|
-
startAutosave() {
|
|
2318
|
-
if (this.autosaveIntervalId || typeof window === "undefined")
|
|
2319
|
-
return;
|
|
2320
|
-
this.autosaveIntervalId = window.setInterval(() => void this.saveState(), this.autosaveIntervalMs);
|
|
2321
|
-
}
|
|
2322
|
-
stopAutosave() {
|
|
2323
|
-
if (!this.autosaveIntervalId || typeof window === "undefined")
|
|
2324
|
-
return;
|
|
2325
|
-
window.clearInterval(this.autosaveIntervalId);
|
|
2326
|
-
this.autosaveIntervalId = null;
|
|
2327
|
-
}
|
|
2328
|
-
scheduleAutosave() {
|
|
2329
|
-
if (typeof window === "undefined")
|
|
2330
|
-
return;
|
|
2331
|
-
if (this.autosaveTimer)
|
|
2332
|
-
window.clearTimeout(this.autosaveTimer);
|
|
2333
|
-
this.autosaveTimer = window.setTimeout(() => {
|
|
2334
|
-
void this.saveState();
|
|
2335
|
-
this.autosaveTimer = null;
|
|
2336
|
-
}, this.autosaveDebounceMs);
|
|
2337
|
-
}
|
|
2338
|
-
async saveState() {
|
|
2339
|
-
try {
|
|
2340
|
-
const json = await this.toJSON();
|
|
2341
|
-
await idbSaveBlob("majik-message-state", autoSaveMajikFileData(json), this.userProfile);
|
|
2342
|
-
}
|
|
2343
|
-
catch (err) {
|
|
2344
|
-
console.error("Failed to save MajikMessage state:", err);
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
async loadState() {
|
|
2348
|
-
try {
|
|
2349
|
-
const saved = await idbLoadBlob("majik-message-state", this.userProfile);
|
|
2350
|
-
if (!saved?.data)
|
|
2351
|
-
return;
|
|
2352
|
-
const loaded = await loadSavedMajikFileData(saved.data);
|
|
2353
|
-
// Pass raw parsed object — fromJSON handles migration internally
|
|
2354
|
-
const restored = await MajikMessage.fromJSON(loaded.j);
|
|
2355
|
-
this.id = restored.id;
|
|
2356
|
-
this.contacts = restored.contacts;
|
|
2357
|
-
this.envelopeCache = restored.envelopeCache;
|
|
2358
|
-
this.ownAccounts = restored.ownAccounts;
|
|
2359
|
-
this.ownAccountsOrder = [...restored.ownAccountsOrder];
|
|
2144
|
+
this._ownAccounts.clear();
|
|
2145
|
+
this._ownAccountsOrder = [];
|
|
2146
|
+
this._keys = new MajikKeyManager(this._keys.adapter);
|
|
2147
|
+
this._emit("active-account-change", null);
|
|
2360
2148
|
}
|
|
2361
2149
|
catch (err) {
|
|
2362
|
-
|
|
2150
|
+
throw new Error(`Failed to reset data: ${err instanceof Error ? err.message : err}`);
|
|
2363
2151
|
}
|
|
2364
2152
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
catch (err) {
|
|
2376
|
-
console.warn("Error loading saved MajikMessage state:", err);
|
|
2153
|
+
// ==========================================================================
|
|
2154
|
+
// ── PRIVATE HELPERS ───────────────────────────────────────────────────────
|
|
2155
|
+
// ==========================================================================
|
|
2156
|
+
_registerOwnAccount(contact) {
|
|
2157
|
+
if (!this._ownAccounts.has(contact.id)) {
|
|
2158
|
+
this._ownAccounts.set(contact.id, contact);
|
|
2159
|
+
this._ownAccountsOrder.push(contact.id);
|
|
2160
|
+
this._scheduleOrderSave();
|
|
2377
2161
|
}
|
|
2378
|
-
|
|
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);
|
|
2162
|
+
if (!this._contacts.hasContact(contact.id)) {
|
|
2163
|
+
this._contacts.addContact(contact);
|
|
2413
2164
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2165
|
+
if (!this.getActiveAccount()) {
|
|
2166
|
+
void this.setActiveAccount(contact.id, true);
|
|
2416
2167
|
}
|
|
2417
2168
|
}
|
|
2418
2169
|
}
|