@thezelijah/majik-message 1.0.13 → 1.0.15

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.
@@ -18,6 +18,11 @@ export declare class MajikContactDirectory {
18
18
  updateContactMeta(id: string, meta: Partial<MajikContactData["meta"]>): MajikContact;
19
19
  getContact(id: string): MajikContact | undefined;
20
20
  getContactByFingerprint(fingerprint: string): MajikContact | undefined;
21
+ /**
22
+ * Get contact by public key (base64)
23
+ * Uses MajikContact.getPublicKeyBase64() for canonical comparison
24
+ */
25
+ getContactByPublicKeyBase64(publicKeyBase64: string): Promise<MajikContact | undefined>;
21
26
  hasFingerprint(fingerprint: string): boolean;
22
27
  listContacts(sortedByLabel?: boolean): MajikContact[];
23
28
  blockContact(id: string): MajikContact;
@@ -81,6 +81,22 @@ export class MajikContactDirectory {
81
81
  const contactId = this.fingerprintMap.get(fingerprint);
82
82
  return contactId ? this.contacts.get(contactId) : undefined;
83
83
  }
84
+ /**
85
+ * Get contact by public key (base64)
86
+ * Uses MajikContact.getPublicKeyBase64() for canonical comparison
87
+ */
88
+ async getContactByPublicKeyBase64(publicKeyBase64) {
89
+ if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") {
90
+ throw new MajikContactDirectoryError("Public key must be a non-empty base64 string");
91
+ }
92
+ for (const contact of this.contacts.values()) {
93
+ const contactKey = await contact.getPublicKeyBase64();
94
+ if (contactKey === publicKeyBase64) {
95
+ return contact;
96
+ }
97
+ }
98
+ return undefined;
99
+ }
84
100
  hasFingerprint(fingerprint) {
85
101
  return this.fingerprintMap.has(fingerprint);
86
102
  }
@@ -1,6 +1,6 @@
1
1
  import { MajikMessageAccountID, MajikMessageChatID, MajikMessagePublicKey } from "../../types";
2
2
  import { MajikMessageIdentity } from "../system/identity";
