@djangocfg/api 2.1.449 → 2.1.452

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.cjs CHANGED
@@ -46,9 +46,18 @@ __export(auth_exports, {
46
46
  formatAuthError: () => formatAuthError,
47
47
  getCacheMetadata: () => getCacheMetadata,
48
48
  getCachedProfile: () => getCachedProfile,
49
+ getTokenExpiry: () => getTokenExpiry,
49
50
  hasValidCache: () => hasValidCache,
51
+ isAllowedAuthPath: () => isAllowedAuthPath,
52
+ isTokenExpired: () => isTokenExpired,
53
+ isTokenExpiringSoon: () => isTokenExpiringSoon,
50
54
  logger: () => logger,
55
+ nextAuthEvaluationDelay: () => nextAuthEvaluationDelay,
56
+ normalizePath: () => normalizePath,
57
+ resolveGuardIsAuthenticated: () => resolveGuardIsAuthenticated,
58
+ resolveGuardIsLoading: () => resolveGuardIsLoading,
51
59
  setCachedProfile: () => setCachedProfile,
60
+ shouldRedirectToAuth: () => shouldRedirectToAuth,
52
61
  useAccountsContext: () => useAccountsContext,
53
62
  useAuth: () => useAuth,
54
63
  useAuthForm: () => useAuthForm,
@@ -348,21 +357,28 @@ var authLogger = logger.withTag("auth");
348
357
  // src/auth/hooks/useAutoAuth.ts
349
358
  var import_navigation3 = require("next/navigation");
350
359
  var import_react5 = require("react");
360
+
361
+ // src/auth/utils/path.ts
351
362
  var normalizePath = /* @__PURE__ */ __name((path) => {
352
363
  if (!path) return "";
353
364
  const withoutLocale = path.replace(/^\/[a-z]{2}(?=\/|$)/, "");
354
365
  const trimmed = withoutLocale.replace(/\/+$/, "");
355
366
  return trimmed || "/";
356
367
  }, "normalizePath");
368
+ var isAllowedAuthPath = /* @__PURE__ */ __name((pathname, allowedPaths) => {
369
+ const normalized = normalizePath(pathname);
370
+ return allowedPaths.some(
371
+ (p) => normalized === p || normalized.startsWith(p + "/")
372
+ );
373
+ }, "isAllowedAuthPath");
374
+
375
+ // src/auth/hooks/useAutoAuth.ts
357
376
  var useAutoAuth = /* @__PURE__ */ __name((options = {}) => {
358
377
  const { onOTPDetected, cleanupUrl = true, allowedPaths = ["/auth"] } = options;
359
378
  const queryParams = useQueryParams();
360
379
  const pathname = (0, import_navigation3.usePathname)();
361
380
  const router = useCfgRouter();
362
- const normalizedPath = normalizePath(pathname);
363
- const isAllowedPath = allowedPaths.some(
364
- (path) => normalizedPath === path || normalizedPath.startsWith(path + "/")
365
- );
381
+ const isAllowedPath = isAllowedAuthPath(pathname, allowedPaths);
366
382
  const queryOtp = queryParams.get("otp") || "";
367
383
  const queryEmail = queryParams.get("email") || void 0;
368
384
  const hasOTP = !!queryOtp;
@@ -3864,24 +3880,45 @@ function triggerRefresh() {
3864
3880
  }
3865
3881
  __name(triggerRefresh, "triggerRefresh");
3866
3882
 
3867
- // src/auth/hooks/useTokenRefresh.ts
3868
- var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
3869
- var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3870
- function getTokenExpiry(token) {
3883
+ // src/auth/utils/jwt.ts
3884
+ function decodeSegment(segment) {
3871
3885
  try {
3872
- const payload = JSON.parse(atob(token.split(".")[1]));
3873
- return payload.exp * 1e3;
3886
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
3887
+ if (typeof atob !== "function") return null;
3888
+ const json = atob(base64);
3889
+ return JSON.parse(json);
3874
3890
  } catch {
3875
3891
  return null;
3876
3892
  }
3877
3893
  }
3894
+ __name(decodeSegment, "decodeSegment");
3895
+ function getTokenExpiry(token) {
3896
+ if (!token || typeof token !== "string") return null;
3897
+ const parts = token.split(".");
3898
+ if (parts.length !== 3) return null;
3899
+ const payload = decodeSegment(parts[1]);
3900
+ if (!payload || typeof payload !== "object") return null;
3901
+ const exp = payload.exp;
3902
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
3903
+ return exp * 1e3;
3904
+ }
3878
3905
  __name(getTokenExpiry, "getTokenExpiry");
3879
- function isTokenExpiringSoon(token, thresholdMs) {
3906
+ function isTokenExpired(token, skewMs = 0, now = Date.now()) {
3907
+ const expiry = getTokenExpiry(token);
3908
+ if (expiry === null) return true;
3909
+ return expiry - skewMs <= now;
3910
+ }
3911
+ __name(isTokenExpired, "isTokenExpired");
3912
+ function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
3880
3913
  const expiry = getTokenExpiry(token);
3881
- if (!expiry) return false;
3882
- return expiry - Date.now() < thresholdMs;
3914
+ if (expiry === null) return false;
3915
+ return expiry - now < thresholdMs;
3883
3916
  }
3884
3917
  __name(isTokenExpiringSoon, "isTokenExpiringSoon");
3918
+
3919
+ // src/auth/hooks/useTokenRefresh.ts
3920
+ var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
3921
+ var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3885
3922
  function useTokenRefresh(options = {}) {
3886
3923
  const { enabled = true, onRefresh, onRefreshError } = options;
3887
3924
  const refreshToken = (0, import_react14.useCallback)(async () => {
@@ -3981,6 +4018,30 @@ var useDeleteAccount = /* @__PURE__ */ __name(() => {
3981
4018
  };
3982
4019
  }, "useDeleteAccount");
3983
4020
 
4021
+ // src/auth/utils/guard.ts
4022
+ function resolveGuardIsLoading(s) {
4023
+ if (!s.mounted) return true;
4024
+ if (!s.requireAuth) return false;
4025
+ return s.authLoading || s.isRedirecting || !s.isAuthenticated;
4026
+ }
4027
+ __name(resolveGuardIsLoading, "resolveGuardIsLoading");
4028
+ function shouldRedirectToAuth(s) {
4029
+ return s.mounted && s.requireAuth && !s.authLoading && !s.isAuthenticated && !s.isRedirecting;
4030
+ }
4031
+ __name(shouldRedirectToAuth, "shouldRedirectToAuth");
4032
+ function resolveGuardIsAuthenticated(s) {
4033
+ return !s.requireAuth || s.isAuthenticated;
4034
+ }
4035
+ __name(resolveGuardIsAuthenticated, "resolveGuardIsAuthenticated");
4036
+ function nextAuthEvaluationDelay(accessExp, refreshExp, now) {
4037
+ const future = [accessExp, refreshExp].filter(
4038
+ (d) => d !== null && d > now
4039
+ );
4040
+ if (future.length === 0) return null;
4041
+ return Math.max(0, Math.min(...future) - now) + 1e3;
4042
+ }
4043
+ __name(nextAuthEvaluationDelay, "nextAuthEvaluationDelay");
4044
+
3984
4045
  // src/auth/context/AccountsContext.tsx
3985
4046
  var import_react16 = require("react");
3986
4047
 
@@ -4737,6 +4798,20 @@ var hasValidTokens = /* @__PURE__ */ __name(() => {
4737
4798
  if (typeof window === "undefined") return false;
4738
4799
  return CfgAccountsApi.isAuthenticated();
4739
4800
  }, "hasValidTokens");
4801
+ var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
4802
+ const accessDead = isTokenExpired(accessToken, 0, now);
4803
+ if (!accessDead) return false;
4804
+ const refreshDead = isTokenExpired(refreshToken, 0, now);
4805
+ return refreshDead;
4806
+ }, "isSessionDeadOnBootstrap");
4807
+ var isSessionAlive = /* @__PURE__ */ __name((now = Date.now()) => {
4808
+ if (typeof window === "undefined") return false;
4809
+ return !isSessionDeadOnBootstrap(
4810
+ CfgAccountsApi.getToken(),
4811
+ CfgAccountsApi.getRefreshToken(),
4812
+ now
4813
+ );
4814
+ }, "isSessionAlive");
4740
4815
  var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4741
4816
  const accounts = useAccountsContext();
4742
4817
  ensureRefreshHandler();
@@ -4752,19 +4827,24 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4752
4827
  return true;
4753
4828
  });
4754
4829
  const [initialized, setInitialized] = (0, import_react17.useState)(false);
4830
+ const [authTick, setAuthTick] = (0, import_react17.useState)(0);
4831
+ const bumpAuthTick = (0, import_react17.useCallback)(() => setAuthTick((t) => t + 1), []);
4832
+ (0, import_react17.useEffect)(() => {
4833
+ if (typeof window === "undefined") return;
4834
+ const delay = nextAuthEvaluationDelay(
4835
+ getTokenExpiry(CfgAccountsApi.getToken()),
4836
+ getTokenExpiry(CfgAccountsApi.getRefreshToken()),
4837
+ Date.now()
4838
+ );
4839
+ if (delay === null) return;
4840
+ const timer = setTimeout(bumpAuthTick, Math.min(delay, 2e9));
4841
+ return () => clearTimeout(timer);
4842
+ }, [authTick, bumpAuthTick]);
4843
+ const onUnauthorizedRef = (0, import_react17.useRef)(false);
4755
4844
  const router = useCfgRouter();
4756
4845
  const pathname = (0, import_navigation4.usePathname)();
4757
4846
  const queryParams = useQueryParams();
4758
4847
  const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(EMAIL_STORAGE_KEY, null);
4759
- useTokenRefresh({
4760
- enabled: true,
4761
- onRefresh: /* @__PURE__ */ __name((newToken) => {
4762
- authLogger.info("Token auto-refreshed successfully");
4763
- }, "onRefresh"),
4764
- onRefreshError: /* @__PURE__ */ __name((error) => {
4765
- authLogger.warn("Token auto-refresh failed:", error.message);
4766
- }, "onRefreshError")
4767
- });
4768
4848
  const user = accounts.profile;
4769
4849
  const userRef = (0, import_react17.useRef)(user);
4770
4850
  const configRef = (0, import_react17.useRef)(config);
@@ -4781,7 +4861,23 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4781
4861
  clearProfileCache();
4782
4862
  setInitialized(true);
4783
4863
  setIsLoading(false);
4784
- }, []);
4864
+ bumpAuthTick();
4865
+ }, [bumpAuthTick]);
4866
+ const handleProactiveRefreshError = (0, import_react17.useCallback)((error) => {
4867
+ authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
4868
+ if (onUnauthorizedRef.current) return;
4869
+ onUnauthorizedRef.current = true;
4870
+ clearAuthState("proactiveRefresh:failed");
4871
+ const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4872
+ router.hardReplace(authCallbackUrl);
4873
+ }, [clearAuthState, router]);
4874
+ useTokenRefresh({
4875
+ enabled: true,
4876
+ onRefresh: /* @__PURE__ */ __name(() => {
4877
+ authLogger.info("Token auto-refreshed successfully");
4878
+ }, "onRefresh"),
4879
+ onRefreshError: handleProactiveRefreshError
4880
+ });
4785
4881
  const handleGlobalAuthError = (0, import_react17.useCallback)((error, context = "API Request") => {
4786
4882
  const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
4787
4883
  if (isAuthError) {
@@ -4794,6 +4890,20 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4794
4890
  }
4795
4891
  return false;
4796
4892
  }, [clearAuthState]);
