@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.
@@ -0,0 +1,27 @@
1
+ import { MajikContactDirectoryData, MajikContactManagerJSON } from "./types";
2
+ export interface MajikMessageRawJSON extends Record<string, unknown> {
3
+ id: string;
4
+ contacts: MajikContactManagerJSON | MajikContactDirectoryData;
5
+ envelopeCache: unknown;
6
+ ownAccounts?: unknown;
7
+ }
8
+ /**
9
+ * Accepts a raw parsed JSON object from IDB/storage and guarantees the
10
+ * returned value always uses the current `MajikContactManagerJSON` shape
11
+ * for its `contacts` field — even if the blob was saved before groups existed.
12
+ *
13
+ * This is the single migration choke-point. Call it once at the top of
14
+ * `MajikMessage.fromJSON()` before touching any field.
15
+ *
16
+ * Migration performed (legacy → current):
17
+ * contacts: { contacts: [...] }
18
+ * →
19
+ * contacts: { contacts: [...], groups: { groups: [] } }
20
+ *
21
+ * The empty `groups` payload means `MajikContactGroupManager.fromJSON()`
22
+ * will bootstrap the two system groups (Favorites, Blocked) with no members,
23
+ * which is exactly correct for a first-time migration.
24
+ */
25
+ export declare function migrateMajikMessageJSON(raw: unknown): MajikMessageRawJSON & {
26
+ contacts: MajikContactManagerJSON;
27
+ };
@@ -0,0 +1,84 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // majik-contact-migration.ts
3
+ //
4
+ // Detects whether a persisted MajikMessage JSON blob was saved before
5
+ // MajikContactManager existed (legacy) or after (current), and returns a
6
+ // normalised MajikMessageJSON that always carries the new shape.
7
+ //
8
+ // Legacy shape (pre-migration):
9
+ // { id, contacts: { contacts: [...] }, envelopeCache, ownAccounts?, pinHash? }
10
+ // ↑ contacts is a MajikContactDirectoryData — no `groups` field
11
+ //
12
+ // Current shape (post-migration):
13
+ // { id, contacts: { contacts: [...], groups: { groups: [...] } }, envelopeCache, ... }
14
+ // ↑ contacts is a MajikContactManagerJSON — always has both `contacts` and `groups`
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ /* -------------------------------
17
+ * Shape-detection helpers
18
+ * ------------------------------- */
19
+ /**
20
+ * Returns true when the blob looks like a legacy save that only has
21
+ * a flat `{ contacts: [...] }` directory payload — no `groups` key.
22
+ */
23
+ function isLegacyContactsShape(raw) {
24
+ if (!raw || typeof raw !== "object")
25
+ return false;
26
+ const obj = raw;
27
+ // Must have a `contacts` field that is an object …
28
+ if (!obj.contacts || typeof obj.contacts !== "object")
29
+ return false;
30
+ const contacts = obj.contacts;
31
+ // … that contains a `contacts` array (the raw serialized contacts) …
32
+ if (!Array.isArray(contacts.contacts))
33
+ return false;
34
+ // … but does NOT yet have a `groups` field — that is the migration trigger.
35
+ return !("groups" in contacts);
36
+ }
37
+ /**
38
+ * Accepts a raw parsed JSON object from IDB/storage and guarantees the
39
+ * returned value always uses the current `MajikContactManagerJSON` shape
40
+ * for its `contacts` field — even if the blob was saved before groups existed.
41
+ *
42
+ * This is the single migration choke-point. Call it once at the top of
43
+ * `MajikMessage.fromJSON()` before touching any field.
44
+ *
45
+ * Migration performed (legacy → current):
46
+ * contacts: { contacts: [...] }
47
+ * →
48
+ * contacts: { contacts: [...], groups: { groups: [] } }
49
+ *
50
+ * The empty `groups` payload means `MajikContactGroupManager.fromJSON()`
51
+ * will bootstrap the two system groups (Favorites, Blocked) with no members,
52
+ * which is exactly correct for a first-time migration.
53
+ */
54
+ export function migrateMajikMessageJSON(raw) {
55
+ if (!raw || typeof raw !== "object") {
56
+ throw new Error("migrateMajikMessageJSON: input must be a non-null object");
57
+ }
58
+ const obj = raw;
59
+ if (!obj.id || typeof obj.id !== "string") {
60
+ throw new Error("migrateMajikMessageJSON: missing required field 'id'");
61
+ }
62
+ // ── Already in current shape — pass through untouched ────────────────────
63
+ if (!isLegacyContactsShape(obj)) {
64
+ // Validate it at least has the expected structure
65
+ const contacts = obj.contacts;
66
+ if (!contacts || !Array.isArray(contacts.contacts) || !contacts.groups) {
67
+ throw new Error("migrateMajikMessageJSON: 'contacts' field has an unrecognised shape — " +
68
+ "expected either a legacy { contacts: [...] } or current { contacts: [...], groups: {...} }");
69
+ }
70
+ return obj;
71
+ }
72
+ // ── Legacy shape — lift it into the current shape ─────────────────────────
73
+ console.info("[MajikMessage] Migrating persisted state from legacy contacts schema → " +
74
+ "MajikContactManager schema. Groups will be initialised empty.");
75
+ const legacyContacts = obj.contacts;
76
+ const migratedContacts = {
77
+ contacts: legacyContacts, // preserve all serialized contacts as-is
78
+ groups: { groups: [] }, // empty groups — system groups are bootstrapped at runtime
79
+ };
80
+ return {
81
+ ...obj,
82
+ contacts: migratedContacts,
83
+ };
84
+ }
@@ -0,0 +1,11 @@
1
+ import { SerializedMajikContact, SerializedMajikContactGroup } from "@majikah/majik-contact";
2
+ export interface MajikContactManagerJSON {
3
+ contacts: MajikContactDirectoryData;
4
+ groups: MajikContactGroupManagerData;
5
+ }
6
+ export interface MajikContactDirectoryData {
7
+ contacts: SerializedMajikContact[];
8
+ }
9
+ export interface MajikContactGroupManagerData {
10
+ groups: SerializedMajikContactGroup[];
11
+ }
@@ -0,0 +1,4 @@
1
+ /* -------------------------------
2
+ * Types
3
+ * ------------------------------- */
4
+ export {};
@@ -1,27 +1,33 @@
1
- import { MajikContact, type MajikContactMeta, type SerializedMajikContact } from "@majikah/majik-contact";
1
+ import { MajikContact, MajikContactGroup, MajikContactGroupMeta, type MajikContactMeta, type SerializedMajikContact } from "@majikah/majik-contact";
2
2
  import { MessageEnvelope } from "./core/messages/message-envelope";