3
- import { MajikMessageChatJSON } from "./types";
3
+ import { MajikMessageChatJSON, RedisKey } from "./types";
4
4
  /**
5
5
  * Represents a temporary, compressed message with automatic expiration.
6
6
  * Messages are automatically compressed on creation and stored in Redis by default.
@@ -88,8 +88,13 @@ export declare class MajikMessageChat {
88
88
  getUnreadRecipients(): string[];
89
89
  toJSON(): MajikMessageChatJSON;
90
90
  static fromJSON(json: string | MajikMessageChatJSON): MajikMessageChat;
91
- getRedisKey(): string;
91
+ getRedisKey(): RedisKey;
92
+ getRedisMessageKey(): RedisKey;
93
+ getRedisConversationIndexKey(): RedisKey;
94
+ getRedisInboxIndexKey(publicKey: MajikMessagePublicKey): RedisKey;
95
+ getTTLUnixTimestamp(): number;
92
96
  getTTLSeconds(): number;
97
+ toRedisPayload(): string;
93
98
  clone(): MajikMessageChat;
94
99
  /**
95
100
  * Validates raw message length before compression.
@@ -365,15 +365,29 @@ export class MajikMessageChat {
365
365
  return new MajikMessageChat(rawParse.id, rawParse.account, rawParse.message, rawParse.sender, rawParse.recipients || [], rawParse.timestamp, rawParse.expires_at, rawParse.read_by || [], rawParse?.conversation_id);
366
366
  }
367
367
  // ============= REDIS METHODS =============
368
- // Generate Redis key
369
368
  getRedisKey() {
370
- return `majik_message:${this.id}`;
369
+ return `majik_message:${this.conversation_id}:${this.id}`;
371
370
  }
372
- // Get TTL in seconds for Redis EXPIREAT
373
- getTTLSeconds() {
371
+ // Generate Redis key
372
+ getRedisMessageKey() {
373
+ return `msg:${this.id}`;
374
+ }
375
+ getRedisConversationIndexKey() {
376
+ return `conv:${this.conversation_id}:msgs`;
377
+ }
378
+ getRedisInboxIndexKey(publicKey) {
379
+ return `inbox:${publicKey}`;
380
+ }
381
+ getTTLUnixTimestamp() {
374
382
  const expiresAt = new Date(this.expires_at);
375
383
  return Math.floor(expiresAt.getTime() / 1000);
376
384
  }
385
+ getTTLSeconds() {
386
+ return Math.max(0, Math.floor((new Date(this.expires_at).getTime() - Date.now()) / 1000));
387
+ }
388
+ toRedisPayload() {
389
+ return JSON.stringify(this.toJSON());
390
+ }
377
391
  // Clone method for updates
378
392
  clone() {
379
393
  return MajikMessageChat.fromJSON(this.toJSON());
@@ -10,3 +10,4 @@ export interface MajikMessageChatJSON {
10
10
  expires_at: string;
11
11
  read_by: string[];
12
12
  }
13
+ export type RedisKey = string;
@@ -1,5 +1,5 @@
1
- import type { UserBasicInformation, FullName, Address, UserSettings, MajikUserJSON, SupabaseUser, YYYYMMDD } from './types';
2
- import type { UserGenderOptions } from './enums';
1
+ import type { UserBasicInformation, FullName, Address, UserSettings, MajikUserJSON, SupabaseUser, YYYYMMDD } from "./types";
2
+ import type { UserGenderOptions } from "./enums";
3
3
  export interface MajikUserData<TMetadata extends UserBasicInformation = UserBasicInformation> {
4
4
  id: string;
5
5
  email: string;
@@ -74,6 +74,10 @@ export declare class MajikUser<TMetadata extends UserBasicInformation = UserBasi
74
74
  * Get user's first name if available
75
75
  */
76
76
  get birthday(): YYYYMMDD | null;
77
+ /**
78
+ * Get user's full address if available
79
+ */
80
+ get address(): string | null;
77
81
  /**
78
82
  * Check if email is verified
79
83
  */
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { arrayBufferToBase64, arrayToBase64, dateToYYYYMMDD, stripUndefined } from './utils';
3
- import { generateMnemonic, mnemonicToSeedSync } from '@scure/bip39';
4
- import * as ed25519 from '@stablelib/ed25519';
5
- import ed2curve from 'ed2curve';
6
- import { hash } from '@stablelib/sha256';
7
- import { wordlist } from '@scure/bip39/wordlists/english';
2
+ import { arrayBufferToBase64, arrayToBase64, dateToYYYYMMDD, stripUndefined, } from "./utils";
3
+ import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
4
+ import * as ed25519 from "@stablelib/ed25519";
5
+ import ed2curve from "ed2curve";
6
+ import { hash } from "@stablelib/sha256";
7
+ import { wordlist } from "@scure/bip39/wordlists/english";
8
8
  /**
9
9
  * Base user class for database persistence
10
10
  * Designed to be extended by subclasses with additional metadata
@@ -35,10 +35,10 @@ export class MajikUser {
35
35
  */
