@nauth-toolkit/client-angular 0.1.59 → 0.1.61

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