3
3
  import { EnvelopeCache, type EnvelopeCacheItem, type EnvelopeCacheJSON } from "./core/messages/envelope-cache";
4
4
  import { MajikKeyStore } from "./core/crypto/keystore";
5
- import { MajikContactDirectory, type MajikContactDirectoryData } from "./core/contacts/majik-contact-directory";
6
5
  import type { DecryptFileOptions, EncryptFileOptions, EncryptFileResult, MAJIK_API_RESPONSE, MajikMessagePublicKey } from "./core/types";
7
6
  import { MajikMessageChat } from "./core/database/chat/majik-message-chat";
8
7
  import { MajikMessageIdentity } from "./core/database/system/identity";
9
8
  import { MajikKey } from "@majikah/majik-key";
10
9
  import { MajikFile, MajikFileJSON } from "@majikah/majik-file";
11
10
  import { EnvelopeInfo, ExpectedSigner, MajikSignature, SealInfo, SealVerificationResult, SignatoriesFilter, SignatoriesResult, SignatoryInfo, type MajikSignatureJSON, type MajikSignerPublicKeys, type VerificationResult } from "@majikah/majik-signature";
12
- type MajikMessageEvents = "message" | "envelope" | "untrusted" | "error" | "new-account" | "new-contact" | "removed-account" | "removed-contact" | "active-account-change";
11
+ import { MajikContactManager } from "./core/contacts/majik-contact-manager";
12
+ import { MajikContactManagerJSON } from "./core/contacts/types";
13
+ type MajikMessageEvents = "message" | "envelope" | "untrusted" | "error" | "new-account" | "new-contact" | "new-contact-group" | "removed-account" | "removed-contact" | "removed-contact-group" | "contact-group-change" | "active-account-change";
13
14
  interface MajikMessageStatic<T extends MajikMessage> {
14
15
  new (config: MajikMessageConfig, id?: string): T;
15
16
  fromJSON(json: MajikMessageJSON): Promise<T>;
16
17
  }
