@ursalock/client 0.2.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/dist/index.js ADDED
@@ -0,0 +1,866 @@
1
+ import { ZKCredentials } from '@z-base/zero-knowledge-credentials';
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+
4
+ // src/passkey.ts
5
+
6
+ // src/interfaces/http-client.ts
7
+ var FetchHttpClient = class {
8
+ async fetch(url, options) {
9
+ return fetch(url, options);
10
+ }
11
+ };
12
+
13
+ // src/passkey.ts
14
+ var PasskeyAuth = class _PasskeyAuth {
15
+ options;
16
+ httpClient;
17
+ constructor(options) {
18
+ this.options = {
19
+ serverUrl: options.serverUrl.replace(/\/$/, ""),
20
+ rpName: options.rpName ?? "ursalock",
21
+ httpClient: options.httpClient ?? new FetchHttpClient()
22
+ };
23
+ this.httpClient = this.options.httpClient;
24
+ }
25
+ getName() {
26
+ return "passkey";
27
+ }
28
+ /**
29
+ * Check if passkeys with PRF are supported in this browser
30
+ * Implements IAuthProvider.isSupported
31
+ */
32
+ isSupported() {
33
+ return _PasskeyAuth.isSupported();
34
+ }
35
+ /**
36
+ * Check if passkeys with PRF are supported in this browser (static helper)
37
+ */
38
+ static isSupported() {
39
+ if (typeof window === "undefined") return false;
40
+ if (!window.PublicKeyCredential) return false;
41
+ return true;
42
+ }
43
+ /**
44
+ * Sign up - Register a new passkey
45
+ * Creates a passkey with PRF extension and derives encryption keys
46
+ * Implements IAuthProvider.signUp
47
+ */
48
+ async signUp(options) {
49
+ const opts = options;
50
+ const displayName = opts?.displayName ?? "User";
51
+ if (!_PasskeyAuth.isSupported()) {
52
+ return { success: false, error: "Passkeys not supported in this browser" };
53
+ }
54
+ try {
55
+ await ZKCredentials.registerCredential(
56
+ displayName,
57
+ "cross-platform"
58
+ // Allow platform + cross-platform authenticators
59
+ );
60
+ const credential = await ZKCredentials.discoverCredential();
61
+ const registerRes = await this.httpClient.fetch(
62
+ `${this.options.serverUrl}/auth/zkc/register`,
63
+ {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({
67
+ opaqueId: credential.id,
68
+ displayName
69
+ })
70
+ }
71
+ );
72
+ if (!registerRes.ok) {
73
+ const err = await registerRes.json();
74
+ return { success: false, error: err.message ?? "Failed to register" };
75
+ }
76
+ const result = await registerRes.json();
77
+ return {
78
+ success: true,
79
+ user: result.user,
80
+ token: result.token,
81
+ credential
82
+ };
83
+ } catch (error) {
84
+ if (error instanceof Error) {
85
+ if (error.name === "NotAllowedError" || error.message.includes("aborted")) {
86
+ return { success: false, error: "Registration cancelled" };
87
+ }
88
+ return { success: false, error: error.message };
89
+ }
90
+ return { success: false, error: String(error) };
91
+ }
92
+ }
93
+ /**
94
+ * Legacy method - kept for backward compatibility
95
+ * @deprecated Use signUp() instead
96
+ */
97
+ async register(displayName) {
98
+ return this.signUp({ displayName });
99
+ }
100
+ /**
101
+ * Sign in - Authenticate with an existing passkey
102
+ * Uses discoverCredential to authenticate and derive keys
103
+ * Implements IAuthProvider.signIn
104
+ */
105
+ async signIn(_options) {
106
+ if (!_PasskeyAuth.isSupported()) {
107
+ return { success: false, error: "Passkeys not supported in this browser" };
108
+ }
109
+ try {
110
+ const credential = await ZKCredentials.discoverCredential();
111
+ const authRes = await this.httpClient.fetch(
112
+ `${this.options.serverUrl}/auth/zkc/authenticate`,
113
+ {
114
+ method: "POST",
115
+ headers: { "Content-Type": "application/json" },
116
+ body: JSON.stringify({
117
+ opaqueId: credential.id
118
+ })
119
+ }
120
+ );
121
+ if (!authRes.ok) {
122
+ const err = await authRes.json();
123
+ return { success: false, error: err.message ?? "Authentication failed" };
124
+ }
125
+ const result = await authRes.json();
126
+ return {
127
+ success: true,
128
+ user: result.user,
129
+ token: result.token,
130
+ credential
131
+ };
132
+ } catch (error) {
133
+ if (error instanceof Error) {
134
+ if (error.name === "NotAllowedError" || error.message.includes("aborted")) {
135
+ return { success: false, error: "Authentication cancelled" };
136
+ }
137
+ if (error.message.includes("no-credential")) {
138
+ return { success: false, error: "No passkey found. Please sign up first." };
139
+ }
140
+ return { success: false, error: error.message };
141
+ }
142
+ return { success: false, error: String(error) };
143
+ }
144
+ }
145
+ /**
146
+ * Legacy method - kept for backward compatibility
147
+ * @deprecated Use signIn() instead
148
+ */
149
+ async authenticate() {
150
+ return this.signIn({});
151
+ }
152
+ /**
153
+ * Check if user has any registered passkeys
154
+ */
155
+ async hasPasskey(opaqueId) {
156
+ try {
157
+ const res = await this.httpClient.fetch(
158
+ `${this.options.serverUrl}/auth/zkc/check`,
159
+ {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ opaqueId })
163
+ }
164
+ );
165
+ if (!res.ok) return false;
166
+ const data = await res.json();
167
+ return data.hasPasskey === true;
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+ };
173
+
174
+ // src/email.ts
175
+ var EmailAuth = class {
176
+ options;
177
+ httpClient;
178
+ constructor(options) {
179
+ this.options = {
180
+ serverUrl: options.serverUrl.replace(/\/$/, ""),
181
+ httpClient: options.httpClient ?? new FetchHttpClient()
182
+ };
183
+ this.httpClient = this.options.httpClient;
184
+ }
185
+ getName() {
186
+ return "email";
187
+ }
188
+ isSupported() {
189
+ return true;
190
+ }
191
+ /**
192
+ * Sign up - Register a new account with email/password
193
+ * Implements IAuthProvider.signUp
194
+ */
195
+ async signUp(options) {
196
+ const { email, password } = options;
197
+ if (!email || !this.isValidEmail(email)) {
198
+ return { success: false, error: "Invalid email address" };
199
+ }
200
+ if (!password || password.length < 8) {
201
+ return { success: false, error: "Password must be at least 8 characters" };
202
+ }
203
+ try {
204
+ const res = await this.httpClient.fetch(
205
+ `${this.options.serverUrl}/auth/email/register`,
206
+ {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify({ email, password })
210
+ }
211
+ );
212
+ if (!res.ok) {
213
+ const err = await res.json();
214
+ return {
215
+ success: false,
216
+ error: err.message ?? "Registration failed"
217
+ };
218
+ }
219
+ const result = await res.json();
220
+ return {
221
+ success: true,
222
+ user: result.user,
223
+ token: result.token
224
+ // Email auth doesn't provide encryption keys
225
+ };
226
+ } catch {
227
+ return { success: false, error: "Network error" };
228
+ }
229
+ }
230
+ /**
231
+ * Sign in - Authenticate with email/password
232
+ * Implements IAuthProvider.signIn
233
+ */
234
+ async signIn(options) {
235
+ const { email, password } = options;
236
+ if (!email || !password) {
237
+ return { success: false, error: "Email and password required" };
238
+ }
239
+ try {
240
+ const res = await this.httpClient.fetch(
241
+ `${this.options.serverUrl}/auth/email/login`,
242
+ {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json" },
245
+ body: JSON.stringify({ email, password })
246
+ }
247
+ );
248
+ if (!res.ok) {
249
+ const err = await res.json();
250
+ if (res.status === 401) {
251
+ return { success: false, error: "Invalid email or password" };
252
+ }
253
+ return { success: false, error: err.message ?? "Login failed" };
254
+ }
255
+ const result = await res.json();
256
+ return {
257
+ success: true,
258
+ user: result.user,
259
+ token: result.token
260
+ // Email auth doesn't provide encryption keys
261
+ };
262
+ } catch {
263
+ return { success: false, error: "Network error" };
264
+ }
265
+ }
266
+ /**
267
+ * Legacy method - kept for backward compatibility
268
+ * @deprecated Use signUp() instead
269
+ */
270
+ async register(credentials) {
271
+ return this.signUp(credentials);
272
+ }
273
+ /**
274
+ * Request password reset email
275
+ */
276
+ async forgotPassword(email) {
277
+ if (!this.isValidEmail(email)) {
278
+ return { success: false, error: "Invalid email address" };
279
+ }
280
+ try {
281
+ await this.httpClient.fetch(
282
+ `${this.options.serverUrl}/auth/email/forgot-password`,
283
+ {
284
+ method: "POST",
285
+ headers: { "Content-Type": "application/json" },
286
+ body: JSON.stringify({ email })
287
+ }
288
+ );
289
+ return { success: true };
290
+ } catch {
291
+ return { success: true };
292
+ }
293
+ }
294
+ /**
295
+ * Reset password with token
296
+ */
297
+ async resetPassword(token, newPassword) {
298
+ if (!newPassword || newPassword.length < 8) {
299
+ return { success: false, error: "Password must be at least 8 characters" };
300
+ }
301
+ try {
302
+ const res = await this.httpClient.fetch(
303
+ `${this.options.serverUrl}/auth/email/reset-password`,
304
+ {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json" },
307
+ body: JSON.stringify({ token, password: newPassword })
308
+ }
309
+ );
310
+ if (!res.ok) {
311
+ const err = await res.json();
312
+ return { success: false, error: err.message ?? "Reset failed" };
313
+ }
314
+ return { success: true };
315
+ } catch {
316
+ return { success: false, error: "Network error" };
317
+ }
318
+ }
319
+ /**
320
+ * Change password (when logged in)
321
+ */
322
+ async changePassword(currentPassword, newPassword, authToken) {
323
+ if (!newPassword || newPassword.length < 8) {
324
+ return { success: false, error: "New password must be at least 8 characters" };
325
+ }
326
+ try {
327
+ const res = await this.httpClient.fetch(
328
+ `${this.options.serverUrl}/auth/email/change-password`,
329
+ {
330
+ method: "POST",
331
+ headers: {
332
+ "Content-Type": "application/json",
333
+ "Authorization": `Bearer ${authToken}`
334
+ },
335
+ body: JSON.stringify({ currentPassword, newPassword })
336
+ }
337
+ );
338
+ if (!res.ok) {
339
+ const err = await res.json();
340
+ return { success: false, error: err.message ?? "Change failed" };
341
+ }
342
+ return { success: true };
343
+ } catch {
344
+ return { success: false, error: "Network error" };
345
+ }
346
+ }
347
+ /**
348
+ * Validate email format
349
+ */
350
+ isValidEmail(email) {
351
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
352
+ }
353
+ };
354
+
355
+ // src/token.ts
356
+ var TokenManager = class _TokenManager {
357
+ token = null;
358
+ refreshTimer = null;
359
+ options;
360
+ listeners = /* @__PURE__ */ new Set();
361
+ isRefreshing = false;
362
+ constructor(options = {}) {
363
+ this.options = {
364
+ storageKey: options.storageKey ?? "ursalock:token",
365
+ serverUrl: options.serverUrl,
366
+ onExpire: options.onExpire ?? (() => {
367
+ }),
368
+ refreshBuffer: options.refreshBuffer ?? 5 * 60 * 1e3,
369
+ // 5 minutes
370
+ autoRefresh: options.autoRefresh ?? !!options.serverUrl
371
+ };
372
+ this.loadFromStorage();
373
+ }
374
+ /**
375
+ * Set a new token
376
+ */
377
+ setToken(token) {
378
+ this.token = token;
379
+ this.saveToStorage();
380
+ this.scheduleRefresh();
381
+ this.notifyListeners();
382
+ }
383
+ /**
384
+ * Get current token
385
+ */
386
+ getToken() {
387
+ if (!this.token) return null;
388
+ if (Date.now() >= this.token.expiresAt) {
389
+ this.clearToken();
390
+ return null;
391
+ }
392
+ return this.token;
393
+ }
394
+ /**
395
+ * Get access token string (convenience method)
396
+ */
397
+ getAccessToken() {
398
+ return this.getToken()?.accessToken ?? null;
399
+ }
400
+ /**
401
+ * Check if token is valid
402
+ */
403
+ isValid() {
404
+ const token = this.getToken();
405
+ return token !== null && Date.now() < token.expiresAt;
406
+ }
407
+ /**
408
+ * Clear token
409
+ */
410
+ clearToken() {
411
+ this.token = null;
412
+ this.clearRefreshTimer();
413
+ this.removeFromStorage();
414
+ this.notifyListeners();
415
+ }
416
+ /**
417
+ * Subscribe to token changes
418
+ */
419
+ subscribe(callback) {
420
+ this.listeners.add(callback);
421
+ return () => this.listeners.delete(callback);
422
+ }
423
+ /**
424
+ * Manually refresh the token
425
+ * Returns true if refresh succeeded
426
+ */
427
+ async refresh() {
428
+ if (!this.options.serverUrl || !this.token) return false;
429
+ if (this.isRefreshing) return false;
430
+ this.isRefreshing = true;
431
+ try {
432
+ const res = await fetch(`${this.options.serverUrl}/auth/refresh`, {
433
+ method: "POST",
434
+ headers: {
435
+ "Content-Type": "application/json",
436
+ "Authorization": `Bearer ${this.token.accessToken}`
437
+ },
438
+ body: this.token.refreshToken ? JSON.stringify({ refreshToken: this.token.refreshToken }) : void 0
439
+ });
440
+ if (!res.ok) {
441
+ this.isRefreshing = false;
442
+ return false;
443
+ }
444
+ const data = await res.json();
445
+ const payload = _TokenManager.parseToken(data.token);
446
+ const expiresAt = payload?.exp ? payload.exp * 1e3 : Date.now() + (data.expiresIn ?? 3600) * 1e3;
447
+ this.setToken({
448
+ accessToken: data.token,
449
+ expiresAt,
450
+ refreshToken: this.token.refreshToken
451
+ });
452
+ this.isRefreshing = false;
453
+ return true;
454
+ } catch {
455
+ this.isRefreshing = false;
456
+ return false;
457
+ }
458
+ }
459
+ /**
460
+ * Parse JWT payload (without verification)
461
+ */
462
+ static parseToken(token) {
463
+ try {
464
+ const [, payload] = token.split(".");
465
+ const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
466
+ return JSON.parse(decoded);
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+ // Private methods
472
+ loadFromStorage() {
473
+ if (typeof window === "undefined") return;
474
+ try {
475
+ const stored = localStorage.getItem(this.options.storageKey);
476
+ if (stored) {
477
+ const token = JSON.parse(stored);
478
+ if (Date.now() < token.expiresAt) {
479
+ this.token = token;
480
+ this.scheduleRefresh();
481
+ } else {
482
+ this.removeFromStorage();
483
+ }
484
+ }
485
+ } catch {
486
+ }
487
+ }
488
+ saveToStorage() {
489
+ if (typeof window === "undefined" || !this.token) return;
490
+ try {
491
+ localStorage.setItem(this.options.storageKey, JSON.stringify(this.token));
492
+ } catch {
493
+ }
494
+ }
495
+ removeFromStorage() {
496
+ if (typeof window === "undefined") return;
497
+ try {
498
+ localStorage.removeItem(this.options.storageKey);
499
+ } catch {
500
+ }
501
+ }
502
+ scheduleRefresh() {
503
+ this.clearRefreshTimer();
504
+ if (!this.token) return;
505
+ const timeUntilExpiry = this.token.expiresAt - Date.now();
506
+ const refreshIn = Math.max(0, timeUntilExpiry - this.options.refreshBuffer);
507
+ this.refreshTimer = setTimeout(async () => {
508
+ if (this.options.autoRefresh && this.options.serverUrl) {
509
+ const success = await this.refresh();
510
+ if (success) return;
511
+ }
512
+ this.options.onExpire();
513
+ }, refreshIn);
514
+ }
515
+ clearRefreshTimer() {
516
+ if (this.refreshTimer) {
517
+ clearTimeout(this.refreshTimer);
518
+ this.refreshTimer = null;
519
+ }
520
+ }
521
+ notifyListeners() {
522
+ for (const listener of this.listeners) {
523
+ listener(this.token);
524
+ }
525
+ }
526
+ };
527
+
528
+ // src/client.ts
529
+ var VaultClient = class {
530
+ options;
531
+ passkeyAuth;
532
+ emailAuth;
533
+ tokenManager;
534
+ state;
535
+ listeners = /* @__PURE__ */ new Set();
536
+ constructor(options) {
537
+ this.options = {
538
+ serverUrl: options.serverUrl.replace(/\/$/, ""),
539
+ rpName: options.rpName ?? "ursalock",
540
+ preferPasskey: options.preferPasskey ?? true,
541
+ storageKey: options.storageKey ?? "ursalock:auth"
542
+ };
543
+ this.passkeyAuth = new PasskeyAuth({
544
+ serverUrl: this.options.serverUrl,
545
+ rpName: this.options.rpName
546
+ });
547
+ this.emailAuth = new EmailAuth({
548
+ serverUrl: this.options.serverUrl
549
+ });
550
+ this.tokenManager = new TokenManager({
551
+ storageKey: `${this.options.storageKey}:token`,
552
+ onExpire: () => this.handleTokenExpire()
553
+ });
554
+ this.state = {
555
+ isAuthenticated: false,
556
+ user: null,
557
+ isLoading: true,
558
+ error: null,
559
+ credential: null
560
+ };
561
+ this.initialize();
562
+ }
563
+ // ==================
564
+ // Public Auth Methods
565
+ // ==================
566
+ /**
567
+ * Sign up a new user
568
+ */
569
+ async signUp(options = {}) {
570
+ const usePasskey = options.usePasskey ?? (this.options.preferPasskey && PasskeyAuth.isSupported() && !options.password);
571
+ let result;
572
+ if (usePasskey) {
573
+ result = await this.passkeyAuth.register(options.displayName ?? options.email);
574
+ } else {
575
+ if (!options.email || !options.password) {
576
+ return { success: false, error: "Email and password required" };
577
+ }
578
+ const emailResult = await this.emailAuth.register({
579
+ email: options.email,
580
+ password: options.password
581
+ });
582
+ result = {
583
+ success: emailResult.success,
584
+ user: emailResult.user,
585
+ token: emailResult.token,
586
+ error: emailResult.error
587
+ };
588
+ }
589
+ if (result.success && result.token) {
590
+ this.handleAuthSuccess(result);
591
+ }
592
+ return result;
593
+ }
594
+ /**
595
+ * Sign in an existing user
596
+ */
597
+ async signIn(options = {}) {
598
+ const usePasskey = options.usePasskey ?? (this.options.preferPasskey && PasskeyAuth.isSupported() && !options.password);
599
+ let result;
600
+ if (usePasskey) {
601
+ result = await this.passkeyAuth.authenticate();
602
+ } else {
603
+ if (!options.email || !options.password) {
604
+ return { success: false, error: "Email and password required" };
605
+ }
606
+ const emailResult = await this.emailAuth.signIn({
607
+ email: options.email,
608
+ password: options.password
609
+ });
610
+ result = {
611
+ success: emailResult.success,
612
+ user: emailResult.user,
613
+ token: emailResult.token,
614
+ error: emailResult.error
615
+ };
616
+ }
617
+ if (result.success && result.token) {
618
+ this.handleAuthSuccess(result);
619
+ }
620
+ return result;
621
+ }
622
+ /**
623
+ * Sign out
624
+ */
625
+ async signOut() {
626
+ const token = this.tokenManager.getAccessToken();
627
+ if (token) {
628
+ try {
629
+ await fetch(`${this.options.serverUrl}/auth/logout`, {
630
+ method: "POST",
631
+ headers: { "Authorization": `Bearer ${token}` }
632
+ });
633
+ } catch {
634
+ }
635
+ }
636
+ this.tokenManager.clearToken();
637
+ this.clearUserFromStorage();
638
+ this.updateState({
639
+ isAuthenticated: false,
640
+ user: null,
641
+ isLoading: false,
642
+ error: null,
643
+ credential: null
644
+ });
645
+ }
646
+ /**
647
+ * Check if passkeys are supported
648
+ */
649
+ supportsPasskey() {
650
+ return PasskeyAuth.isSupported();
651
+ }
652
+ // ==================
653
+ // State Management
654
+ // ==================
655
+ /**
656
+ * Get current auth state
657
+ * Returns the same reference unless state changes (required for useSyncExternalStore)
658
+ */
659
+ getState() {
660
+ return this.state;
661
+ }
662
+ /**
663
+ * Get current user
664
+ */
665
+ getUser() {
666
+ return this.state.user;
667
+ }
668
+ /**
669
+ * Get current ZK credential (with encryption keys)
670
+ */
671
+ getCredential() {
672
+ return this.state.credential;
673
+ }
674
+ /**
675
+ * Check if authenticated
676
+ */
677
+ isAuthenticated() {
678
+ return this.state.isAuthenticated;
679
+ }
680
+ /**
681
+ * Subscribe to auth state changes
682
+ */
683
+ subscribe(callback) {
684
+ this.listeners.add(callback);
685
+ callback(this.state);
686
+ return () => this.listeners.delete(callback);
687
+ }
688
+ // ==================
689
+ // API Methods
690
+ // ==================
691
+ /**
692
+ * Get authorization header
693
+ */
694
+ getAuthHeader() {
695
+ const token = this.tokenManager.getAccessToken();
696
+ return token ? { "Authorization": `Bearer ${token}` } : {};
697
+ }
698
+ /**
699
+ * Make authenticated API request
700
+ */
701
+ async fetch(path, options = {}) {
702
+ const url = path.startsWith("http") ? path : `${this.options.serverUrl}${path}`;
703
+ return fetch(url, {
704
+ ...options,
705
+ headers: {
706
+ ...this.getAuthHeader(),
707
+ ...options.headers
708
+ }
709
+ });
710
+ }
711
+ // ==================
712
+ // Private Methods
713
+ // ==================
714
+ async initialize() {
715
+ if (this.tokenManager.isValid()) {
716
+ try {
717
+ const res = await this.fetch("/auth/me");
718
+ if (res.ok) {
719
+ const data = await res.json();
720
+ this.updateState({
721
+ isAuthenticated: true,
722
+ user: data.user,
723
+ isLoading: false,
724
+ error: null,
725
+ credential: null
726
+ // Will need to re-authenticate to get credential
727
+ });
728
+ return;
729
+ } else {
730
+ this.tokenManager.clearToken();
731
+ this.clearUserFromStorage();
732
+ }
733
+ } catch {
734
+ const user = this.loadUserFromStorage();
735
+ if (user) {
736
+ this.updateState({
737
+ isAuthenticated: true,
738
+ user,
739
+ isLoading: false,
740
+ error: null,
741
+ credential: null
742
+ });
743
+ return;
744
+ }
745
+ }
746
+ }
747
+ this.tokenManager.clearToken();
748
+ this.clearUserFromStorage();
749
+ this.updateState({
750
+ isAuthenticated: false,
751
+ user: null,
752
+ isLoading: false,
753
+ error: null,
754
+ credential: null
755
+ });
756
+ }
757
+ handleAuthSuccess(result) {
758
+ if (!result.token || !result.user) return;
759
+ const payload = TokenManager.parseToken(result.token);
760
+ const expiresAt = payload?.exp ? payload.exp * 1e3 : Date.now() + 7 * 24 * 60 * 60 * 1e3;
761
+ this.tokenManager.setToken({
762
+ accessToken: result.token,
763
+ expiresAt
764
+ });
765
+ this.saveUserToStorage(result.user);
766
+ this.updateState({
767
+ isAuthenticated: true,
768
+ user: result.user,
769
+ isLoading: false,
770
+ error: null,
771
+ credential: result.credential ?? null
772
+ });
773
+ }
774
+ handleTokenExpire() {
775
+ this.updateState({
776
+ isAuthenticated: false,
777
+ user: null,
778
+ isLoading: false,
779
+ error: new Error("Session expired"),
780
+ credential: null
781
+ });
782
+ }
783
+ updateState(newState) {
784
+ this.state = newState;
785
+ for (const listener of this.listeners) {
786
+ listener(this.state);
787
+ }
788
+ }
789
+ saveUserToStorage(user) {
790
+ if (typeof window === "undefined") return;
791
+ try {
792
+ localStorage.setItem(`${this.options.storageKey}:user`, JSON.stringify(user));
793
+ } catch {
794
+ }
795
+ }
796
+ loadUserFromStorage() {
797
+ if (typeof window === "undefined") return null;
798
+ try {
799
+ const stored = localStorage.getItem(`${this.options.storageKey}:user`);
800
+ return stored ? JSON.parse(stored) : null;
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+ clearUserFromStorage() {
806
+ if (typeof window === "undefined") return;
807
+ try {
808
+ localStorage.removeItem(`${this.options.storageKey}:user`);
809
+ } catch {
810
+ }
811
+ }
812
+ };
813
+ function useAuth(client) {
814
+ const subscribe = useCallback(
815
+ (callback) => {
816
+ return client.subscribe(callback);
817
+ },
818
+ [client]
819
+ );
820
+ const getSnapshot = useCallback(() => client.getState(), [client]);
821
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
822
+ }
823
+ function useSignUp(client) {
824
+ const state = useAuth(client);
825
+ const signUp = useCallback(
826
+ async (options) => {
827
+ return client.signUp(options ?? {});
828
+ },
829
+ [client]
830
+ );
831
+ return {
832
+ signUp,
833
+ isLoading: state.isLoading,
834
+ error: state.error
835
+ };
836
+ }
837
+ function useSignIn(client) {
838
+ const state = useAuth(client);
839
+ const signIn = useCallback(
840
+ async (options) => {
841
+ return client.signIn(options ?? {});
842
+ },
843
+ [client]
844
+ );
845
+ return {
846
+ signIn,
847
+ isLoading: state.isLoading,
848
+ error: state.error
849
+ };
850
+ }
851
+ function useSignOut(client) {
852
+ return useCallback(() => client.signOut(), [client]);
853
+ }
854
+ function useUser(client) {
855
+ const state = useAuth(client);
856
+ return state.user;
857
+ }
858
+ function useCredential(client) {
859
+ const state = useAuth(client);
860
+ return state.credential;
861
+ }
862
+ function usePasskeySupport(client) {
863
+ return client.supportsPasskey();
864
+ }
865
+
866
+ export { EmailAuth, FetchHttpClient, PasskeyAuth, TokenManager, VaultClient, useAuth, useCredential, usePasskeySupport, useSignIn, useSignOut, useSignUp, useUser };