@nauth-toolkit/client 0.1.58 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  AuthAuditEventType: () => AuthAuditEventType,
26
26
  AuthChallenge: () => AuthChallenge,
27
27
  BrowserStorage: () => BrowserStorage,
28
+ ChallengeRouter: () => ChallengeRouter,
28
29
  EventEmitter: () => EventEmitter,
29
30
  FetchAdapter: () => FetchAdapter,
30
31
  InMemoryStorage: () => InMemoryStorage,
@@ -157,6 +158,7 @@ var defaultEndpoints = {
157
158
  requestPasswordChange: "/request-password-change",
158
159
  forgotPassword: "/forgot-password",
159
160
  confirmForgotPassword: "/forgot-password/confirm",
161
+ confirmAdminResetPassword: "/admin/reset-password/confirm",
160
162
  mfaStatus: "/mfa/status",
161
163
  mfaDevices: "/mfa/devices",
162
164
  mfaSetupData: "/mfa/setup-data",
@@ -562,6 +564,155 @@ var FetchAdapter = class {
562
564
  }
563
565
  };
564
566
 
567
+ // src/core/challenge-router.ts
568
+ var ChallengeRouter = class {
569
+ constructor(config) {
570
+ this.config = config;
571
+ }
572
+ /**
573
+ * Handle auth response - either call callback or auto-navigate.
574
+ *
575
+ * @param response - Auth response from backend
576
+ * @param context - Context about the auth operation
577
+ */
578
+ async handleAuthResponse(response, context) {
579
+ if (this.config.onAuthResponse) {
580
+ await this.config.onAuthResponse(response, context);
581
+ return;
582
+ }
583
+ if (response.challengeName) {
584
+ await this.navigateToChallenge(response);
585
+ } else {
586
+ await this.navigateToSuccess();
587
+ }
588
+ }
589
+ /**
590
+ * Navigate to appropriate challenge route.
591
+ *
592
+ * @param response - Auth response containing challenge info
593
+ */
594
+ async navigateToChallenge(response) {
595
+ const url = this.buildChallengeUrl(response);
596
+ await this.navigate(url);
597
+ }
598
+ /**
599
+ * Navigate to success URL.
600
+ */
601
+ async navigateToSuccess() {
602
+ const url = this.config.redirects?.success || "/";
603
+ await this.navigate(url);
604
+ }
605
+ /**
606
+ * Navigate to error URL.
607
+ *
608
+ * @param type - Type of error (oauth or session)
609
+ */
610
+ async navigateToError(type) {
611
+ const url = type === "oauth" ? this.config.redirects?.oauthError || "/login" : this.config.redirects?.sessionExpired || "/login";
612
+ await this.navigate(url);
613
+ }
614
+ /**
615
+ * Build challenge URL based on configuration.
616
+ *
617
+ * Priority:
618
+ * 1. Custom route mapping (challengeRoutes)
619
+ * 2. Single route with query param (useSingleChallengeRoute)
620
+ * 3. MFA-specific routes (mfaRoutes) - for MFA_REQUIRED challenge only
621
+ * 4. Default separate routes (challengeBase + kebab-case)
622
+ *
623
+ * @param response - Auth response containing challenge info
624
+ * @returns URL to navigate to
625
+ */
626
+ buildChallengeUrl(response) {
627
+ const challengeName = response.challengeName;
628
+ if (this.config.redirects?.challengeRoutes?.[challengeName]) {
629
+ return this.config.redirects.challengeRoutes[challengeName];
630
+ }
631
+ const base = this.config.redirects?.challengeBase || "/auth/challenge";
632
+ if (this.config.redirects?.useSingleChallengeRoute) {
633
+ return `${base}?challenge=${challengeName}`;
634
+ }
635
+ if (challengeName === "MFA_REQUIRED" /* MFA_REQUIRED */) {
636
+ const mfaUrl = this.buildMFAUrl(response);
637
+ if (mfaUrl) {
638
+ return mfaUrl;
639
+ }
640
+ }
641
+ const route = this.buildDefaultRouteSegment(challengeName, response);
642
+ return `${base}/${route}`;
643
+ }
644
+ /**
645
+ * Build MFA-specific URL if custom mfaRoutes are configured.
646
+ *
647
+ * @param response - Auth response with MFA challenge parameters
648
+ * @returns Custom MFA URL if configured, null otherwise
649
+ */
650
+ buildMFAUrl(response) {
651
+ const params = response.challengeParameters;
652
+ const method = params?.["preferredMethod"] || params?.["method"];
653
+ const mfaRoutes = this.config.redirects?.mfaRoutes;
654
+ if (!mfaRoutes) {
655
+ return null;
656
+ }
657
+ if (method === "passkey" && mfaRoutes.passkey) {
658
+ return mfaRoutes.passkey;
659
+ }
660
+ if (!method && params?.["availableMethods"] && Array.isArray(params["availableMethods"]) && params["availableMethods"].length > 1) {
661
+ if (mfaRoutes.selector) {
662
+ return mfaRoutes.selector;
663
+ }
664
+ }
665
+ if (mfaRoutes.default) {
666
+ return mfaRoutes.default;
667
+ }
668
+ return null;
669
+ }
670
+ /**
671
+ * Build default route segment for a challenge.
672
+ *
673
+ * @param challengeName - Challenge type
674
+ * @param response - Auth response for extracting challenge parameters (needed for MFA)
675
+ * @returns Route segment (e.g., 'mfa-required/passkey', 'verify-email')
676
+ */
677
+ buildDefaultRouteSegment(challengeName, response) {
678
+ if (challengeName === "MFA_REQUIRED" /* MFA_REQUIRED */ && response) {
679
+ const params = response.challengeParameters;
680
+ const method = params?.["preferredMethod"] || params?.["method"];
681
+ if (method === "passkey") {
682
+ return "mfa-required/passkey";
683
+ }
684
+ if (!method && params?.["availableMethods"] && Array.isArray(params["availableMethods"]) && params["availableMethods"].length > 1) {
685
+ return "mfa-selector";
686
+ }
687
+ return "mfa-required";
688
+ }
689
+ return challengeName.toLowerCase().replace(/_/g, "-");
690
+ }
691
+ /**
692
+ * Execute navigation using configured handler or default.
693
+ *
694
+ * @param url - URL to navigate to
695
+ */
696
+ async navigate(url) {
697
+ if (this.config.navigationHandler) {
698
+ await this.config.navigationHandler(url);
699
+ } else {
700
+ if (typeof window !== "undefined") {
701
+ window.location.replace(url);
702
+ }
703
+ }
704
+ }
705
+ /**
706
+ * Expose URL builder for guards/components that need it.
707
+ *
708
+ * @param response - Auth response containing challenge info
709
+ * @returns URL for the challenge
710
+ */
711
+ getChallengeUrl(response) {
712
+ return this.buildChallengeUrl(response);
713
+ }
714
+ };
715
+
565
716
  // src/core/client.ts
