@nauth-toolkit/client-angular 0.1.91 → 0.1.93

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,7 +1,7 @@
1
1
  import { NAuthErrorCode, NAuthClientError, NAuthClient } from '@nauth-toolkit/client';
2
2
  export * from '@nauth-toolkit/client';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, Injectable, Inject, Optional, inject, NgModule, PLATFORM_ID } from '@angular/core';
4
+ import { InjectionToken, Injectable, PLATFORM_ID, Inject, Optional, inject, APP_INITIALIZER, NgModule, makeEnvironmentProviders } from '@angular/core';
5
5
  import { firstValueFrom, BehaviorSubject, Subject, from, switchMap, of, map, catchError, throwError, finalize, shareReplay } from 'rxjs';
6
6
  import { filter } from 'rxjs/operators';
7
7
  import * as i1 from '@angular/common/http';
@@ -136,6 +136,374 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
136
136
  type: Injectable
137
137
  }], ctorParameters: () => [{ type: i1.HttpClient }] });
138
138
 
139
+ /**
140
+ * Injection token for reCAPTCHA configuration
141
+ */
142
+ const RECAPTCHA_CONFIG = new InjectionToken('RECAPTCHA_CONFIG', {
143
+ providedIn: 'root',
144
+ factory: () => undefined,
145
+ });
146
+ /**
147
+ * Google reCAPTCHA service for Angular applications.
148
+ *
149
+ * Provides lazy loading of reCAPTCHA script and platform-aware token generation.
150
+ * Automatically detects platform (web, Capacitor WebView, Capacitor native, SSR)
151
+ * and skips reCAPTCHA in environments where it's not supported or needed.
152
+ *
153
+ * Features:
154
+ * - Lazy script loading (only loads when needed)
155
+ * - v2 (checkbox) and v3 (invisible) support
156
+ * - Platform detection (web, Capacitor, SSR)
157
+ * - Automatic skip for Capacitor native mode
158
+ * - Automatic skip for SSR
159
+ *
160
+ * @example v3 Automatic Mode
161
+ * ```typescript
162
+ * constructor(private recaptcha: RecaptchaService) {}
163
+ *
164
+ * async login() {
165
+ * const token = await this.recaptcha.execute('login');
166
+ * await this.auth.login(email, password, token);
167
+ * }
168
+ * ```
169
+ *
170
+ * @example v2 Manual Mode
171
+ * ```typescript
172
+ * ngOnInit() {
173
+ * this.recaptcha.render('recaptcha-container', (token) => {
174
+ * this.recaptchaToken = token;
175
+ * });
176
+ * }
177
+ * ```
178
+ */
179
+ class RecaptchaService {
180
+ platformId;
181
+ config;
182
+ scriptLoaded = false;
183
+ scriptLoading = null;
184
+ platform;
185
+ widgetId = null;
186
+ constructor(platformId, config) {
187
+ this.platformId = platformId;
188
+ this.config = config;
189
+ this.platform = this.detectPlatform();
190
+ // Auto-preload script for v3/Enterprise so it's ready before first login/signup
191
+ // No-op when disabled, shouldSkip, or v2 (v2 renders on-demand)
192
+ if (this.config?.enabled && (this.config.version === 'v3' || this.config.version === 'enterprise')) {
193
+ if (!this.shouldSkip()) {
194
+ this.loadScript().catch(() => {
195
+ // Silently fail - execute() will handle errors when called
196
+ });
197
+ }
198
+ }
199
+ }
200
+ // ============================================================================
201
+ // Platform Detection
202
+ // ============================================================================
203
+ /**
204
+ * Detect the current platform environment.
205
+ *
206
+ * Detection priority:
207
+ * 1. SSR (not in browser) → 'ssr'
208
+ * 2. Capacitor native (no web view) → 'capacitor-native'
209
+ * 3. Capacitor WebView → 'capacitor-webview'
210
+ * 4. Web browser → 'web'
211
+ *
212
+ * @returns Detected platform type
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * const platform = this.detectPlatform();
217
+ * if (platform === 'capacitor-native') {
218
+ * // Skip reCAPTCHA, use device attestation
219
+ * }
220
+ * ```
221
+ */
222
+ detectPlatform() {
223
+ // SSR detection
224
+ if (!isPlatformBrowser(this.platformId)) {
225
+ return 'ssr';
226
+ }
227
+ // Capacitor detection (window.Capacitor exists)
228
+ const windowRef = window;
229
+ if (windowRef.Capacitor) {
230
+ // Capacitor native (iOS/Android app)
231
+ if (typeof windowRef.Capacitor.isNativePlatform === 'function' && windowRef.Capacitor.isNativePlatform()) {
232
+ return 'capacitor-native';
233
+ }
234
+ // Capacitor WebView
235
+ return 'capacitor-webview';
236
+ }
237
+ return 'web';
238
+ }
239
+ /**
240
+ * Get the current platform.
241
+ *
242
+ * @returns Current platform type
243
+ */
244
+ getPlatform() {
245
+ return this.platform;
246
+ }
247
+ /**
248
+ * Check if reCAPTCHA should be skipped for current platform.
249
+ *
250
+ * Skips for:
251
+ * - SSR (no window object)
252
+ * - Capacitor native (use device attestation instead)
253
+ *
254
+ * @returns True if should skip reCAPTCHA
255
+ */
256
+ shouldSkip() {
257
+ return this.platform === 'ssr' || this.platform === 'capacitor-native';
258
+ }
259
+ // ============================================================================
260
+ // Script Loading
261
+ // ============================================================================
262
+ /**
263
+ * Load Google reCAPTCHA script if not already loaded.
264
+ *
265
+ * Script URL format:
266
+ * - v2: https://www.google.com/recaptcha/api.js
267
+ * - v3: https://www.google.com/recaptcha/api.js?render={siteKey}
268
+ * - Enterprise: https://www.google.com/recaptcha/enterprise.js?render={siteKey}
269
+ *
270
+ * @returns Promise that resolves when script is loaded
271
+ *
272
+ * @throws Error if config is missing or script fails to load
273
+ */
274
+ async loadScript() {
275
+ // Skip in SSR or Capacitor native
276
+ if (this.shouldSkip()) {
277
+ return;
278
+ }
279
+ // Skip if disabled
280
+ if (!this.config?.enabled) {
281
+ return;
282
+ }
283
+ // Already loaded
284
+ if (this.scriptLoaded) {
285
+ return;
286
+ }
287
+ // Already loading (return existing promise)
288
+ if (this.scriptLoading) {
289
+ return this.scriptLoading;
290
+ }
291
+ // Validate config
292
+ if (!this.config.siteKey) {
293
+ throw new Error('[RecaptchaService] Site key is required');
294
+ }
295
+ // Start loading
296
+ this.scriptLoading = this.injectScript();
297
+ try {
298
+ await this.scriptLoading;
299
+ this.scriptLoaded = true;
300
+ }
301
+ finally {
302
+ this.scriptLoading = null;
303
+ }
304
+ }
305
+ /**
306
+ * Inject the reCAPTCHA script into the DOM.
307
+ *
308
+ * @returns Promise that resolves when script loads
309
+ */
310
+ injectScript() {
311
+ return new Promise((resolve, reject) => {
312
+ const script = document.createElement('script');
313
+ script.async = true;
314
+ script.defer = true;
315
+ // Set script URL based on version
316
+ if (this.config.version === 'enterprise') {
317
+ script.src = `https://www.google.com/recaptcha/enterprise.js?render=${this.config.siteKey}`;
318
+ }
319
+ else if (this.config.version === 'v3') {
320
+ script.src = `https://www.google.com/recaptcha/api.js?render=${this.config.siteKey}`;
321
+ }
322
+ else {
323
+ // v2 - load without render parameter
324
+ let url = 'https://www.google.com/recaptcha/api.js';
325
+ if (this.config.language) {
326
+ url += `?hl=${this.config.language}`;
327
+ }
328
+ script.src = url;
329
+ }
330
+ script.onload = () => resolve();
331
+ script.onerror = () => reject(new Error('[RecaptchaService] Failed to load reCAPTCHA script'));
332
+ document.head.appendChild(script);
333
+ });
334
+ }
335
+ // ============================================================================
336
+ // v3/Enterprise Methods (Invisible Challenge)
337
+ // ============================================================================
338
+ /**
339
+ * Execute reCAPTCHA v3/Enterprise challenge (invisible).
340
+ *
341
+ * Automatically loads script if needed and generates a token.
342
+ * Skips automatically for SSR and Capacitor native.
343
+ *
344
+ * @param action - Action name for v3 analytics (e.g., 'login', 'signup')
345
+ * @returns Promise resolving to reCAPTCHA token, or undefined if skipped
346
+ *
347
+ * @throws Error if version is v2, config missing, or execution fails
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * const token = await this.recaptcha.execute('login');
352
+ * if (token) {
353
+ * await this.auth.login(email, password, token);
354
+ * }
355
+ * ```
356
+ */
357
+ async execute(action) {
358
+ // Skip for platforms that don't support reCAPTCHA
359
+ if (this.shouldSkip()) {
360
+ return undefined;
361
+ }
362
+ // Skip if disabled
363
+ if (!this.config?.enabled) {
364
+ return undefined;
365
+ }
366
+ // v2 requires manual render
367
+ if (this.config.version === 'v2') {
368
+ throw new Error('[RecaptchaService] execute() is only for v3/Enterprise. Use render() for v2.');
369
+ }
370
+ // Load script if needed
371
+ await this.loadScript();
372
+ // Get grecaptcha object
373
+ const grecaptcha = window.grecaptcha;
374
+ if (!grecaptcha) {
375
+ throw new Error('[RecaptchaService] grecaptcha is not loaded');
376
+ }
377
+ // Execute reCAPTCHA
378
+ const actionName = action || this.config.action || 'submit';
379
+ try {
380
+ if (this.config.version === 'enterprise' && grecaptcha.enterprise?.execute) {
381
+ return await grecaptcha.enterprise.execute(this.config.siteKey, { action: actionName });
382
+ }
383
+ else if (grecaptcha.execute) {
384
+ return await grecaptcha.execute(this.config.siteKey, { action: actionName });
385
+ }
386
+ else {
387
+ throw new Error('[RecaptchaService] grecaptcha.execute is not available');
388
+ }
389
+ }
390
+ catch (error) {
391
+ throw new Error(`[RecaptchaService] Failed to execute reCAPTCHA: ${error instanceof Error ? error.message : 'unknown error'}`);
392
+ }
393
+ }
394
+ // ============================================================================
395
+ // v2 Methods (Visible Checkbox)
396
+ // ============================================================================
397
+ /**
398
+ * Render reCAPTCHA v2 checkbox widget.
399
+ *
400
+ * @param containerId - DOM element ID or element to render in
401
+ * @param callback - Callback when user completes challenge
402
+ * @returns Promise resolving to widget ID
403
+ *
404
+ * @throws Error if version is not v2, config missing, or render fails
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * ngAfterViewInit() {
409
+ * this.recaptcha.render('recaptcha-container', (token) => {
410
+ * this.recaptchaToken = token;
411
+ * this.loginForm.patchValue({ recaptchaToken: token });
412
+ * });
413
+ * }
414
+ * ```
415
+ */
416
+ async render(containerId, callback) {
417
+ // Skip for platforms that don't support reCAPTCHA
418
+ if (this.shouldSkip()) {
419
+ throw new Error('[RecaptchaService] reCAPTCHA v2 is not supported in SSR or Capacitor native');
420
+ }
421
+ // Skip if disabled
422
+ if (!this.config?.enabled) {
423
+ throw new Error('[RecaptchaService] reCAPTCHA is not enabled');
424
+ }
425
+ // Only for v2
426
+ if (this.config.version !== 'v2') {
427
+ throw new Error('[RecaptchaService] render() is only for v2. Use execute() for v3/Enterprise.');
428
+ }
429
+ // Load script if needed
430
+ await this.loadScript();
431
+ // Get grecaptcha object
432
+ const grecaptcha = window.grecaptcha;
433
+ if (!grecaptcha?.render) {
434
+ throw new Error('[RecaptchaService] grecaptcha.render is not available');
435
+ }
436
+ // Render widget
437
+ try {
438
+ this.widgetId = grecaptcha.render(containerId, {
439
+ sitekey: this.config.siteKey,
440
+ callback: callback,
441
+ });
442
+ return this.widgetId;
443
+ }
444
+ catch (error) {
445
+ throw new Error(`[RecaptchaService] Failed to render reCAPTCHA: ${error instanceof Error ? error.message : 'unknown error'}`);
446
+ }
447
+ }
448
+ /**
449
+ * Get response token from v2 widget.
450
+ *
451
+ * @param widgetId - Widget ID (optional, uses last rendered widget if not provided)
452
+ * @returns reCAPTCHA token or null if not completed
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * const token = this.recaptcha.getResponse();
457
+ * if (token) {
458
+ * await this.auth.login(email, password, token);
459
+ * }
460
+ * ```
461
+ */
462
+ getResponse(widgetId) {
463
+ const grecaptcha = window.grecaptcha;
464
+ if (!grecaptcha?.getResponse) {
465
+ return null;
466
+ }
467
+ const id = widgetId !== undefined ? widgetId : this.widgetId ?? undefined;
468
+ return grecaptcha.getResponse(id) || null;
469
+ }
470
+ /**
471
+ * Reset v2 widget (clear response).
472
+ *
473
+ * @param widgetId - Widget ID (optional, uses last rendered widget if not provided)
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * // After failed login
478
+ * this.recaptcha.reset();
479
+ * ```
480
+ */
481
+ reset(widgetId) {
482
+ const grecaptcha = window.grecaptcha;
483
+ if (!grecaptcha?.reset) {
484
+ return;
485
+ }
486
+ const id = widgetId !== undefined ? widgetId : this.widgetId ?? undefined;
487
+ grecaptcha.reset(id);
488
+ }
489
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, deps: [{ token: PLATFORM_ID }, { token: RECAPTCHA_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
490
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, providedIn: 'root' });
491
+ }
492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecaptchaService, decorators: [{
493
+ type: Injectable,
494
+ args: [{
495
+ providedIn: 'root',
496
+ }]
497
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
498
+ type: Inject,
499
+ args: [PLATFORM_ID]
500
+ }] }, { type: undefined, decorators: [{
501
+ type: Optional
502
+ }, {
503
+ type: Inject,
504
+ args: [RECAPTCHA_CONFIG]
505
+ }] }] });
506
+
139
507
  /**
140
508
  * Angular wrapper around NAuthClient that provides promise-based auth methods and reactive state.
141
509
  *
@@ -170,6 +538,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
170
538
  */