17
18
  export interface MajikMessageConfig {
18
19
  keyStore?: typeof MajikKeyStore;
19
- contactDirectory?: MajikContactDirectory;
20
+ /**
21
+ * Optional pre-constructed MajikContactManager.
22
+ * When omitted, MajikMessage creates a fresh one internally.
23
+ * Pass one when restoring state from fromJSON() or loadState().
24
+ */
25
+ contactManager?: MajikContactManager;
20
26
  envelopeCache?: EnvelopeCache;
21
27
  }
22
28
  export interface MajikMessageJSON {
23
29
  id: string;
24
- contacts: MajikContactDirectoryData;
30
+ contacts: MajikContactManagerJSON;
25
31
  envelopeCache: EnvelopeCacheJSON;
26
32
  ownAccounts?: {
27
33
  accounts: SerializedMajikContact[];
@@ -31,11 +37,9 @@ export interface MajikMessageJSON {
31
37
  type EventCallback = (...args: any[]) => void;
32
38
  export declare class MajikMessage {
33
39
  private userProfile;
34
- private pinHash?;
35
40
  private id;
36
- private contactDirectory;
41
+ private contacts;
37
42
  private envelopeCache;
38
- private scanner;
39
43
  private listeners;
40
44
  private ownAccounts;
41
45
  private ownAccountsOrder;
@@ -106,8 +110,8 @@ export declare class MajikMessage {
106
110
  hasContact(id: string): boolean;
107
111
  hasContactByPublicKeyBase64(publicKey: MajikMessagePublicKey): Promise<boolean>;
108
112
  getContactByPublicKey(publicKeyBase64: MajikMessagePublicKey): Promise<MajikContact | null>;
109
- exportContactAsJSON(contactId: string): Promise<string | null>;
110
- exportContactAsString(contactId: string): Promise<string | null>;
113
+ exportContactAsJSON(contactID: string): Promise<string | null>;
114
+ exportContactAsString(contactID: string): Promise<string | null>;
111
115
  importContactFromJSON(jsonStr: string): Promise<MAJIK_API_RESPONSE>;
112
116
  importContactFromString(base64Str: string): Promise<MAJIK_API_RESPONSE>;
113
117
  exportContactCompressed(contact: MajikContact): Promise<string>;
@@ -121,6 +125,134 @@ export declare class MajikMessage {
121
125
  isContactMajikahRegistered(id: string): boolean;
122
126
  isContactMajikahIdentityChecked(id: string): boolean;
123
127
  setContactMajikahStatus(id: string, status: boolean): void;
128
+ /**
129
+ * Creates and registers a new user-defined group.
130
+ * Throws if a group with the same ID already exists.
131
+ */
132
+ createGroup(id: string, name: string, meta?: Partial<Omit<MajikContactGroupMeta, "name">>, initialMemberIds?: string[]): this;
133
+ /**
134
+ * Registers an already-constructed MajikContactGroup instance.
135
+ * Throws if a group with the same ID already exists.
136
+ */
137
+ addGroup(group: MajikContactGroup): this;
138
+ /**
139
+ * Removes a user group by ID.
140
+ * System groups (Favorites, Blocked) cannot be deleted.
141
+ */
142
+ removeGroup(id: string): MAJIK_API_RESPONSE;
143
+ /**
144
+ * Returns a group by ID, or undefined if not found.
145
+ */
146
+ getContactGroup(id: string): MajikContactGroup | undefined;
147
+ /**
148
+ * Returns a group by ID. Throws if not found.
149
+ */
150
+ getGroupOrThrow(id: string): MajikContactGroup;
151
+ /**
152
+ * Returns true if a group with the given ID exists.
153
+ */
154
+ hasGroup(id: string): boolean;
155
+ /**
156
+ * Returns all groups.
157
+ *
158
+ * @param includeSystem Include system groups (Favorites, Blocked). Default: true.
159
+ * @param sortedByName Sort results alphabetically by group name. Default: false.
160
+ */
161
+ listContactGroups(includeSystem?: boolean, sortedByName?: boolean): MajikContactGroup[];
162
+ /**
163
+ * Returns only user-created groups (excludes Favorites and Blocked).
164
+ * Sorted alphabetically by name.
165
+ */
166
+ listUserGroups(sortedByName?: boolean): MajikContactGroup[];
167
+ /**
168
+ * Returns only system groups (Favorites and Blocked).
169
+ */
170
+ listSystemGroups(): MajikContactGroup[];
171
+ /**
172
+ * Updates mutable metadata on a group (name, description).
173
+ * Name is locked on system groups — will throw if attempted.
174
+ */
175
+ updateGroupMeta(id: string, meta: Partial<Pick<MajikContactGroupMeta, "name" | "description">>): this;
176
+ /**
177
+ * Adds a contact to a group.
178
+ * Validates the contact exists in the directory.
179
+ * If the group is the system Blocked group, also calls contact.block().
180
+ * Throws if the contact is already a member — use addContactToGroupIfAbsent for idempotent.
181
+ */
182
+ addContactToGroup(groupID: string, contactID: string): this;
183
+ /**
184
+ * Adds multiple contacts to a group in one call (all-or-nothing).
185
+ */
186
+ addContactsToGroup(groupID: string, contactIds: string[]): this;
187
+ /**
188
+ * Removes a contact from a group.
189
+ * If the group is the system Blocked group, also calls contact.unblock().
190
+ * Throws if the contact is not a member — use removeContactFromGroupIfPresent for idempotent.
191
+ */
192
+ removeContactFromGroup(groupID: string, contactID: string): this;
193
+ /**
194
+ * Moves a contact from one group to another atomically.
195
+ * Throws if the contact is not a member of the source group.
196
+ */
197
+ moveContactBetweenGroups(contactID: string, fromGroupId: string, toGroupId: string): this;
198
+ /**
199
+ * Returns all hydrated MajikContact instances in the given group.
200
+ * Contacts removed from the directory since last save are silently skipped.
201
+ */
202
+ getContactsInGroup(groupID: string): MajikContact[];
203
+ /**
204
+ * Returns hydrated contacts in the group, sorted by label (or ID if no label).
205
+ */
206
+ getContactsInGroupSorted(groupID: string): MajikContact[];
207
+ /**
208
+ * Returns true if the contact is a member of the given group.
209
+ */
210
+ isContactInGroup(groupID: string, contactID: string): boolean;
211
+ /**
212
+ * Returns all groups the contact belongs to.
213
+ */
214
+ getGroupsForContact(contactID: string): MajikContactGroup[];
215
+ /**
216
+ * Returns all group IDs the contact belongs to.
217
+ */
218
+ getGroupIdsForContact(contactID: string): string[];
219
+ /**
220
+ * Adds the contact to the Favorites group (idempotent).
221
+ */
222
+ addContactToFavorites(contactID: string): this;
223
+ /**
224
+ * Removes the contact from the Favorites group (idempotent).
225
+ */
226
+ removeContactFromFavorites(contactID: string): this;
227
+ /**
228
+ * Returns true if the contact is in the Favorites group.
229
+ */
230
+ isContactFavorite(contactID: string): boolean;
231
+ /**
232
+ * Returns true if the contact is in the Blocked group.
233
+ */
234
+ isContactBlocked(contactID: string): boolean;
235
+ /**
236
+ * Returns the Favorites system group instance.
237
+ */
238
+ getFavoritesGroup(): MajikContactGroup;
239
+ /**
240
+ * Returns the Blocked system group instance.
241
+ */
242
+ getBlockedGroup(): MajikContactGroup;
243
+ /**
244
+ * Returns all contacts in the Favorites group as hydrated MajikContact instances.
245
+ */
246
+ getFavoriteContacts(): MajikContact[];
247
+ /**
248
+ * Returns all contacts in the Blocked group as hydrated MajikContact instances.
249
+ */
250
+ getBlockedContacts(): MajikContact[];
251
+ /**
252
+ * Clears both the directory and all group memberships.
253
+ * System groups are preserved (re-bootstrapped by the group manager).
254
+ */
255
+ clearDirectory(): this;
124
256
  /**
125
257
  * Compose and encrypt a message for one or more recipients.
126
258
  * Single recipient → solo envelope. Two or more → group envelope.
@@ -209,7 +341,7 @@ export declare class MajikMessage {
209
341
  * metadata: row,
210
342
  * });
211
343
  * if (signature) {
212
- * const result = await majik.verifyMajikFile(file, { contactId: row.user_id });
344
+ * const result = await majik.verifyMajikFile(file, { contactID: row.user_id });
213
345
  * }
214
346
  * ```
215
347
  */
@@ -249,7 +381,7 @@ export declare class MajikMessage {
249
381
  * if the instance was restored from a metadata-only Supabase row.
250
382
  *
251
383
  * Signer resolution:
252
- * - contactId: looked up in the contact directory (own accounts included)
384
+ * - contactID: looked up in the contact directory (own accounts included)
253
385
  * - publicKeyBase64: looked up via contact directory
254
386
  * - key: used directly (skips directory lookup)
255
387
  * - none provided: falls back to public keys embedded in the signature
@@ -260,12 +392,12 @@ export declare class MajikMessage {
260
392
  * @example — verify against the file's owner contact
261
393
  * file.attachBinary(await r2.get(row.r2_key).arrayBuffer());
262
394
  * const result = await majik.verifyMajikFile(file, {
263
- * contactId: ownerContactId,
395
+ * contactID: ownerContactId,
264
396
  * });
265
397
  * if (result?.valid) console.log("Verified, signed by", result.signerId);
266
398
  */
267
399
  verifyMajikFile(file: MajikFile, options?: {
268
- contactId?: string;
400
+ contactID?: string;
269
401
  publicKeyBase64?: string;
270
402
  key?: MajikKey;
271
403
  }): Promise<VerificationResult | null>;
@@ -285,12 +417,12 @@ export declare class MajikMessage {
285
417
  *
286
418
  * @example
287
419
  * const result = await majik.verifyMajikFileBinary(file, {
288
- * contactId: "contact_abc",
420
+ * contactID: "contact_abc",
289
421
  * });
290
422
  * if (result.valid) console.log("Plaintext verified");
291
423
  */
292
424
  verifyMajikFileBinary(file: MajikFile, options?: {
293
- contactId?: string;
425
+ contactID?: string;
294
426
  publicKeyBase64?: string;
295
427
  key?: MajikKey;
296
428
  decryptAccountId?: string;
@@ -455,12 +587,12 @@ export declare class MajikMessage {
455
587
  *
456
588
  * @example
457
589
  * const result = await majik.verifyText("Hello world", sig, {
458
- * contactId: "contact_abc",
590
+ * contactID: "contact_abc",
459
591
  * });
460
592
  * if (result.valid) console.log("Authentic");
461
593
  */
462
594
  verifyText(text: string, signature: MajikSignature | MajikSignatureJSON | string, options?: {
463
- contactId?: string;
595
+ contactID?: string;
464
596
  publicKeyBase64?: string;
465
597
  key?: MajikKey;
466
598
  expectedSignerId?: string;
@@ -479,12 +611,12 @@ export declare class MajikMessage {
479
611
  * @example
480
612
  * const row = await db.findOne({ doc_id });
481
613
  * const result = await majik.verifyDetached(docBytes, row.signature, {
482
- * contactId: row.signer_contact_id,
614
+ * contactID: row.signer_contact_id,
483
615
  * });
484
616
  * if (result.valid) console.log("Signed by", result.signerId);
485
617
  */
486
618
  verifyDetached(content: Uint8Array | string, serializedSignature: string, options?: {
487
- contactId?: string;
619
+ contactID?: string;
488
620
  publicKeyBase64?: string;
489
621
  key?: MajikKey;
490
622
  expectedSignerId?: string;
@@ -555,9 +687,6 @@ export declare class MajikMessage {
555
687
  raw: Uint8Array;
556
688
  }>;
557
689
  isPassphraseValid(passphrase: string, id?: string): Promise<boolean>;
558
- scanDOM(rootNode: Node): void;
559
- startDOMObserver(rootNode: Node): void;
560
- stopDOMObserver(): void;
561
690
  on(event: MajikMessageEvents, callback: EventCallback): void;
562
691
  off(event: MajikMessageEvents, callback?: EventCallback): void;
563
692
  private emit;
@@ -656,7 +785,7 @@ export declare class MajikMessage {
656
785
  * > against a known contact fingerprint before trusting the result.
657
786
  *
658
787
  * @example — verify against a known contact
659
- * const result = await majik.verifyContent(docBytes, sig, { contactId: "contact_abc" });
788
+ * const result = await majik.verifyContent(docBytes, sig, { contactID: "contact_abc" });
660
789
  * if (result.valid) console.log("Authentic, signed by:", result.signerId);
661
790
  *
662
791
  * @example — verify using embedded keys (self-reported)
@@ -664,7 +793,7 @@ export declare class MajikMessage {
664
793
  * // always check result.signerId matches a known fingerprint
665
794
  */
666
795
  verifyContent(content: Uint8Array | string, signature: MajikSignature | MajikSignatureJSON, options?: {
667
- contactId?: string;
796
+ contactID?: string;
668
797
  publicKeyBase64?: string;
669
798
  key?: MajikKey;
670
799
  expectedSignerId?: string;
@@ -681,16 +810,16 @@ export declare class MajikMessage {
681
810
  * envelope are used (self-reported — see security note on verifyContent).
682
811
  *
683
812
  * @example — verify a signed PDF against a known contact
684
- * const result = await majik.verifyFile(signedPdf, { contactId: "contact_abc" });
813
+ * const result = await majik.verifyFile(signedPdf, { contactID: "contact_abc" });
685
814
  * if (result.valid) console.log("Verified:", result.signerId, result.timestamp);
686
815
  *
687
816
  * @example — check own signed file using active account
688
817
  * const result = await majik.verifyFile(signedWav, {
689
- * contactId: majik.getActiveAccount()?.id,
818
+ * contactID: majik.getActiveAccount()?.id,
690
819
  * });
691
820
  */
692
821
  verifyFile(file: Blob, options?: {
693
- contactId?: string;
822
+ contactID?: string;
694
823
  publicKeyBase64?: string;
695
824
  key?: MajikKey;
696
825
  expectedSignerId?: string;
@@ -709,7 +838,7 @@ export declare class MajikMessage {
709
838
  * @example
710
839
  * const results = await majik.batchVerifyFiles(
711
840
  * [pdfBlob, wavBlob, mp4Blob],
712
- * { contactId: "contact_abc" },
841
+ * { contactID: "contact_abc" },
713
842
  * );
714
843
  * const allValid = results.every(r => r.valid);
715
844
  */
@@ -718,7 +847,7 @@ export declare class MajikMessage {
718
847
  mimeType?: string;
719
848
  expectedSignerId?: string;
720
849
  }>, options?: {
721
- contactId?: string;
850
+ contactID?: string;
722
851
  publicKeyBase64?: string;
723
852
  key?: MajikKey;
724
853
  expectedSignerId?: string;
@@ -758,7 +887,7 @@ export declare class MajikMessage {
758
887
  *
759
888
  * @example
760
889
  * if (await majik.isFileSigned(file)) {
761
- * const result = await majik.verifyFile(file, { contactId });
890
+ * const result = await majik.verifyFile(file, { contactID });
762
891
  * }
763
892
  */
764
893
  isFileSigned(file: Blob, options?: {
@@ -995,11 +1124,6 @@ export declare class MajikMessage {
995
1124
  * throughout MajikMessage — consistent account/contact resolution in one place.
996
1125
  */
997
1126
  private _resolveSignerPublicKeys;
998
- setPIN(pin: string): Promise<void>;
999
- clearPIN(): Promise<void>;
1000
- isValidPIN(pin: string): Promise<boolean>;
1001
- getPinHash(): string | null;
1002
- private static _hashPIN;
1003
1127
  toJSON(): Promise<MajikMessageJSON>;
1004
1128
  static fromJSON<T extends MajikMessage>(this: new (config: MajikMessageConfig, id?: string) => T, json: MajikMessageJSON): Promise<T>;
1005
1129
  private attachAutosaveHandlers;