36
36
  static initialize(email, displayName, id) {
37
37
  if (!email) {
38
- throw new Error('Email cannot be empty');
38
+ throw new Error("Email cannot be empty");
39
39
  }
40
40
  if (!displayName) {
41
- throw new Error('Display name cannot be empty');
41
+ throw new Error("Display name cannot be empty");
42
42
  }
43
43
  const userID = !id?.trim() ? MajikUser.generateID() : id;
44
44
  const instance = new this({
@@ -70,18 +70,18 @@ export class MajikUser {
70
70
  */
71
71
  static fromJSON(json) {
72
72
  // Parse string to object if needed
73
- const data = typeof json === 'string' ? JSON.parse(json) : json;
74
- if (!data.id || typeof data.id !== 'string') {
75
- throw new Error('Invalid user data: missing or invalid id');
73
+ const data = typeof json === "string" ? JSON.parse(json) : json;
74
+ if (!data.id || typeof data.id !== "string") {
75
+ throw new Error("Invalid user data: missing or invalid id");
76
76
  }
77
- if (!data.email || typeof data.email !== 'string') {
78
- throw new Error('Invalid user data: missing or invalid email');
77
+ if (!data.email || typeof data.email !== "string") {
78
+ throw new Error("Invalid user data: missing or invalid email");
79
79
  }
80
- if (!data.displayName || typeof data.displayName !== 'string') {
81
- throw new Error('Invalid user data: missing or invalid displayName');
80
+ if (!data.displayName || typeof data.displayName !== "string") {
81
+ throw new Error("Invalid user data: missing or invalid displayName");
82
82
  }
83
- if (!data.hash || typeof data.hash !== 'string') {
84
- throw new Error('Invalid user data: missing or invalid hash');
83
+ if (!data.hash || typeof data.hash !== "string") {
84
+ throw new Error("Invalid user data: missing or invalid hash");
85
85
  }
86
86
  const userData = {
87
87
  id: data.id,
@@ -106,16 +106,16 @@ export class MajikUser {
106
106
  */
107
107
  static fromSupabase(supabaseUser) {
108
108
  if (!supabaseUser.id) {
109
- throw new Error('Invalid Supabase user: missing id');
109
+ throw new Error("Invalid Supabase user: missing id");
110
110
  }
111
111
  if (!supabaseUser.email) {
112
- throw new Error('Invalid Supabase user: missing email');
112
+ throw new Error("Invalid Supabase user: missing email");
113
113
  }
114
114
  // Extract display name from user_metadata or email
115
115
  const displayName = supabaseUser.user_metadata?.display_name ||
116
116
  supabaseUser.user_metadata?.full_name ||
117
117
  supabaseUser.user_metadata?.name ||
118
- supabaseUser.email.split('@')[0];
118
+ supabaseUser.email.split("@")[0];
119
119
  // Map user_metadata to MajikUser metadata
120
120
  const metadata = {
121
121
  verification: {
@@ -130,8 +130,8 @@ export class MajikUser {
130
130
  // Name mapping
131
131
  if (userMeta.first_name || userMeta.family_name) {
132
132
  metadata.name = {
133
- first_name: userMeta.first_name || '',
134
- last_name: userMeta.family_name || '',
133
+ first_name: userMeta.first_name || "",
134
+ last_name: userMeta.family_name || "",
135
135
  middle_name: userMeta.middle_name,
136
136
  suffix: userMeta.suffix,
137
137
  };
@@ -172,13 +172,15 @@ export class MajikUser {
172
172
  notifications: supabaseUser.app_metadata?.notifications ?? true,
173
173
  system: {
174
174
  isRestricted: supabaseUser.app_metadata?.is_restricted ?? false,
175
- restrictedUntil: supabaseUser.app_metadata?.restricted_until ? new Date(supabaseUser.app_metadata.restricted_until) : undefined,
175
+ restrictedUntil: supabaseUser.app_metadata?.restricted_until
176
+ ? new Date(supabaseUser.app_metadata.restricted_until)
177
+ : undefined,
176
178
  },
177
179
  };
178
180
  // Add any additional app_metadata to settings
179
181
  if (supabaseUser.app_metadata) {
180
182
  Object.keys(supabaseUser.app_metadata).forEach((key) => {
181
- if (!['notifications', 'is_restricted', 'restricted_until'].includes(key)) {
183
+ if (!["notifications", "is_restricted", "restricted_until"].includes(key)) {
182
184
  settings[key] = supabaseUser.app_metadata[key];
183
185
  }
184
186
  });
@@ -222,7 +224,7 @@ export class MajikUser {
222
224
  return null;
223
225
  const { first_name, middle_name, last_name, suffix } = this._metadata.name;
224
226
  const parts = [first_name, middle_name, last_name, suffix].filter(Boolean);
225
- return parts.join(' ');
227
+ return parts.join(" ");
226
228
  }
227
229
  get fullNameObject() {
228
230
  if (!this._metadata.name)
@@ -231,7 +233,7 @@ export class MajikUser {
231
233
  }
232
234
  set fullNameObject(name) {
233
235
  if (!name || !name?.first_name?.trim() || !name?.last_name?.trim()) {
234
- throw new Error('Full name must contain first and last names');
236
+ throw new Error("Full name must contain first and last names");
235
237
  }
236
238
  this._metadata.name = name;
237
239
  this.updateTimestamp();
@@ -268,7 +270,7 @@ export class MajikUser {
268
270
  * Get user's gender
269
271
  */
270
272
  get gender() {
271
- return this._metadata.gender || 'Unspecified';
273
+ return this._metadata.gender || "Unspecified";
272
274
  }
273
275
  /**
274
276
  * Calculate user's age from birthdate
@@ -281,7 +283,8 @@ export class MajikUser {
281
283
  const birth = new Date(birthdate);
282
284
  let age = today.getFullYear() - birth.getFullYear();
283
285
  const monthDiff = today.getMonth() - birth.getMonth();
284
- if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
286
+ if (monthDiff < 0 ||
287
+ (monthDiff === 0 && today.getDate() < birth.getDate())) {
285
288
  age--;
286
289
  }
287
290
  return age;
@@ -294,6 +297,16 @@ export class MajikUser {
294
297
  return null;
295
298
  return this._metadata.birthdate;
296
299
  }
300
+ /**
301
+ * Get user's full address if available
302
+ */
303
+ get address() {
304
+ if (!this._metadata.address)
305
+ return null;
306
+ const { building, street, area, city, region, zip, country } = this._metadata.address;
307
+ const parts = [building, street, area, city, region, zip, country].filter(Boolean);
308
+ return parts.join(", ");
309
+ }
297
310
  /**
298
311
  * Check if email is verified
299
312
  */
@@ -316,7 +329,7 @@ export class MajikUser {
316
329
  * Check if all verification steps are complete
317
330
  */
318
331
  get isFullyVerified() {
319
- return this.isEmailVerified && this.isPhoneVerified && this.isIdentityVerified;
332
+ return (this.isEmailVerified && this.isPhoneVerified && this.isIdentityVerified);
320
333
  }
321
334
  /**
322
335
  * Get user's initials from name or display name
@@ -324,11 +337,12 @@ export class MajikUser {
324
337
  get initials() {
325
338
  if (this._metadata.name) {
326
339
  const { first_name, last_name } = this._metadata.name;
327
- const firstInitial = first_name?.[0]?.toUpperCase() || '';
328
- const lastInitial = last_name?.[0]?.toUpperCase() || '';
329
- return `${firstInitial}${lastInitial}`.trim() || this._displayName[0].toUpperCase();
340
+ const firstInitial = first_name?.[0]?.toUpperCase() || "";
341
+ const lastInitial = last_name?.[0]?.toUpperCase() || "";
342
+ return (`${firstInitial}${lastInitial}`.trim() ||
343
+ this._displayName[0].toUpperCase());
330
344
  }
331
- const names = this._displayName.split(' ');
345
+ const names = this._displayName.split(" ");
332
346
  if (names.length >= 2) {
333
347
  return `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase();
334
348
  }
@@ -346,14 +360,14 @@ export class MajikUser {
346
360
  }
347
361
  set displayName(value) {
348
362
  if (!value || value.trim().length === 0) {
349
- throw new Error('Display name cannot be empty');
363
+ throw new Error("Display name cannot be empty");
350
364
  }
351
365
  this._displayName = value;
352
366
  this.updateTimestamp();
353
367
  }
354
368
  set hash(value) {
355
369
  if (!value || value.length === 0) {
356
- throw new Error('Hash cannot be empty');
370
+ throw new Error("Hash cannot be empty");
357
371
  }
358
372
  this._hash = value;
359
373
  this.updateTimestamp();
@@ -395,7 +409,7 @@ export class MajikUser {
395
409
  let formatted;
396
410
  if (birthdate instanceof Date) {
397
411
  if (Number.isNaN(birthdate.getTime())) {
398
- throw new Error('Invalid Date object');
412
+ throw new Error("Invalid Date object");
399
413
  }
400
414
  // Format to YYYY-MM-DD (UTC-safe)
401
415
  formatted = dateToYYYYMMDD(birthdate);
@@ -403,7 +417,7 @@ export class MajikUser {
403
417
  else {
404
418
  // Validate ISO date format YYYY-MM-DD
405
419
  if (!/^\d{4}-\d{2}-\d{2}$/.test(birthdate)) {
406
- throw new Error('Invalid birthdate format. Use YYYY-MM-DD');
420
+ throw new Error("Invalid birthdate format. Use YYYY-MM-DD");
407
421
  }
408
422
  formatted = birthdate;
409
423
  }
@@ -639,16 +653,28 @@ export class MajikUser {
639
653
  * Check if user has complete profile information
640
654
  */
641
655
  hasCompleteProfile() {
642
- return !!(this._metadata.name && this._metadata.phone && this._metadata.birthdate && this._metadata.address && this._metadata.gender);
656
+ return !!(this._metadata.name &&
657
+ this._metadata.phone &&
658
+ this._metadata.birthdate &&
659
+ this._metadata.address &&
660
+ this._metadata.gender);
643
661
  }
644
662
  /**
645
663
  * Get profile completion percentage (0-100)
646
664
  */
647
665
  getProfileCompletionPercentage() {
648
- const fields = ['name', 'picture', 'phone', 'gender', 'birthdate', 'address', 'bio'];
666
+ const fields = [
667
+ "name",
668
+ "picture",
669
+ "phone",
670
+ "gender",
671
+ "birthdate",
672
+ "address",
673
+ "bio",
674
+ ];
649
675
  const completedFields = fields.filter((field) => {
650
676
  const value = this._metadata[field];
651
- if (typeof value === 'object' && value !== null) {
677
+ if (typeof value === "object" && value !== null) {
652
678
  return Object.keys(value).length > 0;
653
679
  }
654
680
  return !!value;
@@ -660,13 +686,13 @@ export class MajikUser {
660
686
  const errors = [];
661
687
  // Required fields
662
688
  if (!this.id)
663
- errors.push('ID is required');
689
+ errors.push("ID is required");
664
690
  if (!this._email)
665
- errors.push('Email is required');
691
+ errors.push("Email is required");
666
692
  if (!this._displayName)
667
- errors.push('Display name is required');
693
+ errors.push("Display name is required");
668
694
  if (!this._hash)
669
- errors.push('Hash is required');
695
+ errors.push("Hash is required");
670
696
  // Format validation
671
697
  try {
672
698
  this.validateEmail(this._email);
@@ -676,20 +702,21 @@ export class MajikUser {
676
702
  }
677
703
  // Date validation
678
704
  if (!(this.createdAt instanceof Date) || isNaN(this.createdAt.getTime())) {
679
- errors.push('Invalid createdAt date');
705
+ errors.push("Invalid createdAt date");
680
706
  }
681
- if (!(this._lastUpdate instanceof Date) || isNaN(this._lastUpdate.getTime())) {
682
- errors.push('Invalid lastUpdate date');
707
+ if (!(this._lastUpdate instanceof Date) ||
708
+ isNaN(this._lastUpdate.getTime())) {
709
+ errors.push("Invalid lastUpdate date");
683
710
  }
684
711
  // Metadata validation
685
712
  if (this._metadata.phone) {
686
713
  if (!/^\+?[1-9]\d{1,14}$/.test(this._metadata.phone)) {
687
- errors.push('Invalid phone number format');
714
+ errors.push("Invalid phone number format");
688
715
  }
689
716
  }
690
717
  if (this._metadata.birthdate) {
691
718
  if (!/^\d{4}-\d{2}-\d{2}$/.test(this._metadata.birthdate)) {
692
- errors.push('Invalid birthdate format');
719
+ errors.push("Invalid birthdate format");
693
720
  }
694
721
  }
695
722
  return {
@@ -770,7 +797,7 @@ export class MajikUser {
770
797
  validateEmail(email) {
771
798
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
772
799
  if (!emailRegex.test(email)) {
773
- throw new Error('Invalid email format');
800
+ throw new Error("Invalid email format");
774
801
  }
775
802
  }
776
803
  // ==================== STATIC CRYPTOGRAPHIC METHODS ====================
@@ -786,7 +813,7 @@ export class MajikUser {
786
813
  const skCurve = ed2curve.convertSecretKey(ed.secretKey);
787
814
  const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
788
815
  if (!skCurve || !pkCurve) {
789
- throw new Error('Failed to convert derived Ed25519 keys to Curve25519');
816
+ throw new Error("Failed to convert derived Ed25519 keys to Curve25519");
790
817
  }
791
818
  const pkCurveBytes = new Uint8Array(pkCurve);
792
819
  const publicKey = arrayBufferToBase64(pkCurveBytes.buffer);
@@ -7,6 +7,10 @@ import type { MAJIK_API_RESPONSE } from "./core/types";
7
7
  import { MajikMessageChat } from "./core/database/chat/majik-message-chat";
8
8
  import { MajikMessageIdentity } from "./core/database/system/identity";
9
9
  type MajikMessageEvents = "message" | "envelope" | "untrusted" | "error";
10
+ interface MajikMessageStatic<T extends MajikMessage> {
11
+ new (config: MajikMessageConfig, id?: string): T;
12
+ fromJSON(json: MajikMessageJSON): Promise<T>;
13
+ }
10
14
  export interface MajikMessageConfig {
11
15
  keyStore: KeyStore;
12
16
  contactDirectory?: MajikContactDirectory;
@@ -81,7 +85,7 @@ export declare class MajikMessage {
81
85
  /**
82
86
  * Set an active account (moves it to index 0)
83
87
  */
84
- setActiveAccount(id: string): boolean;
88
+ setActiveAccount(id: string): Promise<boolean>;
85
89
  getActiveAccount(): MajikContact | null;
86
90
  isAccountActive(id: string): boolean;
87
91
  /**
@@ -94,6 +98,12 @@ export declare class MajikMessage {
94
98
  * Returns the MajikContact instance or null if not found.
95
99
  */
96
100
  getContactByID(id: string): MajikContact | null;
101
+ /**
102
+ * Retrieve a contact from the directory by its public key.
103
+ * Validates that the input is a non-empty string.
104
+ * Returns the MajikContact instance or null if not found.
105
+ */
106
+ getContactByPublicKey(id: string): Promise<MajikContact | null>;
97
107
  /**
98
108
  * Returns a JSON string representation of a contact
99
109
  * suitable for sharing.
@@ -194,7 +204,7 @@ export declare class MajikMessage {
194
204
  }>;
195
205
  isPassphraseValid(passphrase: string, id?: string): Promise<boolean>;
196
206
  toJSON(): Promise<MajikMessageJSON>;
197
- static fromJSON(json: MajikMessageJSON): Promise<MajikMessage>;
207
+ static fromJSON<T extends MajikMessage>(this: new (config: MajikMessageConfig, id?: string) => T, json: MajikMessageJSON): Promise<T>;
198
208
  /**
199
209
  * Set a PIN (stores hash). Passphrase is any string; we store SHA-256(base64) of it.
200
210
  */
@@ -215,6 +225,6 @@ export declare class MajikMessage {
215
225
  /**
216
226
  * Try to load an existing state from IDB; if none exists, create a fresh instance and save it.
217
227
  */
218
- static loadOrCreate(config: MajikMessageConfig): Promise<MajikMessage>;
228
+ static loadOrCreate<T extends MajikMessage>(this: MajikMessageStatic<T>, config: MajikMessageConfig): Promise<T>;
219
229
  }
220
230
  export {};
@@ -160,9 +160,17 @@ export class MajikMessage {
160
160
  /**
161
161
  * Set an active account (moves it to index 0)
162
162
  */
163
- setActiveAccount(id) {
163
+ async setActiveAccount(id) {
164
164
  if (!this.ownAccounts.has(id))
165
165
  return false;
166
+ // Ensure identity is unlocked
167
+ try {
168
+ await this.ensureIdentityUnlocked(id);
169
+ }
170
+ catch (err) {
171
+ console.warn("Failed to unlock account:", err);
172
+ return false; // don't set as active if unlock fails
173
+ }
166
174
  // Remove ID from current position
167
175
  const index = this.ownAccountsOrder.indexOf(id);
168
176
  if (index > -1)
@@ -216,6 +224,17 @@ export class MajikMessage {
216
224
  }
217
225
  return this.contactDirectory.getContact(id) ?? null;
218
226
  }
227
+ /**
228
+ * Retrieve a contact from the directory by its public key.
229
+ * Validates that the input is a non-empty string.
230
+ * Returns the MajikContact instance or null if not found.
231
+ */
232
+ async getContactByPublicKey(id) {
233
+ if (typeof id !== "string" || !id.trim()) {
234
+ throw new Error("Invalid contact ID: must be a non-empty string");
235
+ }
236
+ return ((await this.contactDirectory.getContactByPublicKeyBase64(id)) ?? null);
237
+ }
219
238
  /**
220
239
  * Returns a JSON string representation of a contact
221
240
  * suitable for sharing.
@@ -830,7 +849,7 @@ export class MajikMessage {
830
849
  const newDirectory = new MajikContactDirectory();
831
850
  const parsedContacts = await newDirectory.fromJSON(json.contacts);
832
851
  const parsedEnvelopeCache = EnvelopeCache.fromJSON(json.envelopeCache);
833
- const parsedInstance = new MajikMessage({
852
+ const parsedInstance = new this({
834
853
  contactDirectory: parsedContacts,
835
854
  envelopeCache: parsedEnvelopeCache,
836
855
  keyStore: KeyStore,
@@ -1027,7 +1046,7 @@ export class MajikMessage {
1027
1046
  if (saved?.data) {
1028
1047
  const loaded = await loadSavedMajikFileData(saved.data);
1029
1048
  const parsedJSON = loaded.j;
1030
- const instance = await MajikMessage.fromJSON(parsedJSON);
1049
+ const instance = (await this.fromJSON(parsedJSON));
1031
1050
  console.log("Account Loaded Successfully");
1032
1051
  instance.attachAutosaveHandlers();
1033
1052
  return instance;
@@ -1036,8 +1055,8 @@ export class MajikMessage {
1036
1055
  catch (err) {
1037
1056
  console.warn("Error trying to load saved MajikMessage state:", err);
1038
1057
  }
1039
- // No saved state; create new and persist initial state
1040
- const created = new MajikMessage(config);
1058
+ // No saved state create new subclass instance
1059
+ const created = new this(config);
1041
1060
  await created.saveState();
1042
1061
  created.attachAutosaveHandlers();
1043
1062
  return created;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thezelijah/majik-message",
3
3
  "type": "module",
4
4
  "description": "Encrypt and decrypt messages on any website. Secure chats with keypairs and seed-based accounts. Open source.",
5
- "version": "1.0.13",
5
+ "version": "1.0.15",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",