171
539
  class AuthService {
172
540
  router;
541
+ recaptchaService;
173
542
  client;
174
543
  config;
175
544
  currentUserSubject = new BehaviorSubject(null);
@@ -181,9 +550,11 @@ class AuthService {
181
550
  * @param config - Injected client configuration (required)
182
551
  * @param httpAdapter - Angular HTTP adapter for making requests (required)
183
552
  * @param router - Angular Router (optional, automatically used for navigation if available)
553
+ * @param recaptchaService - RecaptchaService (optional, for automatic token generation)
184
554
  */
185
- constructor(config, httpAdapter, router) {
555
+ constructor(config, httpAdapter, router, recaptchaService) {
186
556
  this.router = router;
557
+ this.recaptchaService = recaptchaService;
187
558
  this.config = config;
188
559
  // Use provided httpAdapter (from config or injected)
189
560
  const adapter = config.httpAdapter ?? httpAdapter;
@@ -334,31 +705,39 @@ class AuthService {
334
705
  /**
335
706
  * Login with identifier and password.
336
707
  *
708
+ * Automatically generates reCAPTCHA token if configured (v3 only).
709
+ * For v2 manual mode, pass the token explicitly.
710
+ *
337
711
  * @param identifier - User email or username
338
712
  * @param password - User password
713
+ * @param recaptchaToken - Optional reCAPTCHA token (for v2 manual mode or when auto-generation is disabled)
339
714
  * @returns Promise with auth response or challenge
340
715
  *
341
- * @example
716
+ * @example Basic Login
342
717
  * ```typescript
343
718
  * const response = await this.auth.login('user@example.com', 'password');
344
- * if (response.challengeName) {
345
- * // Handle challenge
346
- * } else {
347
- * // Login successful
348
- * }
719
+ * ```
720
+ *
721
+ * @example With Manual reCAPTCHA (v2)
722
+ * ```typescript
723
+ * const response = await this.auth.login('user@example.com', 'password', recaptchaToken);
349
724
  * ```
350
725
  */
351
- async login(identifier, password) {
352
- const res = await this.client.login(identifier, password);
726
+ async login(identifier, password, recaptchaToken) {
727
+ const token = await this.getRecaptchaToken(recaptchaToken, 'login');
728
+ const res = await this.client.login(identifier, password, token);
353
729
  return this.updateChallengeState(res);
354
730
  }
355
731
  /**
356
732
  * Signup with credentials.
357
733
  *
734
+ * Automatically generates reCAPTCHA token if configured (v3 only).
735
+ * For v2 manual mode, include token in payload.
736
+ *
358
737
  * @param payload - Signup request payload
359
738
  * @returns Promise with auth response or challenge
360
739
  *
361
- * @example
740
+ * @example Basic Signup
362
741
  * ```typescript
363
742
  * const response = await this.auth.signup({
364
743
  * email: 'new@example.com',
@@ -366,8 +745,25 @@ class AuthService {
366
745
  * firstName: 'John',
367
746
  * });
368
747
  * ```
748
+ *
749
+ * @example With Manual reCAPTCHA (v2)
750
+ * ```typescript
751
+ * const response = await this.auth.signup({
752
+ * email: 'new@example.com',
753
+ * password: 'SecurePass123!',
754
+ * recaptchaToken: token,
755
+ * });
756
+ * ```
369
757
  */
370
758
  async signup(payload) {
759
+ // Auto-generate reCAPTCHA token for v3 if configured and not already provided
760
+ const payloadWithRecaptcha = payload;
761
+ if (!payloadWithRecaptcha.recaptchaToken) {
762
+ const token = await this.getRecaptchaToken(undefined, 'signup');
763
+ if (token) {
764
+ payload = { ...payload, recaptchaToken: token };
765
+ }
766
+ }
371
767
  const res = await this.client.signup(payload);
372
768
  return this.updateChallengeState(res);
373
769
  }
@@ -1029,7 +1425,58 @@ class AuthService {
1029
1425
  }
1030
1426
  return response;
1031
1427
  }
1032
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService, deps: [{ token: NAUTH_CLIENT_CONFIG }, { token: AngularHttpAdapter }, { token: i2.Router, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
1428
+ // ============================================================================
1429
+ // reCAPTCHA Helper
1430
+ // ============================================================================
1431
+ /**
1432
+ * Get reCAPTCHA token - auto-generate for v3 or use provided token.
1433
+ *
1434
+ * Handles platform detection:
1435
+ * - Web browser: Generate token if enabled and v3
1436
+ * - Capacitor native: Skip (use device attestation instead)
1437
+ * - SSR: Skip
1438
+ * - Manual mode (v2 or manualChallenge=true): Requires explicit token
1439
+ *
1440
+ * @param providedToken - Explicitly provided token (v2 manual mode)
1441
+ * @param action - Action name for v3 analytics
1442
+ * @returns reCAPTCHA token or undefined
1443
+ *
1444
+ * @private
1445
+ */
1446
+ async getRecaptchaToken(providedToken, action) {
1447
+ // If token explicitly provided, use it (v2 manual mode)
1448
+ if (providedToken) {
1449
+ return providedToken;
1450
+ }
1451
+ // No reCAPTCHA service available
1452
+ if (!this.recaptchaService) {
1453
+ return undefined;
1454
+ }
1455
+ // Check if reCAPTCHA is configured
1456
+ const recaptchaConfig = this.config.recaptcha;
1457
+ if (!recaptchaConfig?.enabled) {
1458
+ return undefined;
1459
+ }
1460
+ // Skip for platforms that don't support reCAPTCHA (SSR, Capacitor native)
1461
+ if (this.recaptchaService.shouldSkip()) {
1462
+ return undefined;
1463
+ }
1464
+ // v2 or manual mode - user must provide token explicitly
1465
+ if (recaptchaConfig.version === 'v2' || recaptchaConfig.manualChallenge) {
1466
+ return undefined;
1467
+ }
1468
+ // Auto-generate token for v3/Enterprise
1469
+ try {
1470
+ return await this.recaptchaService.execute(action);
1471
+ }
1472
+ catch (error) {
1473
+ // Log error but don't block authentication
1474
+ // Server will enforce if required
1475
+ console.warn('[AuthService] Failed to generate reCAPTCHA token:', error);
1476
+ return undefined;
1477
+ }
1478
+ }
1479
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService, deps: [{ token: NAUTH_CLIENT_CONFIG }, { token: AngularHttpAdapter }, { token: i2.Router, optional: true }, { token: RecaptchaService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
1033
1480
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService });
1034
1481
  }
1035
1482
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthService, decorators: [{
@@ -1039,6 +1486,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1039
1486
  args: [NAUTH_CLIENT_CONFIG]
1040
1487
  }] }, { type: AngularHttpAdapter }, { type: i2.Router, decorators: [{
1041
1488
  type: Optional
1489
+ }] }, { type: RecaptchaService, decorators: [{
1490
+ type: Optional
1042
1491
  }] }] });
1043
1492
 
1044
1493
  /**
@@ -1458,7 +1907,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1458
1907
  *
1459
1908
  * Use this for NgModule-based apps (Angular 17+ with NgModule or legacy apps).
1460
1909
  *
1461
- * @example
1910
+ * @example Basic Setup
1462
1911
  * ```typescript
1463
1912
  * // app.module.ts
1464
1913
  * import { NAuthModule } from '@nauth-toolkit/client-angular';
@@ -1473,35 +1922,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1473
1922
  * })
1474
1923
  * export class AppModule {}
1475
1924
  * ```
1925
+ *
1926
+ * @example With reCAPTCHA Enterprise
1927
+ * ```typescript
1928
+ * NAuthModule.forRoot({
1929
+ * baseUrl: 'http://localhost:3000/auth',
1930
+ * tokenDelivery: 'cookies',
1931
+ * recaptcha: {
1932
+ * enabled: true,
1933
+ * version: 'enterprise',
1934
+ * siteKey: environment.recaptchaSiteKey,
1935
+ * action: 'login',
1936
+ * },
1937
+ * })
1938
+ * ```
1476
1939
  */
1477
1940
  class NAuthModule {
1478
1941
  static forRoot(config) {
1479
- return {
1480
- ngModule: NAuthModule,
1481
- providers: [
1482
- {
1483
- provide: NAUTH_CLIENT_CONFIG,
1484
- useValue: config,
1485
- },
1486
- AngularHttpAdapter,
1487
- {
1488
- provide: AuthService,
1489
- useFactory: (httpAdapter) => {
1490
- // Try to inject Router optionally - if not available, pass undefined
1491
- // Router will be undefined if not provided (e.g., in apps without routing)
1492
- const router = inject(Router, { optional: true });
1493
- return new AuthService(config, httpAdapter, router ?? undefined);
1494
- },
1495
- deps: [AngularHttpAdapter],
1942
+ const providers = [
1943
+ {
1944
+ provide: NAUTH_CLIENT_CONFIG,
1945
+ useValue: config,
1946
+ },
1947
+ AngularHttpAdapter,
1948
+ {
1949
+ provide: AuthService,
1950
+ useFactory: (httpAdapter, recaptchaService) => {
1951
+ // Try to inject Router optionally - if not available, pass undefined
1952
+ // Router will be undefined if not provided (e.g., in apps without routing)
1953
+ const router = inject(Router, { optional: true });
1954
+ return new AuthService(config, httpAdapter, router ?? undefined, recaptchaService);
1496
1955
  },
1497
- {
1498
- provide: HTTP_INTERCEPTORS,
1499
- useClass: AuthInterceptorClass,
1500
- multi: true,
1956
+ deps: [AngularHttpAdapter, [new Optional(), RecaptchaService]],
1957
+ },
1958
+ {
1959
+ provide: HTTP_INTERCEPTORS,
1960
+ useClass: AuthInterceptorClass,
1961
+ multi: true,
1962
+ },
1963
+ // Provide AuthGuard so it has access to NAUTH_CLIENT_CONFIG
1964
+ AuthGuard,
1965
+ ];
1966
+ // Add reCAPTCHA providers if configured
1967
+ if (config.recaptcha?.enabled) {
1968
+ providers.push({
1969
+ provide: RECAPTCHA_CONFIG,
1970
+ // Cast because interface extends but they're compatible
1971
+ useValue: config.recaptcha,
1972
+ }, RecaptchaService, {
1973
+ provide: APP_INITIALIZER,
1974
+ useFactory: () => {
1975
+ // Force RecaptchaService instantiation at app startup so script preloads
1976
+ inject(RecaptchaService);
1977
+ // No-op - constructor already handles preload
1978
+ return () => { };
1501
1979
  },
1502
- // Provide AuthGuard so it has access to NAUTH_CLIENT_CONFIG
1503
- AuthGuard,
1504
- ],
1980
+ multi: true,
1981
+ });
1982
+ }
1983
+ return {
1984
+ ngModule: NAuthModule,
1985
+ providers,
1505
1986
  };
1506
1987
  }
1507
1988
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -1557,6 +2038,7 @@ class AuthInterceptor {
1557
2038
  * - If `exchangeToken` exists: exchanges it via backend (SDK handles navigation automatically).
1558
2039
  * - If no `exchangeToken`: treat as cookie-success path (SDK handles navigation automatically).
1559
2040
  * - If `error` exists: redirects to oauthError route.
2041
+ * - If auto-redirect is disabled (redirectUrls set to null): returns true to activate the route.
1560
2042
  *
1561
2043
  * @example
1562
2044
  * ```typescript
@@ -1587,14 +2069,16 @@ const socialRedirectCallbackGuard = async () => {
1587
2069
  if (appState) {
1588
2070
  await auth.getClient().storeOauthState(appState);
1589
2071
  }
1590
- // Provider error: redirect to oauthError
2072
+ // Provider error: redirect to oauthError (or activate route if auto-redirect disabled)
1591
2073
  if (error) {
1592
2074
  await router.navigateToError('oauth');
1593
- return false;
2075
+ // Return true to activate route if oauthError redirect is disabled
2076
+ return router.isErrorRedirectDisabled('oauth');
1594
2077
  }
1595
2078
  // No exchangeToken: cookie success path; hydrate then navigate to success.
1596
2079
  //
1597
- // Note: we do not "activate" the callback route to avoid consumers needing to render a page.
2080
+ // Note: When auto-redirect is enabled, we do not "activate" the callback route to avoid
2081
+ // consumers needing to render a page. When disabled, we activate the route.
1598
2082
  if (!exchangeToken) {
1599
2083
  // ============================================================================
1600
2084
  // Cookies mode: hydrate user state before redirecting
@@ -1636,34 +2120,88 @@ const socialRedirectCallbackGuard = async () => {
1636
2120
  appState: appState ?? undefined,
1637
2121
  });
1638
2122
  }
1639
- catch (err) {
2123
+ catch (error) {
1640
2124
  // Only treat auth failures (401/403) as OAuth errors
1641
2125
  // Network errors or other issues might be temporary - still try success route
1642
- const isAuthError = err instanceof NAuthClientError &&
1643
- (err.statusCode === 401 ||
1644
- err.statusCode === 403 ||
1645
- err.code === NAuthErrorCode.AUTH_TOKEN_INVALID ||
1646
- err.code === NAuthErrorCode.AUTH_SESSION_EXPIRED ||
1647
- err.code === NAuthErrorCode.AUTH_SESSION_NOT_FOUND);
1648
- if (isAuthError) {
1649
- // Cookies weren't set properly - OAuth failed
1650
- await router.navigateToError('oauth');
1651
- }
1652
- else {
1653
- // For network errors or other issues, proceed to success route
1654
- // The auth guard will handle authentication state on the next route
1655
- await router.navigateToSuccess(appState ? { appState } : undefined);
2126
+ // Type guard: check if error is NAuthClientError
2127
+ if (error instanceof NAuthClientError) {
2128
+ const isAuthError = error.statusCode === 401 ||
2129
+ error.statusCode === 403 ||
2130
+ error.code === NAuthErrorCode.AUTH_TOKEN_INVALID ||
2131
+ error.code === NAuthErrorCode.AUTH_SESSION_EXPIRED ||
2132
+ error.code === NAuthErrorCode.AUTH_SESSION_NOT_FOUND;
2133
+ if (isAuthError) {
2134
+ // Cookies weren't set properly - OAuth failed
2135
+ await router.navigateToError('oauth');
2136
+ return router.isErrorRedirectDisabled('oauth');
2137
+ }
1656
2138
  }
2139
+ // For network errors or other non-auth issues, proceed to success route
2140
+ // The auth guard will handle authentication state on the next route
2141
+ await router.navigateToSuccess(appState ? { appState } : undefined);
1657
2142
  }
1658
- return false;
2143
+ // Return true if success redirect is disabled, allowing the callback component to render
2144
+ return router.isSuccessRedirectDisabled({ source: 'social', appState: appState ?? undefined });
1659
2145
  }
1660
2146
  // Exchange token - SDK handles navigation automatically
1661
2147
  // Note: appState will be passed via query params when navigateToSuccess is called
1662
2148
  // by the challenge router after successful exchange
1663
2149
  await auth.exchangeSocialRedirect(exchangeToken);
1664
- return false;
2150
+ // Return true if success redirect is disabled, allowing the callback component to render
2151
+ // We use 'social' as source since this is the social OAuth callback flow
2152
+ return router.isSuccessRedirectDisabled({ source: 'social', appState: appState ?? undefined });
1665
2153
  };
1666
2154
 
2155
+ /**
2156
+ * Provides reCAPTCHA configuration and automatic script preloading.
2157
+ *
2158
+ * Sets up `RECAPTCHA_CONFIG` and forces `RecaptchaService` instantiation at app
2159
+ * startup so the reCAPTCHA script preloads before the user clicks login/signup.
2160
+ *
2161
+ * @param config - reCAPTCHA configuration (enabled, version, siteKey, action)
2162
+ * @returns Environment providers for reCAPTCHA
2163
+ *
2164
+ * @example
2165
+ * ```typescript
2166
+ * export const appConfig: ApplicationConfig = {
2167
+ * providers: [
2168
+ * provideRecaptcha({
2169
+ * enabled: true,
2170
+ * version: 'enterprise',
2171
+ * siteKey: environment.recaptchaSiteKey,
2172
+ * action: 'login',
2173
+ * }),
2174
+ * // ... other providers
2175
+ * ],
2176
+ * };
2177
+ * ```
2178
+ */
2179
+ function provideRecaptcha(config) {
2180
+ return makeEnvironmentProviders([
2181
+ {
2182
+ provide: RECAPTCHA_CONFIG,
2183
+ useValue: config,
2184
+ },
2185
+ RecaptchaService,
2186
+ {
2187
+ provide: APP_INITIALIZER,
2188
+ useFactory: () => {
2189
+ const recaptcha = inject(RecaptchaService);
2190
+ // Return initialization function that ensures script starts loading
2191
+ return () => {
2192
+ // Trigger script load (fire-and-forget, don't block app startup)
2193
+ if (config.enabled && (config.version === 'v3' || config.version === 'enterprise')) {
2194
+ recaptcha.loadScript().catch(() => {
2195
+ // Silent fail - execute() will retry when called
2196
+ });
2197
+ }
2198
+ };
2199
+ },
2200
+ multi: true,
2201
+ },
2202
+ ]);
2203
+ }
2204
+
1667
2205
  /**
1668
2206
  * Public API Surface of @nauth-toolkit/client-angular (NgModule)
1669
2207
  *
@@ -1676,5 +2214,5 @@ const socialRedirectCallbackGuard = async () => {
1676
2214
  * Generated bundle index. Do not edit.
1677
2215
  */
1678
2216
 
1679
- export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthInterceptorClass, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, authGuard, authInterceptor, socialRedirectCallbackGuard };
2217
+ export { AngularHttpAdapter, AuthGuard, AuthInterceptor, AuthInterceptorClass, AuthService, NAUTH_CLIENT_CONFIG, NAuthModule, RECAPTCHA_CONFIG, RecaptchaService, authGuard, authInterceptor, provideRecaptcha, socialRedirectCallbackGuard };
1680
2218
  //# sourceMappingURL=nauth-toolkit-client-angular.mjs.map