@djangocfg/api 2.1.449 → 2.1.450

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/auth.d.cts CHANGED
@@ -951,6 +951,8 @@ interface UseTokenRefreshOptions {
951
951
  }
952
952
  /**
953
953
  * Hook for proactive automatic token refresh.
954
+ * (JWT expiry helpers live in ../utils/jwt so they can be unit-tested and
955
+ * reused by the auth-bootstrap validation path.)
954
956
  */
955
957
  declare function useTokenRefresh(options?: UseTokenRefreshOptions): {
956
958
  refreshToken: () => Promise<boolean>;
package/dist/auth.d.ts CHANGED
@@ -951,6 +951,8 @@ interface UseTokenRefreshOptions {
951
951
  }
952
952
  /**
953
953
  * Hook for proactive automatic token refresh.
954
+ * (JWT expiry helpers live in ../utils/jwt so they can be unit-tested and
955
+ * reused by the auth-bootstrap validation path.)
954
956
  */
955
957
  declare function useTokenRefresh(options?: UseTokenRefreshOptions): {
956
958
  refreshToken: () => Promise<boolean>;
package/dist/auth.mjs CHANGED
@@ -3802,24 +3802,45 @@ function triggerRefresh() {
3802
3802
  }
3803
3803
  __name(triggerRefresh, "triggerRefresh");
3804
3804
 
3805
- // src/auth/hooks/useTokenRefresh.ts
3806
- var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
3807
- var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3808
- function getTokenExpiry(token) {
3805
+ // src/auth/utils/jwt.ts
3806
+ function decodeSegment(segment) {
3809
3807
  try {
3810
- const payload = JSON.parse(atob(token.split(".")[1]));
3811
- return payload.exp * 1e3;
3808
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
3809
+ if (typeof atob !== "function") return null;
3810
+ const json = atob(base64);
3811
+ return JSON.parse(json);
3812
3812
  } catch {
3813
3813
  return null;
3814
3814
  }
3815
3815
  }
3816
+ __name(decodeSegment, "decodeSegment");
3817
+ function getTokenExpiry(token) {
3818
+ if (!token || typeof token !== "string") return null;
3819
+ const parts = token.split(".");
3820
+ if (parts.length !== 3) return null;
3821
+ const payload = decodeSegment(parts[1]);
3822
+ if (!payload || typeof payload !== "object") return null;
3823
+ const exp = payload.exp;
3824
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
3825
+ return exp * 1e3;
3826
+ }
3816
3827
  __name(getTokenExpiry, "getTokenExpiry");
3817
- function isTokenExpiringSoon(token, thresholdMs) {
3828
+ function isTokenExpired(token, skewMs = 0, now = Date.now()) {
3829
+ const expiry = getTokenExpiry(token);
3830
+ if (expiry === null) return true;
3831
+ return expiry - skewMs <= now;
3832
+ }
3833
+ __name(isTokenExpired, "isTokenExpired");
3834
+ function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
3818
3835
  const expiry = getTokenExpiry(token);
3819
- if (!expiry) return false;
3820
- return expiry - Date.now() < thresholdMs;
3836
+ if (expiry === null) return false;
3837
+ return expiry - now < thresholdMs;
3821
3838
  }
3822
3839
  __name(isTokenExpiringSoon, "isTokenExpiringSoon");
3840
+
3841
+ // src/auth/hooks/useTokenRefresh.ts
3842
+ var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
3843
+ var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3823
3844
  function useTokenRefresh(options = {}) {
3824
3845
  const { enabled = true, onRefresh, onRefreshError } = options;
3825
3846
  const refreshToken = useCallback9(async () => {
@@ -4683,6 +4704,12 @@ var hasValidTokens = /* @__PURE__ */ __name(() => {
4683
4704
  if (typeof window === "undefined") return false;
4684
4705
  return CfgAccountsApi.isAuthenticated();
4685
4706
  }, "hasValidTokens");
4707
+ var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
4708
+ const accessDead = isTokenExpired(accessToken, 0, now);
4709
+ if (!accessDead) return false;
4710
+ const refreshDead = isTokenExpired(refreshToken, 0, now);
4711
+ return refreshDead;
4712
+ }, "isSessionDeadOnBootstrap");
4686
4713
  var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4687
4714
  const accounts = useAccountsContext();
4688
4715
  ensureRefreshHandler();
@@ -4698,19 +4725,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4698
4725
  return true;
4699
4726
  });
4700
4727
  const [initialized, setInitialized] = useState12(false);
4728
+ const [authTick, setAuthTick] = useState12(0);
4729
+ const bumpAuthTick = useCallback12(() => setAuthTick((t) => t + 1), []);
4730
+ const onUnauthorizedRef = useRef6(false);
4701
4731
  const router = useCfgRouter();
4702
4732
  const pathname = usePathname3();
4703
4733
  const queryParams = useQueryParams();
4704
4734
  const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(EMAIL_STORAGE_KEY, null);
4705
- useTokenRefresh({
4706
- enabled: true,
4707
- onRefresh: /* @__PURE__ */ __name((newToken) => {
4708
- authLogger.info("Token auto-refreshed successfully");
4709
- }, "onRefresh"),
4710
- onRefreshError: /* @__PURE__ */ __name((error) => {
4711
- authLogger.warn("Token auto-refresh failed:", error.message);
4712
- }, "onRefreshError")
4713
- });
4714
4735
  const user = accounts.profile;
4715
4736
  const userRef = useRef6(user);
4716
4737
  const configRef = useRef6(config);
@@ -4727,7 +4748,23 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4727
4748
  clearProfileCache();
4728
4749
  setInitialized(true);
4729
4750
  setIsLoading(false);
4730
- }, []);
4751
+ bumpAuthTick();
4752
+ }, [bumpAuthTick]);
4753
+ const handleProactiveRefreshError = useCallback12((error) => {
4754
+ authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
4755
+ if (onUnauthorizedRef.current) return;
4756
+ onUnauthorizedRef.current = true;
4757
+ clearAuthState("proactiveRefresh:failed");
4758
+ const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4759
+ router.hardReplace(authCallbackUrl);
4760
+ }, [clearAuthState, router]);
4761
+ useTokenRefresh({
4762
+ enabled: true,
4763
+ onRefresh: /* @__PURE__ */ __name(() => {
4764
+ authLogger.info("Token auto-refreshed successfully");
4765
+ }, "onRefresh"),
4766
+ onRefreshError: handleProactiveRefreshError
4767
+ });
4731
4768
  const handleGlobalAuthError = useCallback12((error, context = "API Request") => {
4732
4769
  const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
4733
4770
  if (isAuthError) {
@@ -4740,6 +4777,20 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4740
4777
  }
4741
4778
  return false;
4742
4779
  }, [clearAuthState]);
