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