566
717
  var USER_KEY2 = "nauth_user";
567
718
  var CHALLENGE_KEY2 = "nauth_challenge_session";
@@ -582,6 +733,7 @@ var NAuthClient = class {
582
733
  __publicField(this, "config");
583
734
  __publicField(this, "tokenManager");
584
735
  __publicField(this, "eventEmitter");
736
+ __publicField(this, "challengeRouter");
585
737
  __publicField(this, "currentUser", null);
586
738
  /**
587
739
  * Handle cross-tab storage updates.
@@ -600,6 +752,7 @@ var NAuthClient = class {
600
752
  this.config = resolveConfig({ ...userConfig, storage }, defaultAdapter);
601
753
  this.tokenManager = new TokenManager(storage);
602
754
  this.eventEmitter = new EventEmitter();
755
+ this.challengeRouter = new ChallengeRouter(this.config);
603
756
  if (hasWindow()) {
604
757
  window.addEventListener("storage", this.handleStorageEvent);
605
758
  }
@@ -629,6 +782,7 @@ var NAuthClient = class {
629
782
  const successEvent = { type: "auth:success", data: response, timestamp: Date.now() };
630
783
  this.eventEmitter.emit(successEvent);
631
784
  }
785
+ await this.challengeRouter.handleAuthResponse(response, { source: "login" });
632
786
  return response;
633
787
  } catch (error) {
634
788
  const authError = error instanceof NAuthClientError ? error : new NAuthClientError("AUTH_INVALID_CREDENTIALS" /* AUTH_INVALID_CREDENTIALS */, error.message || "Login failed");
@@ -650,6 +804,7 @@ var NAuthClient = class {
650
804
  } else {
651
805
  this.eventEmitter.emit({ type: "auth:success", data: response, timestamp: Date.now() });
652
806
  }
