@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.
- package/dist/core/contacts/majik-contact-directory.d.ts +5 -0
- package/dist/core/contacts/majik-contact-directory.js +16 -0
- package/dist/core/database/chat/majik-message-chat.d.ts +7 -2
- package/dist/core/database/chat/majik-message-chat.js +18 -4
- package/dist/core/database/chat/types.d.ts +1 -0
- package/dist/core/database/system/majik-user/majik-user.d.ts +6 -2
- package/dist/core/database/system/majik-user/majik-user.js +78 -51
- package/dist/majik-message.d.ts +13 -3
- package/dist/majik-message.js +24 -5
- package/package.json +1 -1
|
@@ -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():
|
|
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
|
-
//
|
|
373
|
-
|
|
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());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UserBasicInformation, FullName, Address, UserSettings, MajikUserJSON, SupabaseUser, YYYYMMDD } from
|
|
2
|
-
import type { UserGenderOptions } from
|
|
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
|
|
3
|
-
import { generateMnemonic, mnemonicToSeedSync } from
|
|
4
|
-
import * as ed25519 from
|
|
5
|
-
import ed2curve from
|
|
6
|
-
import { hash } from
|
|
7
|
-
import { wordlist } from
|
|
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(
|
|
38
|
+
throw new Error("Email cannot be empty");
|
|
39
39
|
}
|
|
40
40
|
if (!displayName) {
|
|
41
|
-
throw new Error(
|
|
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 ===
|
|
74
|
-
if (!data.id || typeof data.id !==
|
|
75
|
-
throw new Error(
|
|
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 !==
|
|
78
|
-
throw new Error(
|
|
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 !==
|
|
81
|
-
throw new Error(
|
|
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 !==
|
|
84
|
-
throw new Error(
|
|
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(
|
|
109
|
+
throw new Error("Invalid Supabase user: missing id");
|
|
110
110
|
}
|
|
111
111
|
if (!supabaseUser.email) {
|
|
112
|
-
throw new Error(
|
|
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(
|
|
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
|
|
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 (![
|
|
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(
|
|
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 ||
|
|
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 ||
|
|
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() ||
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 &&
|
|
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 = [
|
|
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 ===
|
|
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(
|
|
689
|
+
errors.push("ID is required");
|
|
664
690
|
if (!this._email)
|
|
665
|
-
errors.push(
|
|
691
|
+
errors.push("Email is required");
|
|
666
692
|
if (!this._displayName)
|
|
667
|
-
errors.push(
|
|
693
|
+
errors.push("Display name is required");
|
|
668
694
|
if (!this._hash)
|
|
669
|
-
errors.push(
|
|
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(
|
|
705
|
+
errors.push("Invalid createdAt date");
|
|
680
706
|
}
|
|
681
|
-
if (!(this._lastUpdate instanceof Date) ||
|
|
682
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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);
|
package/dist/majik-message.d.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
228
|
+
static loadOrCreate<T extends MajikMessage>(this: MajikMessageStatic<T>, config: MajikMessageConfig): Promise<T>;
|
|
219
229
|
}
|
|
220
230
|
export {};
|
package/dist/majik-message.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
1040
|
-
const created = new
|
|
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.
|
|
5
|
+
"version": "1.0.15",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|