4780
+ useEffect9(() => {
4781
+ const handler = /* @__PURE__ */ __name(() => {
4782
+ if (onUnauthorizedRef.current) return;
4783
+ onUnauthorizedRef.current = true;
4784
+ authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
4785
+ clearAuthState("onUnauthorized:401");
4786
+ const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4787
+ router.hardReplace(authCallbackUrl);
4788
+ }, "handler");
4789
+ CfgAccountsApi.onUnauthorized(handler);
4790
+ return () => {
4791
+ CfgAccountsApi.onUnauthorized(null);
4792
+ };
4793
+ }, [clearAuthState, router]);
4743
4794
  const isAutoLoggingOutRef = useRef6(false);
4744
4795
  const swrOnError = useCallback12((error) => {
4745
4796
  const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
@@ -4804,6 +4855,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4804
4855
  authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
4805
4856
  authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
4806
4857
  authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
4858
+ if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
4859
+ authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
4860
+ clearAuthState("initializeAuth:deadSession");
4861
+ return;
4862
+ }
4807
4863
  const hasTokens = hasValidTokens();
4808
4864
  authLogger.info("Has tokens:", hasTokens);
4809
4865
  if (userRef.current) {
@@ -5060,7 +5116,9 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5060
5116
  () => ({
5061
5117
  user,
5062
5118
  isLoading,
5063
- // Consider authenticated if we have valid tokens, even without user profile
5119
+ // Consider authenticated if we have valid tokens, even without user profile.
5120
+ // `authTick` (in deps) forces recompute after tokens are cleared/set, since
5121
+ // token state lives in storage, not React state.
5064
5122
  isAuthenticated: CfgAccountsApi.isAuthenticated(),
5065
5123
  isAdminUser,
5066
5124
  loadCurrentProfile,
@@ -5086,6 +5144,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5086
5144
  [
5087
5145
  user,
5088
5146
  isLoading,
5147
+ authTick,
5148
+ // recompute isAuthenticated when tokens are cleared/set
5089
5149
  isAdminUser,
5090
5150
  loadCurrentProfile,
5091
5151
  checkAuthAndRedirect,