@thezelijah/majik-message 1.0.0 → 1.0.2
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/README.md +4 -0
- package/dist/core/compressor/majik-compressor.d.ts +17 -0
- package/dist/core/compressor/majik-compressor.js +86 -0
- package/dist/core/crypto/keystore.js +12 -0
- package/dist/core/database/chat/majik-message-chat.d.ts +94 -0
- package/dist/core/database/chat/majik-message-chat.js +432 -0
- package/dist/core/database/chat/types.d.ts +11 -0
- package/dist/core/database/chat/types.js +1 -0
- package/dist/core/database/system/identity.d.ts +61 -0
- package/dist/core/database/system/identity.js +170 -0
- package/dist/core/database/system/majik-user/enums.d.ts +44 -0
- package/dist/core/database/system/majik-user/enums.js +40 -0
- package/dist/core/database/system/majik-user/majik-user.d.ts +257 -0
- package/dist/core/database/system/majik-user/majik-user.js +812 -0
- package/dist/core/database/system/majik-user/types.d.ts +186 -0
- package/dist/core/database/system/majik-user/types.js +1 -0
- package/dist/core/database/system/majik-user/utils.d.ts +32 -0
- package/dist/core/database/system/majik-user/utils.js +110 -0
- package/dist/core/database/system/utils.d.ts +1 -0
- package/dist/core/database/system/utils.js +8 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/utils/idb-majik-system.d.ts +5 -5
- package/dist/core/utils/utilities.d.ts +7 -0
- package/dist/core/utils/utilities.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/majik-message.d.ts +18 -0
- package/dist/majik-message.js +101 -0
- package/package.json +2 -1
|
@@ -0,0 +1,812 @@
|
|
|
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';
|
|
8
|
+
/**
|
|
9
|
+
* Base user class for database persistence
|
|
10
|
+
* Designed to be extended by subclasses with additional metadata
|
|
11
|
+
*/
|
|
12
|
+
export class MajikUser {
|
|
13
|
+
id;
|
|
14
|
+
_email;
|
|
15
|
+
_displayName;
|
|
16
|
+
_hash;
|
|
17
|
+
_metadata;
|
|
18
|
+
_settings;
|
|
19
|
+
createdAt;
|
|
20
|
+
_lastUpdate;
|
|
21
|
+
constructor(data) {
|
|
22
|
+
this.id = data.id;
|
|
23
|
+
this._email = data.email;
|
|
24
|
+
this._displayName = data.displayName;
|
|
25
|
+
this._hash = data.hash;
|
|
26
|
+
this._metadata = { ...data.metadata };
|
|
27
|
+
this._settings = { ...data.settings };
|
|
28
|
+
this.createdAt = new Date(data.createdAt);
|
|
29
|
+
this._lastUpdate = new Date(data.lastUpdate);
|
|
30
|
+
}
|
|
31
|
+
// ==================== STATIC FACTORY METHODS ====================
|
|
32
|
+
/**
|
|
33
|
+
* Initialize a new user with email and display name
|
|
34
|
+
* Generates a UUID for the id if unset and sets timestamps
|
|
35
|
+
*/
|
|
36
|
+
static initialize(email, displayName, id) {
|
|
37
|
+
if (!email) {
|
|
38
|
+
throw new Error('Email cannot be empty');
|
|
39
|
+
}
|
|
40
|
+
if (!displayName) {
|
|
41
|
+
throw new Error('Display name cannot be empty');
|
|
42
|
+
}
|
|
43
|
+
const userID = !id?.trim() ? MajikUser.generateID() : id;
|
|
44
|
+
const instance = new this({
|
|
45
|
+
id: userID,
|
|
46
|
+
email,
|
|
47
|
+
displayName,
|
|
48
|
+
hash: MajikUser.hashID(userID),
|
|
49
|
+
metadata: {
|
|
50
|
+
verification: {
|
|
51
|
+
email_verified: false,
|
|
52
|
+
phone_verified: false,
|
|
53
|
+
identity_verified: false,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
settings: {
|
|
57
|
+
notifications: true,
|
|
58
|
+
system: {
|
|
59
|
+
isRestricted: false,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
createdAt: new Date(),
|
|
63
|
+
lastUpdate: new Date(),
|
|
64
|
+
});
|
|
65
|
+
instance.validateEmail(email);
|
|
66
|
+
return instance;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Deserialize user from JSON object or JSON string
|
|
70
|
+
*/
|
|
71
|
+
static fromJSON(json) {
|
|
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');
|
|
76
|
+
}
|
|
77
|
+
if (!data.email || typeof data.email !== 'string') {
|
|
78
|
+
throw new Error('Invalid user data: missing or invalid email');
|
|
79
|
+
}
|
|
80
|
+
if (!data.displayName || typeof data.displayName !== 'string') {
|
|
81
|
+
throw new Error('Invalid user data: missing or invalid displayName');
|
|
82
|
+
}
|
|
83
|
+
if (!data.hash || typeof data.hash !== 'string') {
|
|
84
|
+
throw new Error('Invalid user data: missing or invalid hash');
|
|
85
|
+
}
|
|
86
|
+
const userData = {
|
|
87
|
+
id: data.id,
|
|
88
|
+
email: data.email,
|
|
89
|
+
displayName: data.displayName,
|
|
90
|
+
hash: data.hash,
|
|
91
|
+
metadata: data.metadata || {},
|
|
92
|
+
settings: data.settings || {
|
|
93
|
+
notifications: true,
|
|
94
|
+
system: {
|
|
95
|
+
isRestricted: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
createdAt: data.createdAt ? new Date(data.createdAt) : new Date(),
|
|
99
|
+
lastUpdate: data.lastUpdate ? new Date(data.lastUpdate) : new Date(),
|
|
100
|
+
};
|
|
101
|
+
return new this(userData);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create MajikUser from Supabase User object
|
|
105
|
+
* Maps Supabase user fields to MajikUser structure
|
|
106
|
+
*/
|
|
107
|
+
static fromSupabase(supabaseUser) {
|
|
108
|
+
if (!supabaseUser.id) {
|
|
109
|
+
throw new Error('Invalid Supabase user: missing id');
|
|
110
|
+
}
|
|
111
|
+
if (!supabaseUser.email) {
|
|
112
|
+
throw new Error('Invalid Supabase user: missing email');
|
|
113
|
+
}
|
|
114
|
+
// Extract display name from user_metadata or email
|
|
115
|
+
const displayName = supabaseUser.user_metadata?.display_name ||
|
|
116
|
+
supabaseUser.user_metadata?.full_name ||
|
|
117
|
+
supabaseUser.user_metadata?.name ||
|
|
118
|
+
supabaseUser.email.split('@')[0];
|
|
119
|
+
// Map user_metadata to MajikUser metadata
|
|
120
|
+
const metadata = {
|
|
121
|
+
verification: {
|
|
122
|
+
email_verified: !!supabaseUser.email_confirmed_at,
|
|
123
|
+
phone_verified: !!supabaseUser.phone_confirmed_at,
|
|
124
|
+
identity_verified: false,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
// Map optional fields from user_metadata if they exist
|
|
128
|
+
if (supabaseUser.user_metadata) {
|
|
129
|
+
const userMeta = supabaseUser.user_metadata;
|
|
130
|
+
// Name mapping
|
|
131
|
+
if (userMeta.first_name || userMeta.family_name) {
|
|
132
|
+
metadata.name = {
|
|
133
|
+
first_name: userMeta.first_name || '',
|
|
134
|
+
last_name: userMeta.family_name || '',
|
|
135
|
+
middle_name: userMeta.middle_name,
|
|
136
|
+
suffix: userMeta.suffix,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Direct field mappings
|
|
140
|
+
if (userMeta.picture || userMeta.avatar_url) {
|
|
141
|
+
metadata.picture = userMeta.picture || userMeta.avatar_url;
|
|
142
|
+
}
|
|
143
|
+
if (userMeta.bio)
|
|
144
|
+
metadata.bio = userMeta.bio;
|
|
145
|
+
if (userMeta.phone)
|
|
146
|
+
metadata.phone = userMeta.phone;
|
|
147
|
+
if (userMeta.gender)
|
|
148
|
+
metadata.gender = userMeta.gender;
|
|
149
|
+
if (userMeta.birthdate)
|
|
150
|
+
metadata.birthdate = userMeta.birthdate;
|
|
151
|
+
if (userMeta.language)
|
|
152
|
+
metadata.language = userMeta.language;
|
|
153
|
+
if (userMeta.timezone)
|
|
154
|
+
metadata.timezone = userMeta.timezone;
|
|
155
|
+
if (userMeta.pronouns)
|
|
156
|
+
metadata.pronouns = userMeta.pronouns;
|
|
157
|
+
// Address mapping
|
|
158
|
+
if (userMeta.address) {
|
|
159
|
+
metadata.address = userMeta.address;
|
|
160
|
+
}
|
|
161
|
+
// Social links mapping
|
|
162
|
+
if (userMeta.social_links) {
|
|
163
|
+
metadata.social_links = userMeta.social_links;
|
|
164
|
+
}
|
|
165
|
+
// Company information
|
|
166
|
+
if (userMeta.company) {
|
|
167
|
+
metadata.company = userMeta.company;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Map app_metadata to settings
|
|
171
|
+
const settings = {
|
|
172
|
+
notifications: supabaseUser.app_metadata?.notifications ?? true,
|
|
173
|
+
system: {
|
|
174
|
+
isRestricted: supabaseUser.app_metadata?.is_restricted ?? false,
|
|
175
|
+
restrictedUntil: supabaseUser.app_metadata?.restricted_until ? new Date(supabaseUser.app_metadata.restricted_until) : undefined,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
// Add any additional app_metadata to settings
|
|
179
|
+
if (supabaseUser.app_metadata) {
|
|
180
|
+
Object.keys(supabaseUser.app_metadata).forEach((key) => {
|
|
181
|
+
if (!['notifications', 'is_restricted', 'restricted_until'].includes(key)) {
|
|
182
|
+
settings[key] = supabaseUser.app_metadata[key];
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const userData = {
|
|
187
|
+
id: supabaseUser.id,
|
|
188
|
+
email: supabaseUser.email,
|
|
189
|
+
displayName,
|
|
190
|
+
hash: MajikUser.hashID(supabaseUser.id),
|
|
191
|
+
metadata,
|
|
192
|
+
settings,
|
|
193
|
+
createdAt: new Date(supabaseUser.created_at),
|
|
194
|
+
lastUpdate: new Date(supabaseUser.updated_at || supabaseUser.created_at),
|
|
195
|
+
};
|
|
196
|
+
return new this(userData);
|
|
197
|
+
}
|
|
198
|
+
// ==================== GETTERS ====================
|
|
199
|
+
get email() {
|
|
200
|
+
return this._email;
|
|
201
|
+
}
|
|
202
|
+
get displayName() {
|
|
203
|
+
return this._displayName;
|
|
204
|
+
}
|
|
205
|
+
get hash() {
|
|
206
|
+
return this._hash;
|
|
207
|
+
}
|
|
208
|
+
get metadata() {
|
|
209
|
+
return { ...this._metadata };
|
|
210
|
+
}
|
|
211
|
+
get settings() {
|
|
212
|
+
return { ...this._settings };
|
|
213
|
+
}
|
|
214
|
+
get lastUpdate() {
|
|
215
|
+
return new Date(this._lastUpdate);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get user's full name if available
|
|
219
|
+
*/
|
|
220
|
+
get fullName() {
|
|
221
|
+
if (!this._metadata.name)
|
|
222
|
+
return null;
|
|
223
|
+
const { first_name, middle_name, last_name, suffix } = this._metadata.name;
|
|
224
|
+
const parts = [first_name, middle_name, last_name, suffix].filter(Boolean);
|
|
225
|
+
return parts.join(' ');
|
|
226
|
+
}
|
|
227
|
+
get fullNameObject() {
|
|
228
|
+
if (!this._metadata.name)
|
|
229
|
+
return null;
|
|
230
|
+
return this._metadata.name;
|
|
231
|
+
}
|
|
232
|
+
set fullNameObject(name) {
|
|
233
|
+
if (!name || !name?.first_name?.trim() || !name?.last_name?.trim()) {
|
|
234
|
+
throw new Error('Full name must contain first and last names');
|
|
235
|
+
}
|
|
236
|
+
this._metadata.name = name;
|
|
237
|
+
this.updateTimestamp();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get user's formatted name (first + last)
|
|
241
|
+
*/
|
|
242
|
+
get formattedName() {
|
|
243
|
+
if (!this._metadata.name)
|
|
244
|
+
return this._displayName;
|
|
245
|
+
const { first_name, last_name } = this._metadata.name;
|
|
246
|
+
if (first_name && last_name) {
|
|
247
|
+
return `${first_name} ${last_name}`;
|
|
248
|
+
}
|
|
249
|
+
return this._displayName;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get user's first name if available
|
|
253
|
+
*/
|
|
254
|
+
get firstName() {
|
|
255
|
+
if (!this._metadata?.name?.first_name?.trim())
|
|
256
|
+
return null;
|
|
257
|
+
return this._metadata.name.first_name;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get user's last name if available
|
|
261
|
+
*/
|
|
262
|
+
get lastName() {
|
|
263
|
+
if (!this._metadata?.name?.last_name?.trim())
|
|
264
|
+
return null;
|
|
265
|
+
return this._metadata.name.last_name;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get user's gender
|
|
269
|
+
*/
|
|
270
|
+
get gender() {
|
|
271
|
+
return this._metadata.gender || 'Unspecified';
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Calculate user's age from birthdate
|
|
275
|
+
*/
|
|
276
|
+
get age() {
|
|
277
|
+
const birthdate = this._metadata.birthdate;
|
|
278
|
+
if (!birthdate)
|
|
279
|
+
return null;
|
|
280
|
+
const today = new Date();
|
|
281
|
+
const birth = new Date(birthdate);
|
|
282
|
+
let age = today.getFullYear() - birth.getFullYear();
|
|
283
|
+
const monthDiff = today.getMonth() - birth.getMonth();
|
|
284
|
+
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
|
285
|
+
age--;
|
|
286
|
+
}
|
|
287
|
+
return age;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get user's first name if available
|
|
291
|
+
*/
|
|
292
|
+
get birthday() {
|
|
293
|
+
if (!this._metadata?.birthdate?.trim())
|
|
294
|
+
return null;
|
|
295
|
+
return this._metadata.birthdate;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Check if email is verified
|
|
299
|
+
*/
|
|
300
|
+
get isEmailVerified() {
|
|
301
|
+
return this._metadata.verification?.email_verified ?? false;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Check if phone is verified
|
|
305
|
+
*/
|
|
306
|
+
get isPhoneVerified() {
|
|
307
|
+
return this._metadata.verification?.phone_verified ?? false;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check if identity is verified
|
|
311
|
+
*/
|
|
312
|
+
get isIdentityVerified() {
|
|
313
|
+
return this._metadata.verification?.identity_verified ?? false;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Check if all verification steps are complete
|
|
317
|
+
*/
|
|
318
|
+
get isFullyVerified() {
|
|
319
|
+
return this.isEmailVerified && this.isPhoneVerified && this.isIdentityVerified;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get user's initials from name or display name
|
|
323
|
+
*/
|
|
324
|
+
get initials() {
|
|
325
|
+
if (this._metadata.name) {
|
|
326
|
+
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();
|
|
330
|
+
}
|
|
331
|
+
const names = this._displayName.split(' ');
|
|
332
|
+
if (names.length >= 2) {
|
|
333
|
+
return `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase();
|
|
334
|
+
}
|
|
335
|
+
return this._displayName.slice(0, 2).toUpperCase();
|
|
336
|
+
}
|
|
337
|
+
// ==================== SETTERS ====================
|
|
338
|
+
set email(value) {
|
|
339
|
+
this.validateEmail(value);
|
|
340
|
+
this._email = value;
|
|
341
|
+
// Unverify email when changed
|
|
342
|
+
if (this._metadata.verification) {
|
|
343
|
+
this._metadata.verification.email_verified = false;
|
|
344
|
+
}
|
|
345
|
+
this.updateTimestamp();
|
|
346
|
+
}
|
|
347
|
+
set displayName(value) {
|
|
348
|
+
if (!value || value.trim().length === 0) {
|
|
349
|
+
throw new Error('Display name cannot be empty');
|
|
350
|
+
}
|
|
351
|
+
this._displayName = value;
|
|
352
|
+
this.updateTimestamp();
|
|
353
|
+
}
|
|
354
|
+
set hash(value) {
|
|
355
|
+
if (!value || value.length === 0) {
|
|
356
|
+
throw new Error('Hash cannot be empty');
|
|
357
|
+
}
|
|
358
|
+
this._hash = value;
|
|
359
|
+
this.updateTimestamp();
|
|
360
|
+
}
|
|
361
|
+
// ==================== METADATA METHODS ====================
|
|
362
|
+
/**
|
|
363
|
+
* Update user's full name
|
|
364
|
+
*/
|
|
365
|
+
setName(name) {
|
|
366
|
+
this.updateMetadata({ name });
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Update user's profile picture
|
|
370
|
+
*/
|
|
371
|
+
setPicture(url) {
|
|
372
|
+
this.updateMetadata({ picture: url });
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Update user's phone number
|
|
376
|
+
*/
|
|
377
|
+
setPhone(phone) {
|
|
378
|
+
this.updateMetadata({ phone });
|
|
379
|
+
// Unverify phone when changed
|
|
380
|
+
if (this._metadata.verification) {
|
|
381
|
+
this._metadata.verification.phone_verified = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Update user's address
|
|
386
|
+
*/
|
|
387
|
+
setAddress(address) {
|
|
388
|
+
this.updateMetadata({ address });
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Update user's birthdate
|
|
392
|
+
* Accepts either YYYY-MM-DD string or Date object
|
|
393
|
+
*/
|
|
394
|
+
setBirthdate(birthdate) {
|
|
395
|
+
let formatted;
|
|
396
|
+
if (birthdate instanceof Date) {
|
|
397
|
+
if (Number.isNaN(birthdate.getTime())) {
|
|
398
|
+
throw new Error('Invalid Date object');
|
|
399
|
+
}
|
|
400
|
+
// Format to YYYY-MM-DD (UTC-safe)
|
|
401
|
+
formatted = dateToYYYYMMDD(birthdate);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// Validate ISO date format YYYY-MM-DD
|
|
405
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(birthdate)) {
|
|
406
|
+
throw new Error('Invalid birthdate format. Use YYYY-MM-DD');
|
|
407
|
+
}
|
|
408
|
+
formatted = birthdate;
|
|
409
|
+
}
|
|
410
|
+
this.updateMetadata({ birthdate: formatted });
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Update user's address
|
|
414
|
+
*/
|
|
415
|
+
setGender(gender) {
|
|
416
|
+
this.updateMetadata({ gender });
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Update user's bio
|
|
420
|
+
*/
|
|
421
|
+
setBio(bio) {
|
|
422
|
+
this.updateMetadata({ bio });
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Update user's language preference
|
|
426
|
+
*/
|
|
427
|
+
setLanguage(language) {
|
|
428
|
+
this.updateMetadata({ language });
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Update user's timezone
|
|
432
|
+
*/
|
|
433
|
+
setTimezone(timezone) {
|
|
434
|
+
this.updateMetadata({ timezone });
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Add or update a social link
|
|
438
|
+
*/
|
|
439
|
+
setSocialLink(platform, url) {
|
|
440
|
+
const socialLinks = { ...this._metadata.social_links, [platform]: url };
|
|
441
|
+
this.updateMetadata({ social_links: socialLinks });
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Remove a social link
|
|
445
|
+
*/
|
|
446
|
+
removeSocialLink(platform) {
|
|
447
|
+
if (!this._metadata.social_links)
|
|
448
|
+
return;
|
|
449
|
+
const socialLinks = { ...this._metadata.social_links };
|
|
450
|
+
delete socialLinks[platform];
|
|
451
|
+
this.updateMetadata({ social_links: socialLinks });
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Update a specific metadata field
|
|
455
|
+
*/
|
|
456
|
+
setMetadata(key, value) {
|
|
457
|
+
this._metadata[key] = value;
|
|
458
|
+
this.updateTimestamp();
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Merge multiple metadata fields
|
|
462
|
+
*/
|
|
463
|
+
updateMetadata(updates) {
|
|
464
|
+
this._metadata = { ...this._metadata, ...updates };
|
|
465
|
+
this._lastUpdate = new Date();
|
|
466
|
+
}
|
|
467
|
+
// ==================== VERIFICATION METHODS ====================
|
|
468
|
+
/**
|
|
469
|
+
* Mark email as verified
|
|
470
|
+
*/
|
|
471
|
+
verifyEmail() {
|
|
472
|
+
const currentVerification = this._metadata.verification || {
|
|
473
|
+
email_verified: false,
|
|
474
|
+
phone_verified: false,
|
|
475
|
+
identity_verified: false,
|
|
476
|
+
};
|
|
477
|
+
this.updateMetadata({
|
|
478
|
+
verification: {
|
|
479
|
+
...currentVerification,
|
|
480
|
+
email_verified: true,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Mark email as unverified
|
|
486
|
+
*/
|
|
487
|
+
unverifyEmail() {
|
|
488
|
+
const currentVerification = this._metadata.verification || {
|
|
489
|
+
email_verified: false,
|
|
490
|
+
phone_verified: false,
|
|
491
|
+
identity_verified: false,
|
|
492
|
+
};
|
|
493
|
+
this.updateMetadata({
|
|
494
|
+
verification: {
|
|
495
|
+
...currentVerification,
|
|
496
|
+
email_verified: false,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Mark phone as verified
|
|
502
|
+
*/
|
|
503
|
+
verifyPhone() {
|
|
504
|
+
const currentVerification = this._metadata.verification || {
|
|
505
|
+
email_verified: false,
|
|
506
|
+
phone_verified: false,
|
|
507
|
+
identity_verified: false,
|
|
508
|
+
};
|
|
509
|
+
this.updateMetadata({
|
|
510
|
+
verification: {
|
|
511
|
+
...currentVerification,
|
|
512
|
+
phone_verified: true,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Mark phone as unverified
|
|
518
|
+
*/
|
|
519
|
+
unverifyPhone() {
|
|
520
|
+
const currentVerification = this._metadata.verification || {
|
|
521
|
+
email_verified: false,
|
|
522
|
+
phone_verified: false,
|
|
523
|
+
identity_verified: false,
|
|
524
|
+
};
|
|
525
|
+
this.updateMetadata({
|
|
526
|
+
verification: {
|
|
527
|
+
...currentVerification,
|
|
528
|
+
phone_verified: false,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Mark identity as verified (KYC)
|
|
534
|
+
*/
|
|
535
|
+
verifyIdentity() {
|
|
536
|
+
const currentVerification = this._metadata.verification || {
|
|
537
|
+
email_verified: false,
|
|
538
|
+
phone_verified: false,
|
|
539
|
+
identity_verified: false,
|
|
540
|
+
};
|
|
541
|
+
this.updateMetadata({
|
|
542
|
+
verification: {
|
|
543
|
+
...currentVerification,
|
|
544
|
+
identity_verified: true,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Mark identity as unverified
|
|
550
|
+
*/
|
|
551
|
+
unverifyIdentity() {
|
|
552
|
+
const currentVerification = this._metadata.verification || {
|
|
553
|
+
email_verified: false,
|
|
554
|
+
phone_verified: false,
|
|
555
|
+
identity_verified: false,
|
|
556
|
+
};
|
|
557
|
+
this.updateMetadata({
|
|
558
|
+
verification: {
|
|
559
|
+
...currentVerification,
|
|
560
|
+
identity_verified: false,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
// ==================== SETTINGS METHODS ====================
|
|
565
|
+
/**
|
|
566
|
+
* Update a specific setting
|
|
567
|
+
*/
|
|
568
|
+
setSetting(key, value) {
|
|
569
|
+
this._settings[key] = value;
|
|
570
|
+
this.updateTimestamp();
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Merge multiple settings
|
|
574
|
+
*/
|
|
575
|
+
updateSettings(updates) {
|
|
576
|
+
this._settings = {
|
|
577
|
+
...this._settings,
|
|
578
|
+
...updates,
|
|
579
|
+
system: {
|
|
580
|
+
...this._settings.system,
|
|
581
|
+
...(updates.system || {}),
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
this.updateTimestamp();
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Enable notifications
|
|
588
|
+
*/
|
|
589
|
+
enableNotifications() {
|
|
590
|
+
this.updateSettings({ notifications: true });
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Disable notifications
|
|
594
|
+
*/
|
|
595
|
+
disableNotifications() {
|
|
596
|
+
this.updateSettings({ notifications: false });
|
|
597
|
+
}
|
|
598
|
+
// ==================== RESTRICTION METHODS ====================
|
|
599
|
+
/**
|
|
600
|
+
* Check if user is currently restricted
|
|
601
|
+
*/
|
|
602
|
+
isCurrentlyRestricted() {
|
|
603
|
+
if (!this._settings.system.isRestricted)
|
|
604
|
+
return false;
|
|
605
|
+
if (!this._settings.system.restrictedUntil)
|
|
606
|
+
return true;
|
|
607
|
+
return new Date() < this._settings.system.restrictedUntil;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Restrict user until a specific date (or indefinitely)
|
|
611
|
+
*/
|
|
612
|
+
restrict(until) {
|
|
613
|
+
this.updateSettings({
|
|
614
|
+
system: {
|
|
615
|
+
isRestricted: true,
|
|
616
|
+
restrictedUntil: until,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Remove restriction from user
|
|
622
|
+
*/
|
|
623
|
+
unrestrict() {
|
|
624
|
+
this.updateSettings({
|
|
625
|
+
system: {
|
|
626
|
+
isRestricted: false,
|
|
627
|
+
restrictedUntil: undefined,
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
// ==================== COMPARISON & UTILITY METHODS ====================
|
|
632
|
+
/**
|
|
633
|
+
* Check if this user has the same ID as another user
|
|
634
|
+
*/
|
|
635
|
+
equals(other) {
|
|
636
|
+
return this.id === other.id;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Check if user has complete profile information
|
|
640
|
+
*/
|
|
641
|
+
hasCompleteProfile() {
|
|
642
|
+
return !!(this._metadata.name && this._metadata.phone && this._metadata.birthdate && this._metadata.address && this._metadata.gender);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get profile completion percentage (0-100)
|
|
646
|
+
*/
|
|
647
|
+
getProfileCompletionPercentage() {
|
|
648
|
+
const fields = ['name', 'picture', 'phone', 'gender', 'birthdate', 'address', 'bio'];
|
|
649
|
+
const completedFields = fields.filter((field) => {
|
|
650
|
+
const value = this._metadata[field];
|
|
651
|
+
if (typeof value === 'object' && value !== null) {
|
|
652
|
+
return Object.keys(value).length > 0;
|
|
653
|
+
}
|
|
654
|
+
return !!value;
|
|
655
|
+
}).length;
|
|
656
|
+
return Math.round((completedFields / fields.length) * 100);
|
|
657
|
+
}
|
|
658
|
+
// Add detailed validation with error collection
|
|
659
|
+
validate() {
|
|
660
|
+
const errors = [];
|
|
661
|
+
// Required fields
|
|
662
|
+
if (!this.id)
|
|
663
|
+
errors.push('ID is required');
|
|
664
|
+
if (!this._email)
|
|
665
|
+
errors.push('Email is required');
|
|
666
|
+
if (!this._displayName)
|
|
667
|
+
errors.push('Display name is required');
|
|
668
|
+
if (!this._hash)
|
|
669
|
+
errors.push('Hash is required');
|
|
670
|
+
// Format validation
|
|
671
|
+
try {
|
|
672
|
+
this.validateEmail(this._email);
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
errors.push(`Invalid email format: ${e}`);
|
|
676
|
+
}
|
|
677
|
+
// Date validation
|
|
678
|
+
if (!(this.createdAt instanceof Date) || isNaN(this.createdAt.getTime())) {
|
|
679
|
+
errors.push('Invalid createdAt date');
|
|
680
|
+
}
|
|
681
|
+
if (!(this._lastUpdate instanceof Date) || isNaN(this._lastUpdate.getTime())) {
|
|
682
|
+
errors.push('Invalid lastUpdate date');
|
|
683
|
+
}
|
|
684
|
+
// Metadata validation
|
|
685
|
+
if (this._metadata.phone) {
|
|
686
|
+
if (!/^\+?[1-9]\d{1,14}$/.test(this._metadata.phone)) {
|
|
687
|
+
errors.push('Invalid phone number format');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (this._metadata.birthdate) {
|
|
691
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(this._metadata.birthdate)) {
|
|
692
|
+
errors.push('Invalid birthdate format');
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
isValid: errors.length === 0,
|
|
697
|
+
errors,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Create a shallow clone of the user
|
|
702
|
+
*/
|
|
703
|
+
clone() {
|
|
704
|
+
return new this.constructor({
|
|
705
|
+
id: this.id,
|
|
706
|
+
email: this._email,
|
|
707
|
+
displayName: this._displayName,
|
|
708
|
+
hash: this._hash,
|
|
709
|
+
metadata: { ...this._metadata },
|
|
710
|
+
settings: { ...this._settings },
|
|
711
|
+
createdAt: this.createdAt,
|
|
712
|
+
lastUpdate: this._lastUpdate,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Get a supabase ready version of user data (metadata)
|
|
717
|
+
*/
|
|
718
|
+
toSupabaseJSON() {
|
|
719
|
+
return stripUndefined({
|
|
720
|
+
age: this.age,
|
|
721
|
+
name: this.fullName,
|
|
722
|
+
gender: this.metadata.gender,
|
|
723
|
+
address: this.metadata.address,
|
|
724
|
+
picture: this.metadata.picture,
|
|
725
|
+
birthdate: this.metadata.birthdate,
|
|
726
|
+
full_name: this.fullName,
|
|
727
|
+
bio: this.metadata.bio,
|
|
728
|
+
first_name: this.metadata.name?.first_name,
|
|
729
|
+
family_name: this.metadata.name?.last_name,
|
|
730
|
+
display_name: this.displayName,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get a sanitized version of user data (removes sensitive info)
|
|
735
|
+
*/
|
|
736
|
+
toPublicJSON() {
|
|
737
|
+
return {
|
|
738
|
+
id: this.id,
|
|
739
|
+
displayName: this._displayName,
|
|
740
|
+
picture: this._metadata.picture,
|
|
741
|
+
bio: this._metadata.bio,
|
|
742
|
+
createdAt: this.createdAt.toISOString(),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Serialize user to JSON-compatible object
|
|
747
|
+
*/
|
|
748
|
+
toJSON() {
|
|
749
|
+
return {
|
|
750
|
+
id: this.id,
|
|
751
|
+
email: this._email,
|
|
752
|
+
displayName: this._displayName,
|
|
753
|
+
hash: this._hash,
|
|
754
|
+
metadata: { ...this._metadata },
|
|
755
|
+
settings: { ...this._settings },
|
|
756
|
+
createdAt: this.createdAt.toISOString(),
|
|
757
|
+
lastUpdate: this._lastUpdate.toISOString(),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
// ==================== PROTECTED HELPER METHODS ====================
|
|
761
|
+
/**
|
|
762
|
+
* Updates the lastUpdate timestamp
|
|
763
|
+
*/
|
|
764
|
+
updateTimestamp() {
|
|
765
|
+
this._lastUpdate = new Date();
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Validates email format
|
|
769
|
+
*/
|
|
770
|
+
validateEmail(email) {
|
|
771
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
772
|
+
if (!emailRegex.test(email)) {
|
|
773
|
+
throw new Error('Invalid email format');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// ==================== STATIC CRYPTOGRAPHIC METHODS ====================
|
|
777
|
+
/**
|
|
778
|
+
* Generate a cryptographically secure unique identifier
|
|
779
|
+
*/
|
|
780
|
+
static generateID() {
|
|
781
|
+
try {
|
|
782
|
+
const mnemonic = generateMnemonic(wordlist, 128);
|
|
783
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
784
|
+
const seed32 = new Uint8Array(seed.slice(0, 32));
|
|
785
|
+
const ed = ed25519.generateKeyPairFromSeed(seed32);
|
|
786
|
+
const skCurve = ed2curve.convertSecretKey(ed.secretKey);
|
|
787
|
+
const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
|
|
788
|
+
if (!skCurve || !pkCurve) {
|
|
789
|
+
throw new Error('Failed to convert derived Ed25519 keys to Curve25519');
|
|
790
|
+
}
|
|
791
|
+
const pkCurveBytes = new Uint8Array(pkCurve);
|
|
792
|
+
const publicKey = arrayBufferToBase64(pkCurveBytes.buffer);
|
|
793
|
+
return publicKey;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
throw new Error(`Failed to generate user ID: ${error}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Validate ID format
|
|
801
|
+
*/
|
|
802
|
+
static validateID(id) {
|
|
803
|
+
return /^[A-Za-z0-9+/]+=*$/.test(id) && id.length > 0;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Hash an ID using SHA-256
|
|
807
|
+
*/
|
|
808
|
+
static hashID(id) {
|
|
809
|
+
const hashedID = hash(new TextEncoder().encode(id));
|
|
810
|
+
return arrayToBase64(hashedID);
|
|
811
|
+
}
|
|
812
|
+
}
|