4893
+ (0, import_react17.useEffect)(() => {
4894
+ const handler = /* @__PURE__ */ __name(() => {
4895
+ if (onUnauthorizedRef.current) return;
4896
+ onUnauthorizedRef.current = true;
4897
+ authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
4898
+ clearAuthState("onUnauthorized:401");
4899
+ const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4900
+ router.hardReplace(authCallbackUrl);
4901
+ }, "handler");
4902
+ CfgAccountsApi.onUnauthorized(handler);
4903
+ return () => {
4904
+ CfgAccountsApi.onUnauthorized(null);
4905
+ };
4906
+ }, [clearAuthState, router]);
4797
4907
  const isAutoLoggingOutRef = (0, import_react17.useRef)(false);
4798
4908
  const swrOnError = (0, import_react17.useCallback)((error) => {
4799
4909
  const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
@@ -4858,6 +4968,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4858
4968
  authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
4859
4969
  authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
4860
4970
  authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
4971
+ if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
4972
+ authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
4973
+ clearAuthState("initializeAuth:deadSession");
4974
+ return;
4975
+ }
4861
4976
  const hasTokens = hasValidTokens();
4862
4977
  authLogger.info("Has tokens:", hasTokens);
4863
4978
  if (userRef.current) {
@@ -5114,8 +5229,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5114
5229
  () => ({
5115
5230
  user,
5116
5231
  isLoading,
5117
- // Consider authenticated if we have valid tokens, even without user profile
5118
- isAuthenticated: CfgAccountsApi.isAuthenticated(),
5232
+ // Authenticated iff the session is locally alive (access valid OR a usable
5233
+ // refresh token) — validated by `exp`, NOT by mere token presence. This is
5234
+ // what makes the guard self-sufficient: an expired session reads as
5235
+ // unauthenticated WITHOUT waiting for a server 401 (which CSP / a hung
5236
+ // request / offline can swallow). `authTick` (in deps) forces recompute
5237
+ // after tokens are cleared/set, since token state lives in storage.
5238
+ isAuthenticated: isSessionAlive(),
5119
5239
  isAdminUser,
5120
5240
  loadCurrentProfile,
5121
5241
  checkAuthAndRedirect,
@@ -5140,6 +5260,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5140
5260
  [
5141
5261
  user,
5142
5262
  isLoading,
5263
+ authTick,
5264
+ // recompute isAuthenticated when tokens are cleared/set
5143
5265
  isAdminUser,
5144
5266
  loadCurrentProfile,
5145
5267
  checkAuthAndRedirect,