807
+ await this.challengeRouter.handleAuthResponse(response, { source: "signup" });
653
808
  return response;
654
809
  } catch (error) {
655
810
  const authError = error instanceof NAuthClientError ? error : new NAuthClientError("AUTH_INVALID_CREDENTIALS" /* AUTH_INVALID_CREDENTIALS */, error.message || "Signup failed");
@@ -779,6 +934,7 @@ var NAuthClient = class {
779
934
  const successEvent = { type: "auth:success", data: result, timestamp: Date.now() };
780
935
  this.eventEmitter.emit(successEvent);
781
936
  }
937
+ await this.challengeRouter.handleAuthResponse(result, { source: "challenge" });
782
938
  return result;
783
939
  } catch (error) {
784
940
  const authError = error instanceof NAuthClientError ? error : new NAuthClientError(
@@ -866,6 +1022,45 @@ var NAuthClient = class {
866
1022
  await this.clearAuthState(false);
867
1023
  return result;
868
1024
  }
1025
+ /**
1026
+ * Reset password with code or token (works for both admin-initiated and user-initiated resets).
1027
+ *
1028
+ * Accepts either:
1029
+ * - code: Short numeric code from email/SMS (6-10 digits)
1030
+ * - token: Long hex token from reset link (64 chars)
1031
+ *
1032
+ * WHY: Generic method that works for both admin-initiated (adminResetPassword) and
1033
+ * user-initiated (forgotPassword) password resets. Uses same backend endpoint.
1034
+ *
1035
+ * @param identifier - User identifier (email, username, phone)
1036
+ * @param codeOrToken - Verification code OR token from link (one required)
1037
+ * @param newPassword - New password
1038
+ * @returns Success response
1039
+ * @throws {NAuthClientError} When reset fails
1040
+ *
1041
+ * @example
1042
+ * ```typescript
1043
+ * // With code from email
1044
+ * await client.resetPasswordWithCode('user@example.com', '123456', 'NewPass123!');
1045
+ *
1046
+ * // With token from link
1047
+ * await client.resetPasswordWithCode('user@example.com', '64-char-token', 'NewPass123!');
1048
+ * ```
1049
+ */
1050
+ async resetPasswordWithCode(identifier, codeOrToken, newPassword) {
1051
+ const isToken = codeOrToken.length > 10;
1052
+ const payload = {
1053
+ identifier,
1054
+ ...isToken ? { token: codeOrToken } : { code: codeOrToken },
1055
+ newPassword
1056
+ };
1057
+ const result = await this.post(
1058
+ this.config.endpoints.confirmAdminResetPassword,
1059
+ payload
1060
+ );
1061
+ await this.clearAuthState(false);
1062
+ return result;
1063
+ }
869
1064
  /**
870
1065
  * Request password change (must change on next login).
871
1066
  */
@@ -1026,6 +1221,7 @@ var NAuthClient = class {
1026
1221
  }
1027
1222
  const result = await this.post(this.config.endpoints.socialExchange, { exchangeToken: token });
1028
1223
  await this.handleAuthResponse(result);
1224
+ await this.challengeRouter.handleAuthResponse(result, { source: "social" });
1029
1225
  return result;
1030
1226
  }
1031
1227
  /**
@@ -1379,6 +1575,21 @@ var NAuthClient = class {
1379
1575
  });
1380
1576
  return response.data;
1381
1577
  }
1578
+ /**
1579
+ * Get challenge router for manual navigation control.
1580
+ * Useful for guards that need to handle errors or build custom URLs.
1581
+ *
1582
+ * @returns ChallengeRouter instance
1583
+ *
1584
+ * @example
1585
+ * ```typescript
1586
+ * const router = client.getChallengeRouter();
1587
+ * await router.navigateToError('oauth');
1588
+ * ```
1589
+ */
1590
+ getChallengeRouter() {
1591
+ return this.challengeRouter;
1592
+ }
1382
1593
  };
1383
1594
 
1384
1595
  // src/core/challenge-helpers.ts
@@ -1427,6 +1638,7 @@ function isOTPChallenge(challenge) {
1427
1638
  AuthAuditEventType,
1428
1639
  AuthChallenge,
1429
1640
  BrowserStorage,
1641
+ ChallengeRouter,
1430
1642
  EventEmitter,
1431
1643
  FetchAdapter,
1432
1644
  InMemoryStorage,