@majikah/majik-message 0.2.20 → 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 +162 -37
- package/dist/majik-message.js +292 -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,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,20 +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);
|
|
273
|
+
}
|
|
274
|
+
async hasContactByPublicKeyBase64(publicKey) {
|
|
275
|
+
if (!publicKey?.trim())
|
|
276
|
+
throw new Error("Invalid contact public key");
|
|
277
|
+
return await this.contacts.hasContactByPublicKeyBase64(publicKey);
|
|
307
278
|
}
|
|
308
279
|
async getContactByPublicKey(publicKeyBase64) {
|
|
309
280
|
if (!publicKeyBase64?.trim())
|
|
310
281
|
throw new Error("Invalid public key");
|
|
311
|
-
return ((await this.
|
|
282
|
+
return ((await this.contacts.getContactByPublicKeyBase64(publicKeyBase64)) ?? null);
|
|
312
283
|
}
|
|
313
|
-
async exportContactAsJSON(
|
|
314
|
-
const contact = this.
|
|
284
|
+
async exportContactAsJSON(contactID) {
|
|
285
|
+
const contact = this.contacts.getContact(contactID);
|
|
315
286
|
if (!contact)
|
|
316
287
|
return null;
|
|
317
288
|
let publicKeyBase64;
|
|
@@ -333,8 +304,8 @@ export class MajikMessage {
|
|
|
333
304
|
mlDsaPublicKeyBase64: contact.mlDsaPublicKeyBase64,
|
|
334
305
|
}, null, 2);
|
|
335
306
|
}
|
|
336
|
-
async exportContactAsString(
|
|
337
|
-
const contact = this.
|
|
307
|
+
async exportContactAsString(contactID) {
|
|
308
|
+
const contact = this.contacts.getContact(contactID);
|
|
338
309
|
if (!contact)
|
|
339
310
|
return null;
|
|
340
311
|
const compressedString = this.exportContactCompressed(contact);
|
|
@@ -445,45 +416,276 @@ export class MajikMessage {
|
|
|
445
416
|
!contact?.mlKey) {
|
|
446
417
|
throw new Error("Invalid contact JSON");
|
|
447
418
|
}
|
|
448
|
-
this.
|
|
419
|
+
this.contacts.addContact(contact);
|
|
449
420
|
this.emit("new-contact", contact);
|
|
450
421
|
this.scheduleAutosave();
|
|
451
422
|
}
|
|
452
423
|
removeContact(id) {
|
|
453
|
-
const result = this.
|
|
424
|
+
const result = this.contacts.removeContact(id);
|
|
454
425
|
if (!result.success)
|
|
455
426
|
throw new Error(result.message);
|
|
456
427
|
this.emit("removed-contact", id);
|
|
457
428
|
this.scheduleAutosave();
|
|
458
429
|
}
|
|
459
430
|
updateContactMeta(id, meta) {
|
|
460
|
-
this.
|
|
431
|
+
this.contacts.updateContactMeta(id, meta);
|
|
461
432
|
this.scheduleAutosave();
|
|
462
433
|
}
|
|
463
434
|
blockContact(id) {
|
|
464
|
-
this.
|
|
435
|
+
this.contacts.blockContact(id);
|
|
465
436
|
this.scheduleAutosave();
|
|
466
437
|
}
|
|
467
438
|
unblockContact(id) {
|
|
468
|
-
this.
|
|
439
|
+
this.contacts.unblockContact(id);
|
|
469
440
|
this.scheduleAutosave();
|
|
470
441
|
}
|
|
471
442
|
listContacts(all = true, majikahOnly = false) {
|
|
472
|
-
const contacts = this.
|
|
443
|
+
const contacts = this.contacts.listContacts(true, majikahOnly);
|
|
473
444
|
if (all)
|
|
474
445
|
return contacts;
|
|
475
446
|
const ownIds = new Set(this.listOwnAccounts(majikahOnly).map((a) => a.id));
|
|
476
447
|
return contacts.filter((c) => !ownIds.has(c.id));
|
|
477
448
|
}
|
|
478
449
|
isContactMajikahRegistered(id) {
|
|
479
|
-
return this.
|
|
450
|
+
return this.contacts.isMajikahRegistered(id);
|
|
480
451
|
}
|
|
481
452
|
isContactMajikahIdentityChecked(id) {
|
|
482
|
-
return this.
|
|
453
|
+
return this.contacts.isMajikahIdentityChecked(id);
|
|
483
454
|
}
|
|
484
455
|
setContactMajikahStatus(id, status) {
|
|
485
|
-
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);
|
|
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();
|
|
486
687
|
this.scheduleAutosave();
|
|
688
|
+
return this;
|
|
487
689
|
}
|
|
488
690
|
// ── Encryption / Decryption ───────────────────────────────────────────────
|
|
489
691
|
/**
|
|
@@ -759,7 +961,7 @@ export class MajikMessage {
|
|
|
759
961
|
* metadata: row,
|
|
760
962
|
* });
|
|
761
963
|
* if (signature) {
|
|
762
|
-
* const result = await majik.verifyMajikFile(file, {
|
|
964
|
+
* const result = await majik.verifyMajikFile(file, { contactID: row.user_id });
|
|
763
965
|
* }
|
|
764
966
|
* ```
|
|
765
967
|
*/
|
|
@@ -852,7 +1054,7 @@ export class MajikMessage {
|
|
|
852
1054
|
* if the instance was restored from a metadata-only Supabase row.
|
|
853
1055
|
*
|
|
854
1056
|
* Signer resolution:
|
|
855
|
-
* -
|
|
1057
|
+
* - contactID: looked up in the contact directory (own accounts included)
|
|
856
1058
|
* - publicKeyBase64: looked up via contact directory
|
|
857
1059
|
* - key: used directly (skips directory lookup)
|
|
858
1060
|
* - none provided: falls back to public keys embedded in the signature
|
|
@@ -863,7 +1065,7 @@ export class MajikMessage {
|
|
|
863
1065
|
* @example — verify against the file's owner contact
|
|
864
1066
|
* file.attachBinary(await r2.get(row.r2_key).arrayBuffer());
|
|
865
1067
|
* const result = await majik.verifyMajikFile(file, {
|
|
866
|
-
*
|
|
1068
|
+
* contactID: ownerContactId,
|
|
867
1069
|
* });
|
|
868
1070
|
* if (result?.valid) console.log("Verified, signed by", result.signerId);
|
|
869
1071
|
*/
|
|
@@ -903,7 +1105,7 @@ export class MajikMessage {
|
|
|
903
1105
|
*
|
|
904
1106
|
* @example
|
|
905
1107
|
* const result = await majik.verifyMajikFileBinary(file, {
|
|
906
|
-
*
|
|
1108
|
+
* contactID: "contact_abc",
|
|
907
1109
|
* });
|
|
908
1110
|
* if (result.valid) console.log("Plaintext verified");
|
|
909
1111
|
*/
|
|
@@ -1113,7 +1315,7 @@ export class MajikMessage {
|
|
|
1113
1315
|
*
|
|
1114
1316
|
* @example
|
|
1115
1317
|
* const result = await majik.verifyText("Hello world", sig, {
|
|
1116
|
-
*
|
|
1318
|
+
* contactID: "contact_abc",
|
|
1117
1319
|
* });
|
|
1118
1320
|
* if (result.valid) console.log("Authentic");
|
|
1119
1321
|
*/
|
|
@@ -1139,7 +1341,7 @@ export class MajikMessage {
|
|
|
1139
1341
|
* @example
|
|
1140
1342
|
* const row = await db.findOne({ doc_id });
|
|
1141
1343
|
* const result = await majik.verifyDetached(docBytes, row.signature, {
|
|
1142
|
-
*
|
|
1344
|
+
* contactID: row.signer_contact_id,
|
|
1143
1345
|
* });
|
|
1144
1346
|
* if (result.valid) console.log("Signed by", result.signerId);
|
|
1145
1347
|
*/
|
|
@@ -1273,16 +1475,6 @@ export class MajikMessage {
|
|
|
1273
1475
|
return false;
|
|
1274
1476
|
return MajikKeyStore.isPassphraseValid(target.id, passphrase);
|
|
1275
1477
|
}
|
|
1276
|
-
// ── DOM Scanning ──────────────────────────────────────────────────────────
|
|
1277
|
-
scanDOM(rootNode) {
|
|
1278
|
-
this.scanner.scanDOM(rootNode);
|
|
1279
|
-
}
|
|
1280
|
-
startDOMObserver(rootNode) {
|
|
1281
|
-
this.scanner.startDOMObserver(rootNode);
|
|
1282
|
-
}
|
|
1283
|
-
stopDOMObserver() {
|
|
1284
|
-
this.scanner.stopDOMObserver();
|
|
1285
|
-
}
|
|
1286
1478
|
// ── Events ────────────────────────────────────────────────────────────────
|
|
1287
1479
|
on(event, callback) {
|
|
1288
1480
|
this.listeners.get(event)?.push(callback);
|
|
@@ -1504,7 +1696,7 @@ export class MajikMessage {
|
|
|
1504
1696
|
* > against a known contact fingerprint before trusting the result.
|
|
1505
1697
|
*
|
|
1506
1698
|
* @example — verify against a known contact
|
|
1507
|
-
* const result = await majik.verifyContent(docBytes, sig, {
|
|
1699
|
+
* const result = await majik.verifyContent(docBytes, sig, { contactID: "contact_abc" });
|
|
1508
1700
|
* if (result.valid) console.log("Authentic, signed by:", result.signerId);
|
|
1509
1701
|
*
|
|
1510
1702
|
* @example — verify using embedded keys (self-reported)
|
|
@@ -1540,12 +1732,12 @@ export class MajikMessage {
|
|
|
1540
1732
|
* envelope are used (self-reported — see security note on verifyContent).
|
|
1541
1733
|
*
|
|
1542
1734
|
* @example — verify a signed PDF against a known contact
|
|
1543
|
-
* const result = await majik.verifyFile(signedPdf, {
|
|
1735
|
+
* const result = await majik.verifyFile(signedPdf, { contactID: "contact_abc" });
|
|
1544
1736
|
* if (result.valid) console.log("Verified:", result.signerId, result.timestamp);
|
|
1545
1737
|
*
|
|
1546
1738
|
* @example — check own signed file using active account
|
|
1547
1739
|
* const result = await majik.verifyFile(signedWav, {
|
|
1548
|
-
*
|
|
1740
|
+
* contactID: majik.getActiveAccount()?.id,
|
|
1549
1741
|
* });
|
|
1550
1742
|
*/
|
|
1551
1743
|
async verifyFile(file, options) {
|
|
@@ -1559,7 +1751,7 @@ export class MajikMessage {
|
|
|
1559
1751
|
return results[0];
|
|
1560
1752
|
}
|
|
1561
1753
|
// No signer provided — extract and use self-reported keys from first signature.
|
|
1562
|
-
// For full multi-sig verification, pass a
|
|
1754
|
+
// For full multi-sig verification, pass a contactID or publicKeyBase64.
|
|
1563
1755
|
const extracted = await MajikSignature.extractFrom(file, {
|
|
1564
1756
|
mimeType: options?.mimeType,
|
|
1565
1757
|
});
|
|
@@ -1594,7 +1786,7 @@ export class MajikMessage {
|
|
|
1594
1786
|
* @example
|
|
1595
1787
|
* const results = await majik.batchVerifyFiles(
|
|
1596
1788
|
* [pdfBlob, wavBlob, mp4Blob],
|
|
1597
|
-
* {
|
|
1789
|
+
* { contactID: "contact_abc" },
|
|
1598
1790
|
* );
|
|
1599
1791
|
* const allValid = results.every(r => r.valid);
|
|
1600
1792
|
*/
|
|
@@ -1706,7 +1898,7 @@ export class MajikMessage {
|
|
|
1706
1898
|
*
|
|
1707
1899
|
* @example
|
|
1708
1900
|
* if (await majik.isFileSigned(file)) {
|
|
1709
|
-
* const result = await majik.verifyFile(file, {
|
|
1901
|
+
* const result = await majik.verifyFile(file, { contactID });
|
|
1710
1902
|
* }
|
|
1711
1903
|
*/
|
|
1712
1904
|
async isFileSigned(file, options) {
|
|
@@ -2050,22 +2242,22 @@ export class MajikMessage {
|
|
|
2050
2242
|
return MajikSignature.publicKeysFromMajikKey(options.key);
|
|
2051
2243
|
}
|
|
2052
2244
|
// Option B: contact ID looked up from the contact directory
|
|
2053
|
-
if (options.
|
|
2054
|
-
const contact = this.
|
|
2245
|
+
if (options.contactID) {
|
|
2246
|
+
const contact = this.contacts.getContact(options.contactID);
|
|
2055
2247
|
if (!contact) {
|
|
2056
|
-
throw new Error(`No contact found for id "${options.
|
|
2248
|
+
throw new Error(`No contact found for id "${options.contactID}"`);
|
|
2057
2249
|
}
|
|
2058
2250
|
// Own accounts are in the keystore — get their signing keys directly
|
|
2059
|
-
const ownAccount = this.getOwnAccountById(options.
|
|
2251
|
+
const ownAccount = this.getOwnAccountById(options.contactID);
|
|
2060
2252
|
if (ownAccount) {
|
|
2061
|
-
const key = MajikKeyStore.get(options.
|
|
2253
|
+
const key = MajikKeyStore.get(options.contactID);
|
|
2062
2254
|
if (key?.hasSigningKeys) {
|
|
2063
2255
|
return MajikSignature.publicKeysFromMajikKey(key);
|
|
2064
2256
|
}
|
|
2065
2257
|
}
|
|
2066
2258
|
// External contact — resolve from their contact card fields
|
|
2067
2259
|
if (!contact.edPublicKeyBase64 || !contact.mlDsaPublicKeyBase64) {
|
|
2068
|
-
throw new Error(`Contact "${options.
|
|
2260
|
+
throw new Error(`Contact "${options.contactID}" has no signing public keys. ` +
|
|
2069
2261
|
`They may need to share an updated contact card.`);
|
|
2070
2262
|
}
|
|
2071
2263
|
return {
|
|
@@ -2076,7 +2268,7 @@ export class MajikMessage {
|
|
|
2076
2268
|
}
|
|
2077
2269
|
// Option C: raw base64 public key — look up via contact directory
|
|
2078
2270
|
if (options.publicKeyBase64) {
|
|
2079
|
-
const contact = await this.
|
|
2271
|
+
const contact = await this.contacts.getContactByPublicKeyBase64(options.publicKeyBase64);
|
|
2080
2272
|
if (!contact) {
|
|
2081
2273
|
throw new Error(`No contact found for public key "${options.publicKeyBase64}"`);
|
|
2082
2274
|
}
|
|
@@ -2091,34 +2283,11 @@ export class MajikMessage {
|
|
|
2091
2283
|
}
|
|
2092
2284
|
return null;
|
|
2093
2285
|
}
|
|
2094
|
-
// ── PIN ───────────────────────────────────────────────────────────────────
|
|
2095
|
-
async setPIN(pin) {
|
|
2096
|
-
if (!pin)
|
|
2097
|
-
throw new Error("PIN must be a non-empty string");
|
|
2098
|
-
this.pinHash = await MajikMessage._hashPIN(pin);
|
|
2099
|
-
this.scheduleAutosave();
|
|
2100
|
-
}
|
|
2101
|
-
async clearPIN() {
|
|
2102
|
-
this.pinHash = null;
|
|
2103
|
-
this.scheduleAutosave();
|
|
2104
|
-
}
|
|
2105
|
-
async isValidPIN(pin) {
|
|
2106
|
-
if (!this.pinHash)
|
|
2107
|
-
return true;
|
|
2108
|
-
return (await MajikMessage._hashPIN(pin)) === this.pinHash;
|
|
2109
|
-
}
|
|
2110
|
-
getPinHash() {
|
|
2111
|
-
return this.pinHash ?? null;
|
|
2112
|
-
}
|
|
2113
|
-
static async _hashPIN(pin) {
|
|
2114
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pin));
|
|
2115
|
-
return arrayBufferToBase64(digest);
|
|
2116
|
-
}
|
|
2117
2286
|
// ── Serialization ─────────────────────────────────────────────────────────
|
|
2118
2287
|
async toJSON() {
|
|
2119
2288
|
const json = {
|
|
2120
2289
|
id: this.id,
|
|
2121
|
-
contacts: await this.
|
|
2290
|
+
contacts: await this.contacts.toJSON(),
|
|
2122
2291
|
envelopeCache: this.envelopeCache.toJSON(),
|
|
2123
2292
|
};
|
|
2124
2293
|
try {
|
|
@@ -2133,14 +2302,14 @@ export class MajikMessage {
|
|
|
2133
2302
|
catch (e) {
|
|
2134
2303
|
console.warn("Failed to serialize ownAccounts:", e);
|
|
2135
2304
|
}
|
|
2136
|
-
json.pinHash = this.pinHash ?? null;
|
|
2137
2305
|
return json;
|
|
2138
2306
|
}
|
|
2139
2307
|
static async fromJSON(json) {
|
|
2140
|
-
const
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
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);
|
|
2144
2313
|
try {
|
|
2145
2314
|
if (json.ownAccounts && Array.isArray(json.ownAccounts.accounts)) {
|
|
2146
2315
|
for (const acct of json.ownAccounts.accounts) {
|
|
@@ -2157,19 +2326,19 @@ export class MajikMessage {
|
|
|
2157
2326
|
if (Array.isArray(json.ownAccounts.order)) {
|
|
2158
2327
|
instance.ownAccountsOrder = [...json.ownAccounts.order];
|
|
2159
2328
|
}
|
|
2160
|
-
// Fallback: populate from
|
|
2329
|
+
// Fallback: populate from contacts if accounts array failed
|
|
2161
2330
|
if (instance.ownAccounts.size === 0) {
|
|
2162
2331
|
for (const id of instance.ownAccountsOrder) {
|
|
2163
|
-
const c = instance.
|
|
2332
|
+
const c = instance.contacts.getContact(id);
|
|
2164
2333
|
if (c)
|
|
2165
2334
|
instance.ownAccounts.set(id, c);
|
|
2166
2335
|
}
|
|
2167
2336
|
}
|
|
2168
|
-
// Ensure own accounts are in
|
|
2337
|
+
// Ensure own accounts are in contacts
|
|
2169
2338
|
instance.ownAccountsOrder.forEach((id) => {
|
|
2170
2339
|
const c = instance.ownAccounts.get(id);
|
|
2171
|
-
if (c && !instance.
|
|
2172
|
-
instance.
|
|
2340
|
+
if (c && !instance.contacts.hasContact(c.id)) {
|
|
2341
|
+
instance.contacts.addContact(c);
|
|
2173
2342
|
}
|
|
2174
2343
|
});
|
|
2175
2344
|
}
|
|
@@ -2177,9 +2346,6 @@ export class MajikMessage {
|
|
|
2177
2346
|
catch (e) {
|
|
2178
2347
|
console.warn("Error restoring ownAccounts:", e);
|
|
2179
2348
|
}
|
|
2180
|
-
const anyJson = json;
|
|
2181
|
-
if (anyJson.pinHash)
|
|
2182
|
-
instance.pinHash = anyJson.pinHash;
|
|
2183
2349
|
return instance;
|
|
2184
2350
|
}
|
|
2185
2351
|
// ── Persistence ───────────────────────────────────────────────────────────
|
|
@@ -2232,7 +2398,7 @@ export class MajikMessage {
|
|
|
2232
2398
|
const loaded = await loadSavedMajikFileData(saved.data);
|
|
2233
2399
|
const restored = await MajikMessage.fromJSON(loaded.j);
|
|
2234
2400
|
this.id = restored.id;
|
|
2235
|
-
this.
|
|
2401
|
+
this.contacts = restored.contacts;
|
|
2236
2402
|
this.envelopeCache = restored.envelopeCache;
|
|
2237
2403
|
this.ownAccounts = restored.ownAccounts;
|
|
2238
2404
|
this.ownAccountsOrder = [...restored.ownAccountsOrder];
|
|
@@ -2268,7 +2434,7 @@ export class MajikMessage {
|
|
|
2268
2434
|
this.ownAccounts.clear();
|
|
2269
2435
|
this.ownAccountsOrder = [];
|
|
2270
2436
|
try {
|
|
2271
|
-
this.
|
|
2437
|
+
this.contacts.clear();
|
|
2272
2438
|
}
|
|
2273
2439
|
catch {
|
|
2274
2440
|
/* ignore */
|
|
@@ -2279,7 +2445,6 @@ export class MajikMessage {
|
|
|
2279
2445
|
catch {
|
|
2280
2446
|
/* ignore */
|
|
2281
2447
|
}
|
|
2282
|
-
this.pinHash = null;
|
|
2283
2448
|
this.id = arrayToBase64(randomBytes(32));
|
|
2284
2449
|
try {
|
|
2285
2450
|
await clearAllBlobs(userProfile);
|