@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.
@@ -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
+ }