@nauth-toolkit/client-angular 0.1.53

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,1211 @@
1
+ import { NAuthErrorCode, NAuthClientError, NAuthClient } from '@nauth-toolkit/client';
2
+ export * from '@nauth-toolkit/client';
3
+ import * as i0 from '@angular/core';
4
+ import { InjectionToken, inject, Injectable, Optional, Inject, PLATFORM_ID, NgModule } from '@angular/core';
5
+ import { firstValueFrom, BehaviorSubject, Subject, catchError, throwError, from, switchMap, filter as filter$1, take } from 'rxjs';
6
+ import { filter } from 'rxjs/operators';
7
+ import { HttpClient, HttpErrorResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
8
+ import { isPlatformBrowser } from '@angular/common';
9
+ import { Router } from '@angular/router';
10
+
11
+ /**
12
+ * Injection token for providing NAuthClientConfig in Angular apps.
13
+ */
14
+ const NAUTH_CLIENT_CONFIG = new InjectionToken('NAUTH_CLIENT_CONFIG');
15
+
16
+ /**
17
+ * HTTP adapter for Angular using HttpClient.
18
+ *
19
+ * This adapter:
20
+ * - Uses Angular's HttpClient for all requests
21
+ * - Works with Angular's HTTP interceptors (including authInterceptor)
22
+ * - Auto-provided via Angular DI (providedIn: 'root')
23
+ * - Converts HttpClient responses to HttpResponse format
24
+ * - Converts HttpErrorResponse to NAuthClientError
25
+ *
26
+ * Users don't need to configure this manually - it's automatically
27
+ * injected when using AuthService in Angular apps.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Automatic usage (no manual setup needed)
32
+ * // AuthService automatically injects AngularHttpAdapter
33
+ * constructor(private auth: AuthService) {}
34
+ * ```
35
+ */
36
+ class AngularHttpAdapter {
37
+ http = inject(HttpClient);
38
+ /**
39
+ * Execute HTTP request using Angular's HttpClient.
40
+ *
41
+ * @param config - Request configuration
42
+ * @returns Response with parsed data
43
+ * @throws NAuthClientError if request fails
44
+ */
45
+ async request(config) {
46
+ try {
47
+ // Use Angular's HttpClient - goes through ALL interceptors
48
+ const data = await firstValueFrom(this.http.request(config.method, config.url, {
49
+ body: config.body,
50
+ headers: config.headers,
51
+ withCredentials: config.credentials === 'include',
52
+ observe: 'body', // Only return body data
53
+ }));
54
+ return {
55
+ data,
56
+ status: 200, // HttpClient only returns data on success
57
+ headers: {}, // Can extract from observe: 'response' if needed
58
+ };
59
+ }
60
+ catch (error) {
61
+ if (error instanceof HttpErrorResponse) {
62
+ // Convert Angular's HttpErrorResponse to NAuthClientError
63
+ const errorData = error.error || {};
64
+ const code = typeof errorData['code'] === 'string' ? errorData.code : NAuthErrorCode.INTERNAL_ERROR;
65
+ const message = typeof errorData['message'] === 'string'
66
+ ? errorData.message
67
+ : error.message || `Request failed with status ${error.status}`;
68
+ const timestamp = typeof errorData['timestamp'] === 'string' ? errorData.timestamp : undefined;
69
+ const details = errorData['details'];
70
+ throw new NAuthClientError(code, message, {
71
+ statusCode: error.status,
72
+ timestamp,
73
+ details,
74
+ isNetworkError: error.status === 0, // Network error (no response from server)
75
+ });
76
+ }
77
+ // Re-throw non-HTTP errors
78
+ throw error;
79
+ }
80
+ }
81
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AngularHttpAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
82
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AngularHttpAdapter, providedIn: 'root' });
83
+ }
84
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AngularHttpAdapter, decorators: [{
85
+ type: Injectable,
86
+ args: [{ providedIn: 'root' }]
87
+ }] });
88
+
89
+ /**
90
+ * Angular wrapper around NAuthClient that provides promise-based auth methods and reactive state.
91
+ *
92
+ * This service provides:
93
+ * - Reactive state (currentUser$, isAuthenticated$, challenge$)
94
+ * - All core auth methods as Promises (login, signup, logout, refresh)
95
+ * - Profile management (getProfile, updateProfile, changePassword)
96
+ * - Challenge flow methods (respondToChallenge, resendCode)
97
+ * - MFA management (getMfaStatus, setupMfaDevice, etc.)
98
+ * - Social authentication and account linking
99
+ * - Device trust management
100
+ * - Audit history
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * constructor(private auth: AuthService) {}
105
+ *
106
+ * // Reactive state
107
+ * this.auth.currentUser$.subscribe(user => ...);
108
+ * this.auth.isAuthenticated$.subscribe(isAuth => ...);
109
+ *
110
+ * // Auth operations with async/await
111
+ * const response = await this.auth.login(email, password);
112
+ *
113
+ * // Profile management
114
+ * await this.auth.changePassword(oldPassword, newPassword);
115
+ * const user = await this.auth.updateProfile({ firstName: 'John' });
116
+ *
117
+ * // MFA operations
118
+ * const status = await this.auth.getMfaStatus();
119
+ * ```
120
+ */
121
+ class AuthService {
122
+ client;
123
+ config;
124
+ currentUserSubject = new BehaviorSubject(null);
125
+ isAuthenticatedSubject = new BehaviorSubject(false);
126
+ challengeSubject = new BehaviorSubject(null);
127
+ authEventsSubject = new Subject();
128
+ initialized = false;
129
+ /**
130
+ * @param config - Injected client configuration
131
+ *
132
+ * Note: AngularHttpAdapter is automatically injected via Angular DI.
133
+ * This ensures all requests go through Angular's HttpClient and interceptors.
134
+ */
135
+ constructor(config) {
136
+ if (!config) {
137
+ throw new Error('NAUTH_CLIENT_CONFIG is required to initialize AuthService');
138
+ }
139
+ this.config = config;
140
+ // Auto-inject AngularHttpAdapter (or use provided one)
141
+ const httpAdapter = config.httpAdapter ?? inject(AngularHttpAdapter);
142
+ this.client = new NAuthClient({
143
+ ...config,
144
+ httpAdapter, // Automatically use Angular's HttpClient
145
+ onAuthStateChange: (user) => {
146
+ this.currentUserSubject.next(user);
147
+ this.isAuthenticatedSubject.next(Boolean(user));
148
+ config.onAuthStateChange?.(user);
149
+ },
150
+ });
151
+ // Forward all client events to Observable stream
152
+ this.client.on('*', (event) => {
153
+ this.authEventsSubject.next(event);
154
+ });
155
+ // Auto-initialize on construction (hydrate from storage)
156
+ this.initialize();
157
+ }
158
+ // ============================================================================
159
+ // Reactive State Observables
160
+ // ============================================================================
161
+ /**
162
+ * Current user observable.
163
+ */
164
+ get currentUser$() {
165
+ return this.currentUserSubject.asObservable();
166
+ }
167
+ /**
168
+ * Authenticated state observable.
169
+ */
170
+ get isAuthenticated$() {
171
+ return this.isAuthenticatedSubject.asObservable();
172
+ }
173
+ /**
174
+ * Current challenge observable (for reactive challenge navigation).
175
+ */
176
+ get challenge$() {
177
+ return this.challengeSubject.asObservable();
178
+ }
179
+ /**
180
+ * Authentication events stream.
181
+ * Emits all auth lifecycle events for custom logic, analytics, or UI updates.
182
+ */
183
+ get authEvents$() {
184
+ return this.authEventsSubject.asObservable();
185
+ }
186
+ /**
187
+ * Successful authentication events stream.
188
+ * Emits when user successfully authenticates (login, signup, social auth).
189
+ */
190
+ get authSuccess$() {
191
+ return this.authEventsSubject.pipe(filter((e) => e.type === 'auth:success'));
192
+ }
193
+ /**
194
+ * Authentication error events stream.
195
+ * Emits when authentication fails (login error, OAuth error, etc.).
196
+ */
197
+ get authError$() {
198
+ return this.authEventsSubject.pipe(filter((e) => e.type === 'auth:error' || e.type === 'oauth:error'));
199
+ }
200
+ // ============================================================================
201
+ // Sync State Accessors (for guards, templates)
202
+ // ============================================================================
203
+ /**
204
+ * Check if authenticated (sync, uses cached state).
205
+ */
206
+ isAuthenticated() {
207
+ return this.client.isAuthenticatedSync();
208
+ }
209
+ /**
210
+ * Get current user (sync, uses cached state).
211
+ */
212
+ getCurrentUser() {
213
+ return this.client.getCurrentUser();
214
+ }
215
+ /**
216
+ * Get current challenge (sync).
217
+ */
218
+ getCurrentChallenge() {
219
+ return this.challengeSubject.value;
220
+ }
221
+ // ============================================================================
222
+ // Core Auth Methods
223
+ // ============================================================================
224
+ /**
225
+ * Login with identifier and password.
226
+ *
227
+ * @param identifier - User email or username
228
+ * @param password - User password
229
+ * @returns Promise with auth response or challenge
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * const response = await this.auth.login('user@example.com', 'password');
234
+ * if (response.challengeName) {
235
+ * // Handle challenge
236
+ * } else {
237
+ * // Login successful
238
+ * }
239
+ * ```
240
+ */
241
+ async login(identifier, password) {
242
+ const res = await this.client.login(identifier, password);
243
+ return this.updateChallengeState(res);
244
+ }
245
+ /**
246
+ * Signup with credentials.
247
+ *
248
+ * @param payload - Signup request payload
249
+ * @returns Promise with auth response or challenge
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * const response = await this.auth.signup({
254
+ * email: 'new@example.com',
255
+ * password: 'SecurePass123!',
256
+ * firstName: 'John',
257
+ * });
258
+ * ```
259
+ */
260
+ async signup(payload) {
261
+ const res = await this.client.signup(payload);
262
+ return this.updateChallengeState(res);
263
+ }
264
+ /**
265
+ * Logout current session.
266
+ *
267
+ * @param forgetDevice - If true, removes device trust
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * await this.auth.logout();
272
+ * ```
273
+ */
274
+ async logout(forgetDevice) {
275
+ await this.client.logout(forgetDevice);
276
+ this.challengeSubject.next(null);
277
+ // Explicitly update auth state after logout
278
+ this.currentUserSubject.next(null);
279
+ this.isAuthenticatedSubject.next(false);
280
+ // Clear CSRF token cookie if in cookies mode
281
+ // Note: Backend should clear httpOnly cookies, but we clear non-httpOnly ones
282
+ if (this.config.tokenDelivery === 'cookies' && typeof document !== 'undefined') {
283
+ const csrfCookieName = this.config.csrf?.cookieName ?? 'nauth_csrf_token';
284
+ // Extract domain from baseUrl if possible
285
+ try {
286
+ const url = new URL(this.config.baseUrl);
287
+ document.cookie = `${csrfCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${url.hostname}`;
288
+ // Also try without domain (for localhost)
289
+ document.cookie = `${csrfCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
290
+ }
291
+ catch {
292
+ // Fallback if baseUrl parsing fails
293
+ document.cookie = `${csrfCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
294
+ }
295
+ }
296
+ }
297
+ /**
298
+ * Logout all sessions.
299
+ *
300
+ * Revokes all active sessions for the current user across all devices.
301
+ * Optionally revokes all trusted devices if forgetDevices is true.
302
+ *
303
+ * @param forgetDevices - If true, also revokes all trusted devices (default: false)
304
+ * @returns Promise with number of sessions revoked
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * const result = await this.auth.logoutAll();
309
+ * console.log(`Revoked ${result.revokedCount} sessions`);
310
+ * ```
311
+ */
312
+ async logoutAll(forgetDevices) {
313
+ const res = await this.client.logoutAll(forgetDevices);
314
+ this.challengeSubject.next(null);
315
+ // Explicitly update auth state after logout
316
+ this.currentUserSubject.next(null);
317
+ this.isAuthenticatedSubject.next(false);
318
+ return res;
319
+ }
320
+ /**
321
+ * Refresh tokens.
322
+ *
323
+ * @returns Promise with new tokens
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * const tokens = await this.auth.refresh();
328
+ * ```
329
+ */
330
+ async refresh() {
331
+ return this.client.refreshTokens();
332
+ }
333
+ // ============================================================================
334
+ // Account Recovery (Forgot Password)
335
+ // ============================================================================
336
+ /**
337
+ * Request a password reset code (forgot password).
338
+ *
339
+ * @param identifier - User email, username, or phone
340
+ * @returns Promise with password reset response
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * await this.auth.forgotPassword('user@example.com');
345
+ * ```
346
+ */
347
+ async forgotPassword(identifier) {
348
+ return this.client.forgotPassword(identifier);
349
+ }
350
+ /**
351
+ * Confirm a password reset code and set a new password.
352
+ *
353
+ * @param identifier - User email, username, or phone
354
+ * @param code - One-time reset code
355
+ * @param newPassword - New password
356
+ * @returns Promise with confirmation response
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * await this.auth.confirmForgotPassword('user@example.com', '123456', 'NewPass123!');
361
+ * ```
362
+ */
363
+ async confirmForgotPassword(identifier, code, newPassword) {
364
+ return this.client.confirmForgotPassword(identifier, code, newPassword);
365
+ }
366
+ /**
367
+ * Change user password (requires current password).
368
+ *
369
+ * @param oldPassword - Current password
370
+ * @param newPassword - New password (must meet requirements)
371
+ * @returns Promise that resolves when password is changed
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * await this.auth.changePassword('oldPassword123', 'newSecurePassword456!');
376
+ * ```
377
+ */
378
+ async changePassword(oldPassword, newPassword) {
379
+ return this.client.changePassword(oldPassword, newPassword);
380
+ }
381
+ /**
382
+ * Request password change (must change on next login).
383
+ *
384
+ * @returns Promise that resolves when request is sent
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * await this.auth.requestPasswordChange();
389
+ * ```
390
+ */
391
+ async requestPasswordChange() {
392
+ return this.client.requestPasswordChange();
393
+ }
394
+ // ============================================================================
395
+ // Profile Management
396
+ // ============================================================================
397
+ /**
398
+ * Get current user profile.
399
+ *
400
+ * @returns Promise of current user profile
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * const user = await this.auth.getProfile();
405
+ * console.log('User profile:', user);
406
+ * ```
407
+ */
408
+ async getProfile() {
409
+ const user = await this.client.getProfile();
410
+ // Update local state when profile is fetched
411
+ this.currentUserSubject.next(user);
412
+ return user;
413
+ }
414
+ /**
415
+ * Update user profile.
416
+ *
417
+ * @param updates - Profile fields to update
418
+ * @returns Promise of updated user profile
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * const user = await this.auth.updateProfile({ firstName: 'John', lastName: 'Doe' });
423
+ * console.log('Profile updated:', user);
424
+ * ```
425
+ */
426
+ async updateProfile(updates) {
427
+ const user = await this.client.updateProfile(updates);
428
+ // Update local state when profile is updated
429
+ this.currentUserSubject.next(user);
430
+ return user;
431
+ }
432
+ // ============================================================================
433
+ // Challenge Flow Methods (Essential for any auth flow)
434
+ // ============================================================================
435
+ /**
436
+ * Respond to a challenge (VERIFY_EMAIL, VERIFY_PHONE, MFA_REQUIRED, etc.).
437
+ *
438
+ * @param response - Challenge response data
439
+ * @returns Promise with auth response or next challenge
440
+ *
441
+ * @example
442
+ * ```typescript
443
+ * const result = await this.auth.respondToChallenge({
444
+ * session: challengeSession,
445
+ * type: 'VERIFY_EMAIL',
446
+ * code: '123456',
447
+ * });
448
+ * ```
449
+ */
450
+ async respondToChallenge(response) {
451
+ const res = await this.client.respondToChallenge(response);
452
+ return this.updateChallengeState(res);
453
+ }
454
+ /**
455
+ * Resend challenge code.
456
+ *
457
+ * @param session - Challenge session token
458
+ * @returns Promise with destination information
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * const result = await this.auth.resendCode(session);
463
+ * console.log('Code sent to:', result.destination);
464
+ * ```
465
+ */
466
+ async resendCode(session) {
467
+ return this.client.resendCode(session);
468
+ }
469
+ /**
470
+ * Get MFA setup data (for MFA_SETUP_REQUIRED challenge).
471
+ *
472
+ * Returns method-specific setup information:
473
+ * - TOTP: { secret, qrCode, manualEntryKey }
474
+ * - SMS: { maskedPhone }
475
+ * - Email: { maskedEmail }
476
+ * - Passkey: WebAuthn registration options
477
+ *
478
+ * @param session - Challenge session token
479
+ * @param method - MFA method to set up
480
+ * @returns Promise of setup data response
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * const setupData = await this.auth.getSetupData(session, 'totp');
485
+ * console.log('QR Code:', setupData.setupData.qrCode);
486
+ * ```
487
+ */
488
+ async getSetupData(session, method) {
489
+ return this.client.getSetupData(session, method);
490
+ }
491
+ /**
492
+ * Get MFA challenge data (for MFA_REQUIRED challenge - e.g., passkey options).
493
+ *
494
+ * @param session - Challenge session token
495
+ * @param method - Challenge method
496
+ * @returns Promise of challenge data response
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * const challengeData = await this.auth.getChallengeData(session, 'passkey');
501
+ * ```
502
+ */
503
+ async getChallengeData(session, method) {
504
+ return this.client.getChallengeData(session, method);
505
+ }
506
+ /**
507
+ * Clear stored challenge (when navigating away from challenge flow).
508
+ *
509
+ * @returns Promise that resolves when challenge is cleared
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * await this.auth.clearChallenge();
514
+ * ```
515
+ */
516
+ async clearChallenge() {
517
+ await this.client.clearStoredChallenge();
518
+ this.challengeSubject.next(null);
519
+ }
520
+ // ============================================================================
521
+ // Social Authentication
522
+ // ============================================================================
523
+ /**
524
+ * Initiate social OAuth login flow.
525
+ * Redirects the browser to backend `/auth/social/:provider/redirect`.
526
+ *
527
+ * @param provider - Social provider ('google', 'apple', 'facebook')
528
+ * @param options - Optional redirect options
529
+ * @returns Promise that resolves when redirect starts
530
+ *
531
+ * @example
532
+ * ```typescript
533
+ * await this.auth.loginWithSocial('google', { returnTo: '/auth/callback' });
534
+ * ```
535
+ */
536
+ async loginWithSocial(provider, options) {
537
+ return this.client.loginWithSocial(provider, options);
538
+ }
539
+ /**
540
+ * Exchange an exchangeToken (from redirect callback URL) into an AuthResponse.
541
+ *
542
+ * Used for `tokenDelivery: 'json'` or hybrid flows where the backend redirects back
543
+ * with `exchangeToken` instead of setting cookies.
544
+ *
545
+ * @param exchangeToken - One-time exchange token from the callback URL
546
+ * @returns Promise of AuthResponse
547
+ *
548
+ * @example
549
+ * ```typescript
550
+ * const response = await this.auth.exchangeSocialRedirect(exchangeToken);
551
+ * ```
552
+ */
553
+ async exchangeSocialRedirect(exchangeToken) {
554
+ const res = await this.client.exchangeSocialRedirect(exchangeToken);
555
+ return this.updateChallengeState(res);
556
+ }
557
+ /**
558
+ * Verify native social token (mobile).
559
+ *
560
+ * @param request - Social verification request with provider and token
561
+ * @returns Promise of AuthResponse
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * const result = await this.auth.verifyNativeSocial({
566
+ * provider: 'google',
567
+ * idToken: nativeIdToken,
568
+ * });
569
+ * ```
570
+ */
571
+ async verifyNativeSocial(request) {
572
+ const res = await this.client.verifyNativeSocial(request);
573
+ return this.updateChallengeState(res);
574
+ }
575
+ /**
576
+ * Get linked social accounts.
577
+ *
578
+ * @returns Promise of linked accounts response
579
+ *
580
+ * @example
581
+ * ```typescript
582
+ * const accounts = await this.auth.getLinkedAccounts();
583
+ * console.log('Linked providers:', accounts.providers);
584
+ * ```
585
+ */
586
+ async getLinkedAccounts() {
587
+ return this.client.getLinkedAccounts();
588
+ }
589
+ /**
590
+ * Link social account.
591
+ *
592
+ * @param provider - Social provider to link
593
+ * @param code - OAuth authorization code
594
+ * @param state - OAuth state parameter
595
+ * @returns Promise with success message
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * await this.auth.linkSocialAccount('google', code, state);
600
+ * ```
601
+ */
602
+ async linkSocialAccount(provider, code, state) {
603
+ return this.client.linkSocialAccount(provider, code, state);
604
+ }
605
+ /**
606
+ * Unlink social account.
607
+ *
608
+ * @param provider - Social provider to unlink
609
+ * @returns Promise with success message
610
+ *
611
+ * @example
612
+ * ```typescript
613
+ * await this.auth.unlinkSocialAccount('google');
614
+ * ```
615
+ */
616
+ async unlinkSocialAccount(provider) {
617
+ return this.client.unlinkSocialAccount(provider);
618
+ }
619
+ // ============================================================================
620
+ // MFA Management
621
+ // ============================================================================
622
+ /**
623
+ * Get MFA status for the current user.
624
+ *
625
+ * @returns Promise of MFA status
626
+ *
627
+ * @example
628
+ * ```typescript
629
+ * const status = await this.auth.getMfaStatus();
630
+ * console.log('MFA enabled:', status.enabled);
631
+ * ```
632
+ */
633
+ async getMfaStatus() {
634
+ return this.client.getMfaStatus();
635
+ }
636
+ /**
637
+ * Get MFA devices for the current user.
638
+ *
639
+ * @returns Promise of MFA devices array
640
+ *
641
+ * @example
642
+ * ```typescript
643
+ * const devices = await this.auth.getMfaDevices();
644
+ * ```
645
+ */
646
+ async getMfaDevices() {
647
+ return this.client.getMfaDevices();
648
+ }
649
+ /**
650
+ * Setup MFA device (authenticated user).
651
+ *
652
+ * @param method - MFA method to set up
653
+ * @returns Promise of setup data
654
+ *
655
+ * @example
656
+ * ```typescript
657
+ * const setupData = await this.auth.setupMfaDevice('totp');
658
+ * ```
659
+ */
660
+ async setupMfaDevice(method) {
661
+ return this.client.setupMfaDevice(method);
662
+ }
663
+ /**
664
+ * Verify MFA setup (authenticated user).
665
+ *
666
+ * @param method - MFA method
667
+ * @param setupData - Setup data from setupMfaDevice
668
+ * @param deviceName - Optional device name
669
+ * @returns Promise with device ID
670
+ *
671
+ * @example
672
+ * ```typescript
673
+ * const result = await this.auth.verifyMfaSetup('totp', { code: '123456' }, 'My Phone');
674
+ * ```
675
+ */
676
+ async verifyMfaSetup(method, setupData, deviceName) {
677
+ return this.client.verifyMfaSetup(method, setupData, deviceName);
678
+ }
679
+ /**
680
+ * Remove MFA device.
681
+ *
682
+ * @param method - MFA method to remove
683
+ * @returns Promise with success message
684
+ *
685
+ * @example
686
+ * ```typescript
687
+ * await this.auth.removeMfaDevice('sms');
688
+ * ```
689
+ */
690
+ async removeMfaDevice(method) {
691
+ return this.client.removeMfaDevice(method);
692
+ }
693
+ /**
694
+ * Set preferred MFA method.
695
+ *
696
+ * @param method - Device method to set as preferred ('totp', 'sms', 'email', or 'passkey')
697
+ * @returns Promise with success message
698
+ *
699
+ * @example
700
+ * ```typescript
701
+ * await this.auth.setPreferredMfaMethod('totp');
702
+ * ```
703
+ */
704
+ async setPreferredMfaMethod(method) {
705
+ return this.client.setPreferredMfaMethod(method);
706
+ }
707
+ /**
708
+ * Generate backup codes.
709
+ *
710
+ * @returns Promise of backup codes array
711
+ *
712
+ * @example
713
+ * ```typescript
714
+ * const codes = await this.auth.generateBackupCodes();
715
+ * console.log('Backup codes:', codes);
716
+ * ```
717
+ */
718
+ async generateBackupCodes() {
719
+ return this.client.generateBackupCodes();
720
+ }
721
+ /**
722
+ * Set MFA exemption (admin/test scenarios).
723
+ *
724
+ * @param exempt - Whether to exempt user from MFA
725
+ * @param reason - Optional reason for exemption
726
+ * @returns Promise that resolves when exemption is set
727
+ *
728
+ * @example
729
+ * ```typescript
730
+ * await this.auth.setMfaExemption(true, 'Test account');
731
+ * ```
732
+ */
733
+ async setMfaExemption(exempt, reason) {
734
+ return this.client.setMfaExemption(exempt, reason);
735
+ }
736
+ // ============================================================================
737
+ // Device Trust
738
+ // ============================================================================
739
+ /**
740
+ * Trust current device.
741
+ *
742
+ * @returns Promise with device token
743
+ *
744
+ * @example
745
+ * ```typescript
746
+ * const result = await this.auth.trustDevice();
747
+ * console.log('Device trusted:', result.deviceToken);
748
+ * ```
749
+ */
750
+ async trustDevice() {
751
+ return this.client.trustDevice();
752
+ }
753
+ /**
754
+ * Check if the current device is trusted.
755
+ *
756
+ * @returns Promise with trusted status
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * const result = await this.auth.isTrustedDevice();
761
+ * if (result.trusted) {
762
+ * console.log('This device is trusted');
763
+ * }
764
+ * ```
765
+ */
766
+ async isTrustedDevice() {
767
+ return this.client.isTrustedDevice();
768
+ }
769
+ // ============================================================================
770
+ // Audit History
771
+ // ============================================================================
772
+ /**
773
+ * Get paginated audit history for the current user.
774
+ *
775
+ * @param params - Query parameters for filtering and pagination
776
+ * @returns Promise of audit history response
777
+ *
778
+ * @example
779
+ * ```typescript
780
+ * const history = await this.auth.getAuditHistory({
781
+ * page: 1,
782
+ * limit: 20,
783
+ * eventType: 'LOGIN_SUCCESS'
784
+ * });
785
+ * console.log('Audit history:', history);
786
+ * ```
787
+ */
788
+ async getAuditHistory(params) {
789
+ return this.client.getAuditHistory(params);
790
+ }
791
+ // ============================================================================
792
+ // Escape Hatch
793
+ // ============================================================================
794
+ /**
795
+ * Expose underlying NAuthClient for advanced scenarios.
796
+ *
797
+ * @deprecated All core functionality is now exposed directly on AuthService as Promises.
798
+ * Use the direct methods on AuthService instead (e.g., `auth.changePassword()` instead of `auth.getClient().changePassword()`).
799
+ * This method is kept for backward compatibility only and may be removed in a future version.
800
+ *
801
+ * @returns The underlying NAuthClient instance
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * // Deprecated - use direct methods instead
806
+ * const status = await this.auth.getClient().getMfaStatus();
807
+ *
808
+ * // Preferred - use direct methods
809
+ * const status = await this.auth.getMfaStatus();
810
+ * ```
811
+ */
812
+ getClient() {
813
+ return this.client;
814
+ }
815
+ // ============================================================================
816
+ // Internal Methods
817
+ // ============================================================================
818
+ /**
819
+ * Initialize by hydrating state from storage.
820
+ * Called automatically on construction.
821
+ */
822
+ async initialize() {
823
+ if (this.initialized)
824
+ return;
825
+ this.initialized = true;
826
+ await this.client.initialize();
827
+ // Hydrate challenge state
828
+ const storedChallenge = await this.client.getStoredChallenge();
829
+ if (storedChallenge) {
830
+ this.challengeSubject.next(storedChallenge);
831
+ }
832
+ // Update subjects from client state
833
+ const user = this.client.getCurrentUser();
834
+ if (user) {
835
+ this.currentUserSubject.next(user);
836
+ this.isAuthenticatedSubject.next(true);
837
+ }
838
+ }
839
+ /**
840
+ * Update challenge state after auth response.
841
+ */
842
+ updateChallengeState(response) {
843
+ if (response.challengeName) {
844
+ this.challengeSubject.next(response);
845
+ }
846
+ else {
847
+ this.challengeSubject.next(null);
848
+ }
849
+ return response;
850
+ }
851
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AuthService, deps: [{ token: NAUTH_CLIENT_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
852
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AuthService, providedIn: 'root' });
853
+ }
854
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AuthService, decorators: [{
855
+ type: Injectable,
856
+ args: [{
857
+ providedIn: 'root',
858
+ }]
859
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
860
+ type: Optional
861
+ }, {
862
+ type: Inject,
863
+ args: [NAUTH_CLIENT_CONFIG]
864
+ }] }] });
865
+
866
+ /**
867
+ * Refresh state management.
868
+ * BehaviorSubject pattern is the industry-standard for token refresh.
869
+ */
870
+ let isRefreshing = false;
871
+ const refreshTokenSubject = new BehaviorSubject(null);
872
+ /**
873
+ * Track retried requests to prevent infinite loops.
874
+ */
875
+ const retriedRequests = new WeakSet();
876
+ /**
877
+ * Get CSRF token from cookie.
878
+ */
879
+ function getCsrfToken(cookieName) {
880
+ if (typeof document === 'undefined')
881
+ return null;
882
+ const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
883
+ return match ? decodeURIComponent(match[2]) : null;
884
+ }
885
+ /**
886
+ * Angular HTTP interceptor for nauth-toolkit.
887
+ *
888
+ * Handles:
889
+ * - Cookies mode: withCredentials + CSRF tokens + refresh via POST
890
+ * - JSON mode: refresh via SDK, retry with new token
891
+ */
892
+ const authInterceptor = (req, next) => {
893
+ const config = inject(NAUTH_CLIENT_CONFIG);
894
+ const http = inject(HttpClient);
895
+ const authService = inject(AuthService);
896
+ const platformId = inject(PLATFORM_ID);
897
+ const router = inject(Router);
898
+ const isBrowser = isPlatformBrowser(platformId);
899
+ if (!isBrowser) {
900
+ return next(req);
901
+ }
902
+ const tokenDelivery = config.tokenDelivery;
903
+ const baseUrl = config.baseUrl;
904
+ const endpoints = config.endpoints ?? {};
905
+ const refreshPath = endpoints.refresh ?? '/refresh';
906
+ const loginPath = endpoints.login ?? '/login';
907
+ const signupPath = endpoints.signup ?? '/signup';
908
+ const socialExchangePath = endpoints.socialExchange ?? '/social/exchange';
909
+ const refreshUrl = `${baseUrl}${refreshPath}`;
910
+ const isAuthApiRequest = req.url.includes(baseUrl);
911
+ const isRefreshEndpoint = req.url.includes(refreshPath);
912
+ const isPublicEndpoint = req.url.includes(loginPath) || req.url.includes(signupPath) || req.url.includes(socialExchangePath);
913
+ // Build request with credentials (cookies mode only)
914
+ let authReq = req;
915
+ if (tokenDelivery === 'cookies') {
916
+ authReq = authReq.clone({ withCredentials: true });
917
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
918
+ const csrfCookieName = config.csrf?.cookieName ?? 'nauth_csrf_token';
919
+ const csrfHeaderName = config.csrf?.headerName ?? 'x-csrf-token';
920
+ const csrfToken = getCsrfToken(csrfCookieName);
921
+ if (csrfToken) {
922
+ authReq = authReq.clone({ setHeaders: { [csrfHeaderName]: csrfToken } });
923
+ }
924
+ }
925
+ }
926
+ return next(authReq).pipe(catchError((error) => {
927
+ const shouldHandle = error instanceof HttpErrorResponse &&
928
+ error.status === 401 &&
929
+ isAuthApiRequest &&
930
+ !isRefreshEndpoint &&
931
+ !isPublicEndpoint &&
932
+ !retriedRequests.has(req);
933
+ if (!shouldHandle) {
934
+ return throwError(() => error);
935
+ }
936
+ if (config.debug) {
937
+ console.warn('[nauth-interceptor] 401 detected:', req.url);
938
+ }
939
+ if (!isRefreshing) {
940
+ isRefreshing = true;
941
+ refreshTokenSubject.next(null);
942
+ if (config.debug) {
943
+ console.warn('[nauth-interceptor] Starting refresh...');
944
+ }
945
+ // Refresh based on mode
946
+ const refresh$ = tokenDelivery === 'cookies'
947
+ ? http.post(refreshUrl, {}, { withCredentials: true })
948
+ : from(authService.refresh());
949
+ return refresh$.pipe(switchMap((response) => {
950
+ if (config.debug) {
951
+ console.warn('[nauth-interceptor] Refresh successful');
952
+ }
953
+ isRefreshing = false;
954
+ // Get new token (JSON mode) or signal success (cookies mode)
955
+ const newToken = 'accessToken' in response ? response.accessToken : 'success';
956
+ refreshTokenSubject.next(newToken ?? 'success');
957
+ // Build retry request
958
+ const retryReq = buildRetryRequest(authReq, tokenDelivery, newToken);
959
+ retriedRequests.add(retryReq);
960
+ if (config.debug) {
961
+ console.warn('[nauth-interceptor] Retrying:', req.url);
962
+ }
963
+ return next(retryReq);
964
+ }), catchError((err) => {
965
+ if (config.debug) {
966
+ console.error('[nauth-interceptor] Refresh failed:', err);
967
+ }
968
+ isRefreshing = false;
969
+ refreshTokenSubject.next(null);
970
+ // Handle session expiration - redirect to configured URL
971
+ if (config.redirects?.sessionExpired) {
972
+ router.navigateByUrl(config.redirects.sessionExpired).catch((navError) => {
973
+ if (config.debug) {
974
+ console.error('[nauth-interceptor] Navigation failed:', navError);
975
+ }
976
+ });
977
+ }
978
+ return throwError(() => err);
979
+ }));
980
+ }
981
+ else {
982
+ // Wait for ongoing refresh
983
+ if (config.debug) {
984
+ console.warn('[nauth-interceptor] Waiting for refresh...');
985
+ }
986
+ return refreshTokenSubject.pipe(filter$1((token) => token !== null), take(1), switchMap((token) => {
987
+ if (config.debug) {
988
+ console.warn('[nauth-interceptor] Refresh done, retrying:', req.url);
989
+ }
990
+ const retryReq = buildRetryRequest(authReq, tokenDelivery, token);
991
+ retriedRequests.add(retryReq);
992
+ return next(retryReq);
993
+ }));
994
+ }
995
+ }));
996
+ };
997
+ /**
998
+ * Build retry request with appropriate auth.
999
+ */
1000
+ function buildRetryRequest(originalReq, tokenDelivery, newToken) {
1001
+ if (tokenDelivery === 'json' && newToken && newToken !== 'success') {
1002
+ return originalReq.clone({
1003
+ setHeaders: { Authorization: `Bearer ${newToken}` },
1004
+ });
1005
+ }
1006
+ return originalReq.clone();
1007
+ }
1008
+ /**
1009
+ * Class-based interceptor for NgModule compatibility.
1010
+ */
1011
+ class AuthInterceptor {
1012
+ intercept(req, next) {
1013
+ return authInterceptor(req, next);
1014
+ }
1015
+ }
1016
+
1017
+ /**
1018
+ * Functional route guard for authentication (Angular 17+).
1019
+ *
1020
+ * Protects routes by checking if user is authenticated.
1021
+ * Redirects to login page if not authenticated.
1022
+ *
1023
+ * @param redirectTo - Path to redirect to if not authenticated (default: '/login')
1024
+ * @returns CanActivateFn guard function
1025
+ *
1026
+ * @example
1027
+ * ```typescript
1028
+ * // In route configuration
1029
+ * const routes: Routes = [
1030
+ * {
1031
+ * path: 'home',
1032
+ * component: HomeComponent,
1033
+ * canActivate: [authGuard()]
1034
+ * },
1035
+ * {
1036
+ * path: 'admin',
1037
+ * component: AdminComponent,
1038
+ * canActivate: [authGuard('/admin/login')]
1039
+ * }
1040
+ * ];
1041
+ * ```
1042
+ */
1043
+ function authGuard(redirectTo = '/login') {
1044
+ return () => {
1045
+ const auth = inject(AuthService);
1046
+ const router = inject(Router);
1047
+ if (auth.isAuthenticated()) {
1048
+ return true;
1049
+ }
1050
+ return router.createUrlTree([redirectTo]);
1051
+ };
1052
+ }
1053
+ /**
1054
+ * Class-based authentication guard for NgModule compatibility.
1055
+ *
1056
+ * @example
1057
+ * ```typescript
1058
+ * // In route configuration (NgModule)
1059
+ * const routes: Routes = [
1060
+ * {
1061
+ * path: 'home',
1062
+ * component: HomeComponent,
1063
+ * canActivate: [AuthGuard]
1064
+ * }
1065
+ * ];
1066
+ *
1067
+ * // In module providers
1068
+ * @NgModule({
1069
+ * providers: [AuthGuard]
1070
+ * })
1071
+ * ```
1072
+ */
1073
+ class AuthGuard {
1074
+ auth;
1075
+ router;
1076
+ /**
1077
+ * @param auth - Authentication service
1078
+ * @param router - Angular router
1079
+ */
1080
+ constructor(auth, router) {
1081
+ this.auth = auth;
1082
+ this.router = router;
1083
+ }
1084
+ /**
1085
+ * Check if route can be activated.
1086
+ *
1087
+ * @returns True if authenticated, otherwise redirects to login
1088
+ */
1089
+ canActivate() {
1090
+ if (this.auth.isAuthenticated()) {
1091
+ return true;
1092
+ }
1093
+ return this.router.createUrlTree(['/login']);
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * Social redirect callback route guard.
1099
+ *
1100
+ * This guard supports the redirect-first social flow where the backend redirects
1101
+ * back to the frontend with:
1102
+ * - `appState` (always optional)
1103
+ * - `exchangeToken` (present for json/hybrid flows, and for cookie flows that return a challenge)
1104
+ * - `error` / `error_description` (provider errors)
1105
+ *
1106
+ * Behavior:
1107
+ * - If `exchangeToken` exists: exchanges it via backend and redirects to success or challenge routes.
1108
+ * - If no `exchangeToken`: treat as cookie-success path and redirect to success route.
1109
+ * - If `error` exists: redirects to oauthError route.
1110
+ *
1111
+ * @example
1112
+ * ```typescript
1113
+ * import { socialRedirectCallbackGuard } from '@nauth-toolkit/client/angular';
1114
+ *
1115
+ * export const routes: Routes = [
1116
+ * { path: 'auth/callback', canActivate: [socialRedirectCallbackGuard], component: CallbackComponent },
1117
+ * ];
1118
+ * ```
1119
+ */
1120
+ const socialRedirectCallbackGuard = async () => {
1121
+ const auth = inject(AuthService);
1122
+ const config = inject(NAUTH_CLIENT_CONFIG);
1123
+ const platformId = inject(PLATFORM_ID);
1124
+ const isBrowser = isPlatformBrowser(platformId);
1125
+ if (!isBrowser) {
1126
+ return false;
1127
+ }
1128
+ const params = new URLSearchParams(window.location.search);
1129
+ const error = params.get('error');
1130
+ const exchangeToken = params.get('exchangeToken');
1131
+ // Provider error: redirect to oauthError
1132
+ if (error) {
1133
+ const errorUrl = config.redirects?.oauthError || '/login';
1134
+ window.location.replace(errorUrl);
1135
+ return false;
1136
+ }
1137
+ // No exchangeToken: cookie success path; redirect to success.
1138
+ //
1139
+ // Note: we do not "activate" the callback route to avoid consumers needing to render a page.
1140
+ if (!exchangeToken) {
1141
+ // ============================================================================
1142
+ // Cookies mode: hydrate user state before redirecting
1143
+ // ============================================================================
1144
+ // WHY: In cookie delivery, the OAuth callback completes via browser redirects, so the frontend
1145
+ // does not receive a JSON AuthResponse to populate the SDK's cached `currentUser`.
1146
+ //
1147
+ // Without this, sync guards (`authGuard`) can immediately redirect to /login because
1148
+ // `currentUser` is still null even though cookies were set successfully.
1149
+ try {
1150
+ await auth.getProfile();
1151
+ }
1152
+ catch {
1153
+ const errorUrl = config.redirects?.oauthError || '/login';
1154
+ window.location.replace(errorUrl);
1155
+ return false;
1156
+ }
1157
+ const successUrl = config.redirects?.success || '/';
1158
+ window.location.replace(successUrl);
1159
+ return false;
1160
+ }
1161
+ // Exchange token and route accordingly
1162
+ const response = await auth.exchangeSocialRedirect(exchangeToken);
1163
+ if (response.challengeName) {
1164
+ const challengeBase = config.redirects?.challengeBase || '/auth/challenge';
1165
+ const challengeRoute = response.challengeName.toLowerCase().replace(/_/g, '-');
1166
+ window.location.replace(`${challengeBase}/${challengeRoute}`);
1167
+ return false;
1168
+ }
1169
+ const successUrl = config.redirects?.success || '/';
1170
+ window.location.replace(successUrl);
1171
+ return false;
1172
+ };
1173
+
1174
+ /**
1175
+ * NgModule wrapper to provide configuration and interceptor.
1176
+ */
1177
+ class NAuthModule {
1178
+ /**
1179
+ * Configure the module with client settings.
1180
+ *
1181
+ * @param config - Client configuration
1182
+ */
1183
+ static forRoot(config) {
1184
+ return {
1185
+ ngModule: NAuthModule,
1186
+ providers: [
1187
+ { provide: NAUTH_CLIENT_CONFIG, useValue: config },
1188
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
1189
+ ],
1190
+ };
1191
+ }
1192
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1193
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: NAuthModule });
1194
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NAuthModule });
1195
+ }
1196
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NAuthModule, decorators: [{
1197
+ type: NgModule,
1198
+ args: [{}]
1199
+ }] });
1200
+
1201
+ /**
1202
+ * Public API Surface of @nauth-toolkit/client-angular
1203
+ */
1204
+ // Re-export core client types and utilities
1205
+
1206
+ /**
1207
+ * Generated bundle index. Do not edit.
1208
+ */
1209
+
1210
+ export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, authGuard, authInterceptor, socialRedirectCallbackGuard };
1211
+ //# sourceMappingURL=nauth-toolkit-client-angular.mjs.map