@majikah/majik-message 0.2.21 → 0.3.1
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/dist/core/contacts/errors.d.ts +12 -0
- package/dist/core/contacts/errors.js +27 -0
- package/dist/core/contacts/majik-contact-directory.d.ts +2 -8
- package/dist/core/contacts/majik-contact-directory.js +1 -11
- package/dist/core/contacts/majik-contact-groups.d.ts +185 -0
- package/dist/core/contacts/majik-contact-groups.js +557 -0
- package/dist/core/contacts/majik-contact-manager.d.ts +240 -0
- package/dist/core/contacts/majik-contact-manager.js +449 -0
- package/dist/core/contacts/majik-contact-migration.d.ts +27 -0
- package/dist/core/contacts/majik-contact-migration.js +84 -0
- package/dist/core/contacts/types.d.ts +11 -0
- package/dist/core/contacts/types.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/majik-message.d.ts +160 -36
- package/dist/majik-message.js +287 -127
- package/package.json +4 -4
package/dist/majik-message.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// MajikMessage.ts
|
|
2
2
|
import { MajikContact, } from "@majikah/majik-contact";
|
|
3
3
|
import { KEY_ALGO } from "./core/crypto/constants";
|
|
4
|
-
import { ScannerEngine } from "./core/scanner/scanner-engine";
|
|
5
4
|
import { MessageEnvelope } from "./core/messages/message-envelope";
|
|
6
5
|
import { EnvelopeCache, } from "./core/messages/envelope-cache";
|
|
7
6
|
import { MajikKeyStore } from "./core/crypto/keystore";
|
|
8
|
-
import { MajikContactDirectory, } from "./core/contacts/majik-contact-directory";
|
|
9
7
|
import { arrayBufferToBase64, arrayToBase64, base64ToArrayBuffer, base64ToUint8Array, } from "./core/utils/utilities";
|
|
10
8
|
import { autoSaveMajikFileData, loadSavedMajikFileData, } from "./core/utils/majik-file-utils";
|
|
11
9
|
import { randomBytes } from "@stablelib/random";
|
|
@@ -16,14 +14,13 @@ import { MajikEnvelope, } from "@majikah/majik-envelope";
|
|
|
16
14
|
import { MajikFile, MajikFileError, } from "@majikah/majik-file";
|
|
17
15
|
import { MajikSignature, } from "@majikah/majik-signature";
|
|
18
16
|
import { gzipSync, gunzipSync } from "fflate";
|
|
17
|
+
import { MajikContactManager } from "./core/contacts/majik-contact-manager";
|
|
19
18
|
// ─── MajikMessage ─────────────────────────────────────────────────────────────
|
|
20
19
|
export class MajikMessage {
|
|
21
20
|
userProfile = "default";
|
|
22
|
-
pinHash = null;
|
|
23
21
|
id;
|
|
24
|
-
|
|
22
|
+
contacts;
|
|
25
23
|
envelopeCache;
|
|
26
|
-
scanner;
|
|
27
24
|
listeners = new Map();
|
|
28
25
|
ownAccounts = new Map();
|
|
29
26
|
ownAccountsOrder = [];
|
|
@@ -34,16 +31,9 @@ export class MajikMessage {
|
|
|
34
31
|
constructor(config, id, userProfile = "default") {
|
|
35
32
|
this.userProfile = userProfile || "default";
|
|
36
33
|
this.id = id || arrayToBase64(randomBytes(32));
|
|
37
|
-
this.
|
|
38
|
-
config.contactDirectory || new MajikContactDirectory();
|
|
34
|
+
this.contacts = config.contactManager ?? new MajikContactManager();
|
|
39
35
|
this.envelopeCache =
|
|
40
36
|
config.envelopeCache || new EnvelopeCache(undefined, userProfile);
|
|
41
|
-
this.scanner = new ScannerEngine({
|
|
42
|
-
contactDirectory: this.contactDirectory,
|
|
43
|
-
onEnvelopeFound: (env) => this.handleEnvelope(env),
|
|
44
|
-
onUntrusted: (raw) => this.emit("untrusted", raw),
|
|
45
|
-
onError: (err, ctx) => this.emit("error", err, ctx),
|
|
46
|
-
});
|
|
47
37
|
const events = [
|
|
48
38
|
"message",
|
|
49
39
|
"envelope",
|
|
@@ -58,38 +48,13 @@ export class MajikMessage {
|
|
|
58
48
|
events.forEach((e) => this.listeners.set(e, []));
|
|
59
49
|
this.attachAutosaveHandlers();
|
|
60
50
|
}
|
|
61
|
-
// ── Private: Envelope helpers ────────────────────────────────────────────
|
|
62
|
-
// /**
|
|
63
|
-
// * Resolve a list of account/contact IDs into MajikRecipient objects.
|
|
64
|
-
// * Each recipient needs their ML-KEM public key from MajikKeyStore.
|
|
65
|
-
// */
|
|
66
|
-
// private async _resolveRecipients(ids: string[]): Promise<MajikRecipient[]> {
|
|
67
|
-
// return Promise.all(
|
|
68
|
-
// ids.map(async (id) => {
|
|
69
|
-
// const contact = this.contactDirectory.getContact(id);
|
|
70
|
-
// if (!contact) throw new Error(`No contact found for id "${id}"`);
|
|
71
|
-
// // const key = await MajikKeyStore.load(id);
|
|
72
|
-
// const mlPubKey = base64ToUint8Array(contact.mlKey);
|
|
73
|
-
// if (!mlPubKey) {
|
|
74
|
-
// throw new Error(
|
|
75
|
-
// `Contact "${id}" has no ML-KEM public key. ` +
|
|
76
|
-
// `They may need to upgrade their account via importFromMnemonicBackup().`,
|
|
77
|
-
// );
|
|
78
|
-
// }
|
|
79
|
-
// return {
|
|
80
|
-
// fingerprint: contact.fingerprint,
|
|
81
|
-
// mlKemPublicKey: mlPubKey,
|
|
82
|
-
// } satisfies MajikRecipient;
|
|
83
|
-
// }),
|
|
84
|
-
// );
|
|
85
|
-
// }
|
|
86
51
|
/**
|
|
87
52
|
* Resolve a list of account/contact IDs into MajikRecipient objects.
|
|
88
53
|
* Each recipient needs their ML-KEM public key from MajikKeyStore.
|
|
89
54
|
*/
|
|
90
55
|
async _resolveRecipientsByPublicKey(publicKeys) {
|
|
91
56
|
return Promise.all(publicKeys.map(async (pkey) => {
|
|
92
|
-
const contact = await this.
|
|
57
|
+
const contact = await this.contacts.getContactByPublicKeyBase64(pkey);
|
|
93
58
|
if (!contact)
|
|
94
59
|
throw new Error(`No contact found for public key "${pkey}"`);
|
|
95
60
|
const mlPubKey = base64ToUint8Array(contact.mlKey);
|
|
@@ -159,7 +124,7 @@ export class MajikMessage {
|
|
|
159
124
|
*/
|
|
160
125
|
async _resolveFileRecipientsByPublicKey(publicKeys) {
|
|
161
126
|
return Promise.all(publicKeys.map(async (pkey) => {
|
|
162
|
-
const contact = await this.
|
|
127
|
+
const contact = await this.contacts.getContactByPublicKeyBase64(pkey);
|
|
163
128
|
if (!contact)
|
|
164
129
|
throw new Error(`No contact found for public key "${pkey}"`);
|
|
165
130
|
const mlPubKey = base64ToUint8Array(contact.mlKey);
|
|
@@ -216,8 +181,8 @@ export class MajikMessage {
|
|
|
216
181
|
this.ownAccountsOrder.push(account.id);
|
|
217
182
|
}
|
|
218
183
|
try {
|
|
219
|
-
if (!this.
|
|
220
|
-
this.
|
|
184
|
+
if (!this.contacts.hasContact(account.id)) {
|
|
185
|
+
this.contacts.addContact(account);
|
|
221
186
|
}
|
|
222
187
|
if (!this.getActiveAccount()) {
|
|
223
188
|
this.setActiveAccount(account.id);
|
|
@@ -298,25 +263,25 @@ export class MajikMessage {
|
|
|
298
263
|
getContactByID(id) {
|
|
299
264
|
if (!id?.trim())
|
|
300
265
|
throw new Error("Invalid contact ID");
|
|
301
|
-
return this.
|
|
266
|
+
return this.contacts.getContact(id) ?? null;
|
|
302
267
|
}
|
|
303
268
|
hasContact(id) {
|
|
304
269
|
if (!id?.trim())
|
|
305
270
|
throw new Error("Invalid contact ID");
|
|
306
|
-
return this.
|
|
271
|
+
return this.contacts.hasContact(id);
|
|
307
272
|
}
|
|
308
273
|
async hasContactByPublicKeyBase64(publicKey) {
|
|
309
274
|
if (!publicKey?.trim())
|
|
310
275
|
throw new Error("Invalid contact public key");
|
|
311
|
-
return await this.
|
|
276
|
+
return await this.contacts.hasContactByPublicKeyBase64(publicKey);
|
|
312
277
|
}
|
|
313
278
|
async getContactByPublicKey(publicKeyBase64) {
|
|
314
279
|
if (!publicKeyBase64?.trim())
|
|
315
280
|
throw new Error("Invalid public key");
|
|
316
|
-
return ((await this.
|
|
281
|
+
return ((await this.contacts.getContactByPublicKeyBase64(publicKeyBase64)) ?? null);
|
|
317
282
|
}
|
|
318
|
-
async exportContactAsJSON(
|
|
319
|
-
const contact = this.
|
|
283
|
+
async exportContactAsJSON(contactID) {
|
|
284
|
+
const contact = this.contacts.getContact(contactID);
|
|
320
285
|
if (!contact)
|
|
321
286
|
return null;
|
|
322
287
|
let publicKeyBase64;
|
|
@@ -338,8 +303,8 @@ export class MajikMessage {
|
|
|
338
303
|
mlDsaPublicKeyBase64: contact.mlDsaPublicKeyBase64,
|
|
339
304
|
}, null, 2);
|
|
340
305
|
}
|
|
341
|
-
async exportContactAsString(
|
|
342
|
-
const contact = this.
|
|
306
|
+
async exportContactAsString(contactID) {
|
|
307
|
+
const contact = this.contacts.getContact(contactID);
|
|
343
308
|
if (!contact)
|
|
344
309
|
return null;
|
|
345
310
|
const compressedString = this.exportContactCompressed(contact);
|
|
@@ -450,45 +415,276 @@ export class MajikMessage {
|
|
|
450
415
|
!contact?.mlKey) {
|
|
451
416
|
throw new Error("Invalid contact JSON");
|
|
452
417
|
}
|
|
453
|
-
this.
|
|
418
|
+
this.contacts.addContact(contact);
|
|
454
419
|
this.emit("new-contact", contact);
|
|
455
420
|
this.scheduleAutosave();
|
|
456
421
|
}
|
|
457
422
|
removeContact(id) {
|
|
458
|
-
const result = this.
|
|
423
|
+
const result = this.contacts.removeContact(id);
|
|
459
424
|
if (!result.success)
|
|
460
425
|
throw new Error(result.message);
|
|
461
426
|
this.emit("removed-contact", id);
|
|
462
427
|
this.scheduleAutosave();
|
|
463
428
|
}
|
|
464
429
|
updateContactMeta(id, meta) {
|
|
465
|
-
this.
|
|
430
|
+
this.contacts.updateContactMeta(id, meta);
|
|
466
431
|
this.scheduleAutosave();
|
|
467
432
|
}
|
|
468
433
|
blockContact(id) {
|
|
469
|
-
this.
|
|
434
|
+
this.contacts.blockContact(id);
|
|
470
435
|
this.scheduleAutosave();
|
|
471
436
|
}
|
|
472
437
|
unblockContact(id) {
|
|
473
|
-
this.
|
|
438
|
+
this.contacts.unblockContact(id);
|
|
474
439
|
this.scheduleAutosave();
|
|
475
440
|
}
|
|
476
441
|
listContacts(all = true, majikahOnly = false) {
|
|
477
|
-
const contacts = this.
|
|
442
|
+
const contacts = this.contacts.listContacts(true, majikahOnly);
|
|
478
443
|
if (all)
|
|
479
444
|
return contacts;
|
|
480
445
|
const ownIds = new Set(this.listOwnAccounts(majikahOnly).map((a) => a.id));
|
|
481
446
|
return contacts.filter((c) => !ownIds.has(c.id));
|
|
482
447
|
}
|
|
483
448
|
isContactMajikahRegistered(id) {
|
|
484
|
-
return this.
|
|
449
|
+
return this.contacts.isMajikahRegistered(id);
|
|
485
450
|
}
|
|
486
451
|
isContactMajikahIdentityChecked(id) {
|
|
487
|
-
return this.
|
|
452
|
+
return this.contacts.isMajikahIdentityChecked(id);
|
|
488
453
|
}
|
|
489
454
|
setContactMajikahStatus(id, status) {
|
|
490
|
-
this.
|
|
455
|
+
this.contacts.setMajikahStatus(id, status);
|
|
456
|
+
this.scheduleAutosave();
|
|
457
|
+
}
|
|
458
|
+
/* ================================
|
|
459
|
+
* Group CRUD Pass-throughs
|
|
460
|
+
* ================================ */
|
|
461
|
+
/**
|
|
462
|
+
* Creates and registers a new user-defined group.
|
|
463
|
+
* Throws if a group with the same ID already exists.
|
|
464
|
+
*/
|
|
465
|
+
createGroup(id, name, meta, initialMemberIds) {
|
|
466
|
+
const newGroup = this.contacts.createGroup(id, name, meta, initialMemberIds);
|
|
467
|
+
this.emit("new-contact-group", newGroup);
|
|
468
|
+
this.scheduleAutosave();
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Registers an already-constructed MajikContactGroup instance.
|
|
473
|
+
* Throws if a group with the same ID already exists.
|
|
474
|
+
*/
|
|
475
|
+
addGroup(group) {
|
|
476
|
+
this.contacts.addGroup(group);
|
|
477
|
+
this.emit("new-contact-group", group);
|
|
478
|
+
this.scheduleAutosave();
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Removes a user group by ID.
|
|
483
|
+
* System groups (Favorites, Blocked) cannot be deleted.
|
|
484
|
+
*/
|
|
485
|
+
removeGroup(id) {
|
|
486
|
+
const response = this.contacts.removeGroup(id);
|
|
487
|
+
this.emit("removed-contact-group", response.data);
|
|
488
|
+
this.scheduleAutosave();
|
|
489
|
+
return response;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Returns a group by ID, or undefined if not found.
|
|
493
|
+
*/
|
|
494
|
+
getContactGroup(id) {
|
|
495
|
+
return this.contacts.getGroup(id);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Returns a group by ID. Throws if not found.
|
|
499
|
+
*/
|
|
500
|
+
getGroupOrThrow(id) {
|
|
501
|
+
return this.contacts.getGroupOrThrow(id);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Returns true if a group with the given ID exists.
|
|
505
|
+
*/
|
|
506
|
+
hasGroup(id) {
|
|
507
|
+
return this.contacts.hasGroup(id);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Returns all groups.
|
|
511
|
+
*
|
|
512
|
+
* @param includeSystem Include system groups (Favorites, Blocked). Default: true.
|
|
513
|
+
* @param sortedByName Sort results alphabetically by group name. Default: false.
|
|
514
|
+
*/
|
|
515
|
+
listContactGroups(includeSystem = true, sortedByName = false) {
|
|
516
|
+
return this.contacts.listGroups(includeSystem, sortedByName);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Returns only user-created groups (excludes Favorites and Blocked).
|
|
520
|
+
* Sorted alphabetically by name.
|
|
521
|
+
*/
|
|
522
|
+
listUserGroups(sortedByName = true) {
|
|
523
|
+
return this.contacts.listGroups(false, sortedByName);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Returns only system groups (Favorites and Blocked).
|
|
527
|
+
*/
|
|
528
|
+
listSystemGroups() {
|
|
529
|
+
return this.contacts.listGroups(true).filter((g) => g.isSystem);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Updates mutable metadata on a group (name, description).
|
|
533
|
+
* Name is locked on system groups — will throw if attempted.
|
|
534
|
+
*/
|
|
535
|
+
updateGroupMeta(id, meta) {
|
|
536
|
+
const updatedGroup = this.contacts.updateGroupMeta(id, meta);
|
|
537
|
+
this.emit("contact-group-change", updatedGroup);
|
|
491
538
|
this.scheduleAutosave();
|
|
539
|
+
return this;
|
|
540
|
+
}
|
|
541
|
+
/* ================================
|
|
542
|
+
* Group Membership Pass-throughs
|
|
543
|
+
* ================================ */
|
|
544
|
+
/**
|
|
545
|
+
* Adds a contact to a group.
|
|
546
|
+
* Validates the contact exists in the directory.
|
|
547
|
+
* If the group is the system Blocked group, also calls contact.block().
|
|
548
|
+
* Throws if the contact is already a member — use addContactToGroupIfAbsent for idempotent.
|
|
549
|
+
*/
|
|
550
|
+
addContactToGroup(groupID, contactID) {
|
|
551
|
+
const updatedGroup = this.contacts.addContactToGroup(groupID, contactID);
|
|
552
|
+
this.emit("contact-group-change", updatedGroup);
|
|
553
|
+
this.scheduleAutosave();
|
|
554
|
+
return this;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Adds multiple contacts to a group in one call (all-or-nothing).
|
|
558
|
+
*/
|
|
559
|
+
addContactsToGroup(groupID, contactIds) {
|
|
560
|
+
const updatedGroup = this.contacts.addContactsToGroup(groupID, contactIds);
|
|
561
|
+
this.emit("contact-group-change", updatedGroup);
|
|
562
|
+
this.scheduleAutosave();
|
|
563
|
+
return this;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Removes a contact from a group.
|
|
567
|
+
* If the group is the system Blocked group, also calls contact.unblock().
|
|
568
|
+
* Throws if the contact is not a member — use removeContactFromGroupIfPresent for idempotent.
|
|
569
|
+
*/
|
|
570
|
+
removeContactFromGroup(groupID, contactID) {
|
|
571
|
+
const updatedGroup = this.contacts.removeContactFromGroup(groupID, contactID);
|
|
572
|
+
this.emit("contact-group-change", updatedGroup);
|
|
573
|
+
this.scheduleAutosave();
|
|
574
|
+
return this;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Moves a contact from one group to another atomically.
|
|
578
|
+
* Throws if the contact is not a member of the source group.
|
|
579
|
+
*/
|
|
580
|
+
moveContactBetweenGroups(contactID, fromGroupId, toGroupId) {
|
|
581
|
+
const updatedGroup = this.contacts.moveContactBetweenGroups(contactID, fromGroupId, toGroupId);
|
|
582
|
+
this.emit("contact-group-change", updatedGroup);
|
|
583
|
+
this.scheduleAutosave();
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
/* ================================
|
|
587
|
+
* Group Query Pass-throughs
|
|
588
|
+
* ================================ */
|
|
589
|
+
/**
|
|
590
|
+
* Returns all hydrated MajikContact instances in the given group.
|
|
591
|
+
* Contacts removed from the directory since last save are silently skipped.
|
|
592
|
+
*/
|
|
593
|
+
getContactsInGroup(groupID) {
|
|
594
|
+
return this.contacts.getContactsInGroup(groupID);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Returns hydrated contacts in the group, sorted by label (or ID if no label).
|
|
598
|
+
*/
|
|
599
|
+
getContactsInGroupSorted(groupID) {
|
|
600
|
+
return this.contacts.getContactsInGroupSorted(groupID);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Returns true if the contact is a member of the given group.
|
|
604
|
+
*/
|
|
605
|
+
isContactInGroup(groupID, contactID) {
|
|
606
|
+
return this.contacts.isContactInGroup(groupID, contactID);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Returns all groups the contact belongs to.
|
|
610
|
+
*/
|
|
611
|
+
getGroupsForContact(contactID) {
|
|
612
|
+
return this.contacts.getGroupsForContact(contactID);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Returns all group IDs the contact belongs to.
|
|
616
|
+
*/
|
|
617
|
+
getGroupIdsForContact(contactID) {
|
|
618
|
+
return this.contacts.getGroupIdsForContact(contactID);
|
|
619
|
+
}
|
|
620
|
+
/* ================================
|
|
621
|
+
* System Group Convenience Pass-throughs
|
|
622
|
+
* ================================ */
|
|
623
|
+
/**
|
|
624
|
+
* Adds the contact to the Favorites group (idempotent).
|
|
625
|
+
*/
|
|
626
|
+
addContactToFavorites(contactID) {
|
|
627
|
+
const updatedGroup = this.contacts.addToFavorites(contactID);
|
|
628
|
+
this.emit("contact-group-change", updatedGroup);
|
|
629
|
+
this.scheduleAutosave();
|
|
630
|
+
return this;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Removes the contact from the Favorites group (idempotent).
|
|
634
|
+
*/
|
|
635
|
+
removeContactFromFavorites(contactID) {
|
|
636
|
+
const updatedGroup = this.contacts.removeFromFavorites(contactID);
|
|
637
|
+
this.emit("contact-group-change", updatedGroup);
|
|
638
|
+
this.scheduleAutosave();
|
|
639
|
+
return this;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Returns true if the contact is in the Favorites group.
|
|
643
|
+
*/
|
|
644
|
+
isContactFavorite(contactID) {
|
|
645
|
+
return this.contacts.isFavorite(contactID);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Returns true if the contact is in the Blocked group.
|
|
649
|
+
*/
|
|
650
|
+
isContactBlocked(contactID) {
|
|
651
|
+
return this.contacts.isContactBlocked(contactID);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Returns the Favorites system group instance.
|
|
655
|
+
*/
|
|
656
|
+
getFavoritesGroup() {
|
|
657
|
+
return this.contacts.getFavoritesGroup();
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Returns the Blocked system group instance.
|
|
661
|
+
*/
|
|
662
|
+
getBlockedGroup() {
|
|
663
|
+
return this.contacts.getBlockedGroup();
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Returns all contacts in the Favorites group as hydrated MajikContact instances.
|
|
667
|
+
*/
|
|
668
|
+
getFavoriteContacts() {
|
|
669
|
+
return this.contacts.getContactsInGroup(this.contacts.getFavoritesGroup().id);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Returns all contacts in the Blocked group as hydrated MajikContact instances.
|
|
673
|
+
*/
|
|
674
|
+
getBlockedContacts() {
|
|
675
|
+
return this.contacts.getContactsInGroup(this.contacts.getBlockedGroup().id);
|
|
676
|
+
}
|
|
677
|
+
/* ================================
|
|
678
|
+
* Directory Clear
|
|
679
|
+
* ================================ */
|
|
680
|
+
/**
|
|
681
|
+
* Clears both the directory and all group memberships.
|
|
682
|
+
* System groups are preserved (re-bootstrapped by the group manager).
|
|
683
|
+
*/
|
|
684
|
+
clearDirectory() {
|
|
685
|
+
this.contacts.clear();
|
|
686
|
+
this.scheduleAutosave();
|
|
687
|
+
return this;
|
|
492
688
|
}
|
|
493
689
|
// ── Encryption / Decryption ───────────────────────────────────────────────
|
|
494
690
|
/**
|
|
@@ -764,7 +960,7 @@ export class MajikMessage {
|
|
|
764
960
|
* metadata: row,
|
|
765
961
|
* });
|
|
766
962
|
* if (signature) {
|
|
767
|
-
* const result = await majik.verifyMajikFile(file, {
|
|
963
|
+
* const result = await majik.verifyMajikFile(file, { contactID: row.user_id });
|
|
768
964
|
* }
|
|
769
965
|
* ```
|
|
770
966
|
*/
|
|
@@ -857,7 +1053,7 @@ export class MajikMessage {
|
|
|
857
1053
|
* if the instance was restored from a metadata-only Supabase row.
|
|
858
1054
|
*
|
|
859
1055
|
* Signer resolution:
|
|
860
|
-
* -
|
|
1056
|
+
* - contactID: looked up in the contact directory (own accounts included)
|
|
861
1057
|
* - publicKeyBase64: looked up via contact directory
|
|
862
1058
|
* - key: used directly (skips directory lookup)
|
|
863
1059
|
* - none provided: falls back to public keys embedded in the signature
|
|
@@ -868,7 +1064,7 @@ export class MajikMessage {
|
|
|
868
1064
|
* @example — verify against the file's owner contact
|
|
869
1065
|
* file.attachBinary(await r2.get(row.r2_key).arrayBuffer());
|
|
870
1066
|
* const result = await majik.verifyMajikFile(file, {
|
|
871
|
-
*
|
|
1067
|
+
* contactID: ownerContactId,
|
|
872
1068
|
* });
|
|
873
1069
|
* if (result?.valid) console.log("Verified, signed by", result.signerId);
|
|
874
1070
|
*/
|
|
@@ -908,7 +1104,7 @@ export class MajikMessage {
|
|
|
908
1104
|
*
|
|
909
1105
|
* @example
|
|
910
1106
|
* const result = await majik.verifyMajikFileBinary(file, {
|
|
911
|
-
*
|
|
1107
|
+
* contactID: "contact_abc",
|
|
912
1108
|
* });
|
|
913
1109
|
* if (result.valid) console.log("Plaintext verified");
|
|
914
1110
|
*/
|
|
@@ -1118,7 +1314,7 @@ export class MajikMessage {
|
|
|
1118
1314
|
*
|
|
1119
1315
|
* @example
|
|
1120
1316
|
* const result = await majik.verifyText("Hello world", sig, {
|
|
1121
|
-
*
|
|
1317
|
+
* contactID: "contact_abc",
|
|
1122
1318
|
* });
|
|
1123
1319
|
* if (result.valid) console.log("Authentic");
|
|
1124
1320
|
*/
|
|
@@ -1144,7 +1340,7 @@ export class MajikMessage {
|
|
|
1144
1340
|
* @example
|
|
1145
1341
|
* const row = await db.findOne({ doc_id });
|
|
1146
1342
|
* const result = await majik.verifyDetached(docBytes, row.signature, {
|
|
1147
|
-
*
|
|
1343
|
+
* contactID: row.signer_contact_id,
|
|
1148
1344
|
* });
|
|
1149
1345
|
* if (result.valid) console.log("Signed by", result.signerId);
|
|
1150
1346
|
*/
|
|
@@ -1278,16 +1474,6 @@ export class MajikMessage {
|
|
|
1278
1474
|
return false;
|
|
1279
1475
|
return MajikKeyStore.isPassphraseValid(target.id, passphrase);
|
|
1280
1476
|
}
|
|
1281
|
-
// ── DOM Scanning ──────────────────────────────────────────────────────────
|
|
1282
|
-
scanDOM(rootNode) {
|
|
1283
|
-
this.scanner.scanDOM(rootNode);
|
|
1284
|
-
}
|
|
1285
|
-
startDOMObserver(rootNode) {
|
|
1286
|
-
this.scanner.startDOMObserver(rootNode);
|
|
1287
|
-
}
|
|
1288
|
-
stopDOMObserver() {
|
|
1289
|
-
this.scanner.stopDOMObserver();
|
|
1290
|
-
}
|
|
1291
1477
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
1292
1478
|
on(event, callback) {
|
|
1293
1479
|
this.listeners.get(event)?.push(callback);
|
|
@@ -1509,7 +1695,7 @@ export class MajikMessage {
|
|
|
1509
1695
|
* > against a known contact fingerprint before trusting the result.
|
|
1510
1696
|
*
|
|
1511
1697
|
* @example — verify against a known contact
|
|
1512
|
-
* const result = await majik.verifyContent(docBytes, sig, {
|
|
1698
|
+
* const result = await majik.verifyContent(docBytes, sig, { contactID: "contact_abc" });
|
|
1513
1699
|
* if (result.valid) console.log("Authentic, signed by:", result.signerId);
|
|
1514
1700
|
*
|
|
1515
1701
|
* @example — verify using embedded keys (self-reported)
|
|
@@ -1545,12 +1731,12 @@ export class MajikMessage {
|
|
|
1545
1731
|
* envelope are used (self-reported — see security note on verifyContent).
|
|
1546
1732
|
*
|
|
1547
1733
|
* @example — verify a signed PDF against a known contact
|
|
1548
|
-
* const result = await majik.verifyFile(signedPdf, {
|
|
1734
|
+
* const result = await majik.verifyFile(signedPdf, { contactID: "contact_abc" });
|
|
1549
1735
|
* if (result.valid) console.log("Verified:", result.signerId, result.timestamp);
|
|
1550
1736
|
*
|
|
1551
1737
|
* @example — check own signed file using active account
|
|
1552
1738
|
* const result = await majik.verifyFile(signedWav, {
|
|
1553
|
-
*
|
|
1739
|
+
* contactID: majik.getActiveAccount()?.id,
|
|
1554
1740
|
* });
|
|
1555
1741
|
*/
|
|
1556
1742
|
async verifyFile(file, options) {
|
|
@@ -1564,7 +1750,7 @@ export class MajikMessage {
|
|
|
1564
1750
|
return results[0];
|
|
1565
1751
|
}
|
|
1566
1752
|
// No signer provided — extract and use self-reported keys from first signature.
|
|
1567
|
-
// For full multi-sig verification, pass a
|
|
1753
|
+
// For full multi-sig verification, pass a contactID or publicKeyBase64.
|
|
1568
1754
|
const extracted = await MajikSignature.extractFrom(file, {
|
|
1569
1755
|
mimeType: options?.mimeType,
|
|
1570
1756
|
});
|
|
@@ -1599,7 +1785,7 @@ export class MajikMessage {
|
|
|
1599
1785
|
* @example
|
|
1600
1786
|
* const results = await majik.batchVerifyFiles(
|
|
1601
1787
|
* [pdfBlob, wavBlob, mp4Blob],
|
|
1602
|
-
* {
|
|
1788
|
+
* { contactID: "contact_abc" },
|
|
1603
1789
|
* );
|
|
1604
1790
|
* const allValid = results.every(r => r.valid);
|
|
1605
1791
|
*/
|
|
@@ -1711,7 +1897,7 @@ export class MajikMessage {
|
|
|
1711
1897
|
*
|
|
1712
1898
|
* @example
|
|
1713
1899
|
* if (await majik.isFileSigned(file)) {
|
|
1714
|
-
* const result = await majik.verifyFile(file, {
|
|
1900
|
+
* const result = await majik.verifyFile(file, { contactID });
|
|
1715
1901
|
* }
|
|
1716
1902
|
*/
|
|
1717
1903
|
async isFileSigned(file, options) {
|
|
@@ -2055,22 +2241,22 @@ export class MajikMessage {
|
|
|
2055
2241
|
return MajikSignature.publicKeysFromMajikKey(options.key);
|
|
2056
2242
|
}
|
|
2057
2243
|
// Option B: contact ID looked up from the contact directory
|
|
2058
|
-
if (options.
|
|
2059
|
-
const contact = this.
|
|
2244
|
+
if (options.contactID) {
|
|
2245
|
+
const contact = this.contacts.getContact(options.contactID);
|
|
2060
2246
|
if (!contact) {
|
|
2061
|
-
throw new Error(`No contact found for id "${options.
|
|
2247
|
+
throw new Error(`No contact found for id "${options.contactID}"`);
|
|
2062
2248
|
}
|
|
2063
2249
|
// Own accounts are in the keystore — get their signing keys directly
|
|
2064
|
-
const ownAccount = this.getOwnAccountById(options.
|
|
2250
|
+
const ownAccount = this.getOwnAccountById(options.contactID);
|
|
2065
2251
|
if (ownAccount) {
|
|
2066
|
-
const key = MajikKeyStore.get(options.
|
|
2252
|
+
const key = MajikKeyStore.get(options.contactID);
|
|
2067
2253
|
if (key?.hasSigningKeys) {
|
|
2068
2254
|
return MajikSignature.publicKeysFromMajikKey(key);
|
|
2069
2255
|
}
|
|
2070
2256
|
}
|
|
2071
2257
|
// External contact — resolve from their contact card fields
|
|
2072
2258
|
if (!contact.edPublicKeyBase64 || !contact.mlDsaPublicKeyBase64) {
|
|
2073
|
-
throw new Error(`Contact "${options.
|
|
2259
|
+
throw new Error(`Contact "${options.contactID}" has no signing public keys. ` +
|
|
2074
2260
|
`They may need to share an updated contact card.`);
|
|
2075
2261
|
}
|
|
2076
2262
|
return {
|
|
@@ -2081,7 +2267,7 @@ export class MajikMessage {
|
|
|
2081
2267
|
}
|
|
2082
2268
|
// Option C: raw base64 public key — look up via contact directory
|
|
2083
2269
|
if (options.publicKeyBase64) {
|
|
2084
|
-
const contact = await this.
|
|
2270
|
+
const contact = await this.contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
|
|
2085
2271
|
if (!contact) {
|
|
2086
2272
|
throw new Error(`No contact found for public key "${options.publicKeyBase64}"`);
|
|
2087
2273
|
}
|
|
@@ -2096,34 +2282,11 @@ export class MajikMessage {
|
|
|
2096
2282
|
}
|
|
2097
2283
|
return null;
|
|
2098
2284
|
}
|
|
2099
|
-
// ── PIN ───────────────────────────────────────────────────────────────────
|
|
2100
|
-
async setPIN(pin) {
|
|
2101
|
-
if (!pin)
|
|
2102
|
-
throw new Error("PIN must be a non-empty string");
|
|
2103
|
-
this.pinHash = await MajikMessage._hashPIN(pin);
|
|
2104
|
-
this.scheduleAutosave();
|
|
2105
|
-
}
|
|
2106
|
-
async clearPIN() {
|
|
2107
|
-
this.pinHash = null;
|
|
2108
|
-
this.scheduleAutosave();
|
|
2109
|
-
}
|
|
2110
|
-
async isValidPIN(pin) {
|
|
2111
|
-
if (!this.pinHash)
|
|
2112
|
-
return true;
|
|
2113
|
-
return (await MajikMessage._hashPIN(pin)) === this.pinHash;
|
|
2114
|
-
}
|
|
2115
|
-
getPinHash() {
|
|
2116
|
-
return this.pinHash ?? null;
|
|
2117
|
-
}
|
|
2118
|
-
static async _hashPIN(pin) {
|
|
2119
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pin));
|
|
2120
|
-
return arrayBufferToBase64(digest);
|
|
2121
|
-
}
|
|
2122
2285
|
// ── Serialization ─────────────────────────────────────────────────────────
|
|
2123
2286
|
async toJSON() {
|
|
2124
2287
|
const json = {
|
|
2125
2288
|
id: this.id,
|
|
2126
|
-
contacts: await this.
|
|
2289
|
+
contacts: await this.contacts.toJSON(),
|
|
2127
2290
|
envelopeCache: this.envelopeCache.toJSON(),
|
|
2128
2291
|
};
|
|
2129
2292
|
try {
|
|
@@ -2138,14 +2301,14 @@ export class MajikMessage {
|
|
|
2138
2301
|
catch (e) {
|
|
2139
2302
|
console.warn("Failed to serialize ownAccounts:", e);
|
|
2140
2303
|
}
|
|
2141
|
-
json.pinHash = this.pinHash ?? null;
|
|
2142
2304
|
return json;
|
|
2143
2305
|
}
|
|
2144
2306
|
static async fromJSON(json) {
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2307
|
+
// const migratedJSON = migrateMajikMessageJSON(json);
|
|
2308
|
+
// ── Step 2: restore MajikContactManager (directory + groups together) ─
|
|
2309
|
+
const contactManager = await MajikContactManager.fromJSON(json.contacts, KEY_ALGO);
|
|
2147
2310
|
const envelopeCache = EnvelopeCache.fromJSON(json.envelopeCache);
|
|
2148
|
-
const instance = new this({
|
|
2311
|
+
const instance = new this({ contactManager, envelopeCache }, json.id);
|
|
2149
2312
|
try {
|
|
2150
2313
|
if (json.ownAccounts && Array.isArray(json.ownAccounts.accounts)) {
|
|
2151
2314
|
for (const acct of json.ownAccounts.accounts) {
|
|
@@ -2162,19 +2325,19 @@ export class MajikMessage {
|
|
|
2162
2325
|
if (Array.isArray(json.ownAccounts.order)) {
|
|
2163
2326
|
instance.ownAccountsOrder = [...json.ownAccounts.order];
|
|
2164
2327
|
}
|
|
2165
|
-
// Fallback: populate from
|
|
2328
|
+
// Fallback: populate from contacts if accounts array failed
|
|
2166
2329
|
if (instance.ownAccounts.size === 0) {
|
|
2167
2330
|
for (const id of instance.ownAccountsOrder) {
|
|
2168
|
-
const c = instance.
|
|
2331
|
+
const c = instance.contacts.getContact(id);
|
|
2169
2332
|
if (c)
|
|
2170
2333
|
instance.ownAccounts.set(id, c);
|
|
2171
2334
|
}
|
|
2172
2335
|
}
|
|
2173
|
-
// Ensure own accounts are in
|
|
2336
|
+
// Ensure own accounts are in contacts
|
|
2174
2337
|
instance.ownAccountsOrder.forEach((id) => {
|
|
2175
2338
|
const c = instance.ownAccounts.get(id);
|
|
2176
|
-
if (c && !instance.
|
|
2177
|
-
instance.
|
|
2339
|
+
if (c && !instance.contacts.hasContact(c.id)) {
|
|
2340
|
+
instance.contacts.addContact(c);
|
|
2178
2341
|
}
|
|
2179
2342
|
});
|
|
2180
2343
|
}
|
|
@@ -2182,9 +2345,6 @@ export class MajikMessage {
|
|
|
2182
2345
|
catch (e) {
|
|
2183
2346
|
console.warn("Error restoring ownAccounts:", e);
|
|
2184
2347
|
}
|
|
2185
|
-
const anyJson = json;
|
|
2186
|
-
if (anyJson.pinHash)
|
|
2187
|
-
instance.pinHash = anyJson.pinHash;
|
|
2188
2348
|
return instance;
|
|
2189
2349
|
}
|
|
2190
2350
|
// ── Persistence ───────────────────────────────────────────────────────────
|
|
@@ -2235,9 +2395,10 @@ export class MajikMessage {
|
|
|
2235
2395
|
if (!saved?.data)
|
|
2236
2396
|
return;
|
|
2237
2397
|
const loaded = await loadSavedMajikFileData(saved.data);
|
|
2398
|
+
// Pass raw parsed object — fromJSON handles migration internally
|
|
2238
2399
|
const restored = await MajikMessage.fromJSON(loaded.j);
|
|
2239
2400
|
this.id = restored.id;
|
|
2240
|
-
this.
|
|
2401
|
+
this.contacts = restored.contacts;
|
|
2241
2402
|
this.envelopeCache = restored.envelopeCache;
|
|
2242
2403
|
this.ownAccounts = restored.ownAccounts;
|
|
2243
2404
|
this.ownAccountsOrder = [...restored.ownAccountsOrder];
|
|
@@ -2273,7 +2434,7 @@ export class MajikMessage {
|
|
|
2273
2434
|
this.ownAccounts.clear();
|
|
2274
2435
|
this.ownAccountsOrder = [];
|
|
2275
2436
|
try {
|
|
2276
|
-
this.
|
|
2437
|
+
this.contacts.clear();
|
|
2277
2438
|
}
|
|
2278
2439
|
catch {
|
|
2279
2440
|
/* ignore */
|
|
@@ -2284,7 +2445,6 @@ export class MajikMessage {
|
|
|
2284
2445
|
catch {
|
|
2285
2446
|
/* ignore */
|
|
2286
2447
|
}
|
|
2287
|
-
this.pinHash = null;
|
|
2288
2448
|
this.id = arrayToBase64(randomBytes(32));
|
|
2289
2449
|
try {
|
|
2290
2450
|
await clearAllBlobs(userProfile);
|