@djangocfg/api 2.1.450 → 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.d.cts CHANGED
@@ -634,7 +634,9 @@ interface UseAutoAuthOptions {
634
634
  }
635
635
  /**
636
636
  * Hook for automatic authentication from URL query parameters
637
- * Detects OTP from URL and triggers callback
637
+ * Detects OTP from URL and triggers callback.
638
+ * (Path normalization / allow-list matching live in ../utils/path so they can
639
+ * be unit-tested independently of the hook.)
638
640
  */
639
641
  declare const useAutoAuth: (options?: UseAutoAuthOptions) => {
640
642
  isReady: boolean;
@@ -1004,6 +1006,107 @@ declare const validateEmail: (email: string) => boolean;
1004
1006
  */
1005
1007
  declare const formatAuthError: (error: any) => string;
1006
1008
 
1009
+ /**
1010
+ * Pure auth-guard state resolution.
1011
+ *
1012
+ * This is the decision that produced the "stuck on Authenticating…" bug: given
1013
+ * the auth state, should the UI show a preloader, redirect to login, or render
1014
+ * the protected content? Extracted from useAuthGuard (which lives in a package
1015
+ * with no test infra) so the branching can be unit-tested in isolation — the
1016
+ * hook just wires React state (mounted / isRedirecting) into these functions.
1017
+ *
1018
+ * Invariant that fixes the bug: once auth is resolved (`authLoading === false`)
1019
+ * and the user is unauthenticated, `isLoading` must become false so the guard
1020
+ * redirects to the form instead of spinning forever.
1021
+ */
1022
+ interface GuardInput {
1023
+ /** First client render matches SSR (preloader) until mounted, to avoid hydration mismatch. */
1024
+ mounted: boolean;
1025
+ /** Whether this route requires authentication. */
1026
+ requireAuth: boolean;
1027
+ /** AuthContext still resolving initial state. */
1028
+ authLoading: boolean;
1029
+ /** A redirect to the auth page is in flight. */
1030
+ isRedirecting: boolean;
1031
+ /** User currently has a (valid-looking) session. */
1032
+ isAuthenticated: boolean;
1033
+ }
1034
+ /**
1035
+ * True while the guard should render a preloader rather than the protected
1036
+ * content. Mirrors useAuthGuard's formula exactly.
1037
+ */
1038
+ declare function resolveGuardIsLoading(s: GuardInput): boolean;
1039
+ /**
1040
+ * Whether the guard should trigger a redirect to the auth page. Only once
1041
+ * mounted, on a protected route, after auth resolved, when unauthenticated and
1042
+ * not already redirecting. This is the branch that must fire for a dead session.
1043
+ */
1044
+ declare function shouldRedirectToAuth(s: GuardInput): boolean;
1045
+ /** The effective authenticated flag the guard exposes to consumers. */
1046
+ declare function resolveGuardIsAuthenticated(s: Pick<GuardInput, 'requireAuth' | 'isAuthenticated'>): boolean;
1047
+ /**
1048
+ * Delay (ms from `now`) until `isAuthenticated` should next be re-evaluated,
1049
+ * given the access/refresh expiry timestamps (ms epoch, or null if absent).
1050
+ *
1051
+ * `isAuthenticated` is derived from token `exp`, so it can change with no event
1052
+ * to trigger a recompute — a tab left open silently crosses an expiry boundary.
1053
+ * We schedule a wake-up at the SOONEST future expiry (the next moment the answer
1054
+ * could flip), plus a 1s cushion so we re-check strictly after the inclusive
1055
+ * boundary. Returns null when nothing live remains to expire (no timer needed).
1056
+ * Callers should clamp the result to the platform setTimeout ceiling.
1057
+ */
1058
+ declare function nextAuthEvaluationDelay(accessExp: number | null, refreshExp: number | null, now: number): number | null;
1059
+
1060
+ /**
1061
+ * JWT inspection helpers (browser-safe, dependency-free).
1062
+ *
1063
+ * These decode ONLY the `exp` claim to reason about token freshness on the
1064
+ * client. They do NOT verify the signature — that is the server's job. The
1065
+ * point is purely to avoid firing doomed requests with a token we can already
1066
+ * see is malformed or expired, and to let auth bootstrap collapse a dead
1067
+ * session to the login form instead of hanging on a preloader.
1068
+ *
1069
+ * A JWT that cannot be decoded is treated as EXPIRED/invalid (fail-closed):
1070
+ * better to re-authenticate than to loop on 401s with garbage in storage.
1071
+ */
1072
+ /**
1073
+ * Extract the `exp` claim (epoch ms), or null if the token is undecodable or
1074
+ * has no numeric `exp`.
1075
+ */
1076
+ declare function getTokenExpiry(token: string | null | undefined): number | null;
1077
+ /**
1078
+ * True when the token is missing, malformed, or already past its `exp`.
1079
+ * `skewMs` treats a token expiring within the skew window as already expired
1080
+ * (default 0). Fail-closed: an undecodable token is considered expired.
1081
+ */
1082
+ declare function isTokenExpired(token: string | null | undefined, skewMs?: number, now?: number): boolean;
1083
+ /**
1084
+ * True when a valid token exists but expires within `thresholdMs` (used by the
1085
+ * proactive refresher). A missing/undecodable token returns false here — that
1086
+ * case is handled by the "expired" path, not the "expiring soon" path.
1087
+ */
1088
+ declare function isTokenExpiringSoon(token: string | null | undefined, thresholdMs: number, now?: number): boolean;
1089
+
1090
+ /**
1091
+ * Path normalization for auth routing.
1092
+ *
1093
+ * next-intl may keep a 2-letter locale prefix in the URL (e.g. `/ru/auth`), and
1094
+ * paths arrive with or without trailing slashes. To match a pathname against a
1095
+ * fixed allow-list (like `['/auth']`) reliably, strip the locale segment and
1096
+ * trailing slashes first. Extracted from useAutoAuth so it can be unit-tested
1097
+ * and reused.
1098
+ */
1099
+ /**
1100
+ * Strip a leading 2-letter locale segment and trailing slashes.
1101
+ * '/ru/auth/' → '/auth', '/auth' → '/auth', '/' → '/', '' → ''.
1102
+ */
1103
+ declare const normalizePath: (path: string | null | undefined) => string;
1104
+ /**
1105
+ * True when `pathname` (after locale/slash normalization) is one of the allowed
1106
+ * paths, or nested under one of them (`/auth` matches `/auth/callback`).
1107
+ */
1108
+ declare const isAllowedAuthPath: (pathname: string | null | undefined, allowedPaths: string[]) => boolean;
1109
+
1007
1110
  declare const logger: consola.ConsolaInstance;
1008
1111
  /**
1009
1112
  * Auth-specific logger
@@ -1053,4 +1156,4 @@ declare const Analytics: {
1053
1156
  setUser(userId: string): void;
1054
1157
  };
1055
1158
 
1056
- export { AUTH_CONSTANTS, type AccountsContextValue, AccountsProvider, Analytics, AnalyticsCategory, type AnalyticsCategoryType, AnalyticsEvent, type AnalyticsEventType, type AuthConfig, AuthContext, type AuthContextType, type AuthFormAutoSubmit, type AuthFormReturn, type AuthFormState, type AuthFormStateHandlers, type AuthFormSubmitHandlers, type AuthFormValidation, AuthProvider, type AuthProviderProps, type AuthStep, type DeleteAccountResult, type OTPRequestResult, type PatchedCfgUserUpdateRequest, PatchedCfgUserUpdateRequestSchema, type ProfileCacheOptions, type TwoFactorDevice, type TwoFactorSetupData, type UseAuthFormOptions, type UseAuthFormStateReturn, type UseAutoAuthOptions, type UseDeleteAccountReturn, type UseGithubAuthOptions, type UseGithubAuthReturn, type UseTwoFactorOptions, type UseTwoFactorReturn, type UseTwoFactorSetupOptions, type UseTwoFactorSetupReturn, type UseTwoFactorStatusReturn, type UserProfile, type WebmailLink, authLogger, clearProfileCache, decodeBase64, encodeBase64, formatAuthError, getCacheMetadata, getCachedProfile, hasValidCache, logger, setCachedProfile, useAccountsContext, useAuth, useAuthForm, useAuthFormState, useAuthGuard, useAuthRedirectManager, useAuthValidation, useAutoAuth, useBase64, useCfgRouter, useDeleteAccount, useGithubAuth, useLocalStorage, useQueryParams, useSessionStorage, useTokenRefresh, useTwoFactor, useTwoFactorSetup, useTwoFactorStatus, validateEmail, validateIdentifier };
1159
+ export { AUTH_CONSTANTS, type AccountsContextValue, AccountsProvider, Analytics, AnalyticsCategory, type AnalyticsCategoryType, AnalyticsEvent, type AnalyticsEventType, type AuthConfig, AuthContext, type AuthContextType, type AuthFormAutoSubmit, type AuthFormReturn, type AuthFormState, type AuthFormStateHandlers, type AuthFormSubmitHandlers, type AuthFormValidation, AuthProvider, type AuthProviderProps, type AuthStep, type DeleteAccountResult, type GuardInput, type OTPRequestResult, type PatchedCfgUserUpdateRequest, PatchedCfgUserUpdateRequestSchema, type ProfileCacheOptions, type TwoFactorDevice, type TwoFactorSetupData, type UseAuthFormOptions, type UseAuthFormStateReturn, type UseAutoAuthOptions, type UseDeleteAccountReturn, type UseGithubAuthOptions, type UseGithubAuthReturn, type UseTwoFactorOptions, type UseTwoFactorReturn, type UseTwoFactorSetupOptions, type UseTwoFactorSetupReturn, type UseTwoFactorStatusReturn, type UserProfile, type WebmailLink, authLogger, clearProfileCache, decodeBase64, encodeBase64, formatAuthError, getCacheMetadata, getCachedProfile, getTokenExpiry, hasValidCache, isAllowedAuthPath, isTokenExpired, isTokenExpiringSoon, logger, nextAuthEvaluationDelay, normalizePath, resolveGuardIsAuthenticated, resolveGuardIsLoading, setCachedProfile, shouldRedirectToAuth, useAccountsContext, useAuth, useAuthForm, useAuthFormState, useAuthGuard, useAuthRedirectManager, useAuthValidation, useAutoAuth, useBase64, useCfgRouter, useDeleteAccount, useGithubAuth, useLocalStorage, useQueryParams, useSessionStorage, useTokenRefresh, useTwoFactor, useTwoFactorSetup, useTwoFactorStatus, validateEmail, validateIdentifier };
package/dist/auth.d.ts CHANGED
@@ -634,7 +634,9 @@ interface UseAutoAuthOptions {
634
634
  }
635
635
  /**
636
636
  * Hook for automatic authentication from URL query parameters
637
- * Detects OTP from URL and triggers callback
637
+ * Detects OTP from URL and triggers callback.
638
+ * (Path normalization / allow-list matching live in ../utils/path so they can
639
+ * be unit-tested independently of the hook.)
638
640
  */
639
641
  declare const useAutoAuth: (options?: UseAutoAuthOptions) => {
640
642
  isReady: boolean;
@@ -1004,6 +1006,107 @@ declare const validateEmail: (email: string) => boolean;
1004
1006
  */
1005
1007
  declare const formatAuthError: (error: any) => string;
1006
1008
 
1009
+ /**
1010
+ * Pure auth-guard state resolution.
1011
+ *
1012
+ * This is the decision that produced the "stuck on Authenticating…" bug: given
1013
+ * the auth state, should the UI show a preloader, redirect to login, or render
1014
+ * the protected content? Extracted from useAuthGuard (which lives in a package
1015
+ * with no test infra) so the branching can be unit-tested in isolation — the
1016
+ * hook just wires React state (mounted / isRedirecting) into these functions.
1017
+ *
1018
+ * Invariant that fixes the bug: once auth is resolved (`authLoading === false`)
1019
+ * and the user is unauthenticated, `isLoading` must become false so the guard
1020
+ * redirects to the form instead of spinning forever.
1021
+ */
1022
+ interface GuardInput {
1023
+ /** First client render matches SSR (preloader) until mounted, to avoid hydration mismatch. */
1024
+ mounted: boolean;
1025
+ /** Whether this route requires authentication. */
1026
+ requireAuth: boolean;
1027
+ /** AuthContext still resolving initial state. */
1028
+ authLoading: boolean;
1029
+ /** A redirect to the auth page is in flight. */
1030
+ isRedirecting: boolean;
1031
+ /** User currently has a (valid-looking) session. */
1032
+ isAuthenticated: boolean;
1033
+ }
1034
+ /**
1035
+ * True while the guard should render a preloader rather than the protected
1036
+ * content. Mirrors useAuthGuard's formula exactly.
1037
+ */
1038
+ declare function resolveGuardIsLoading(s: GuardInput): boolean;
1039
+ /**
1040
+ * Whether the guard should trigger a redirect to the auth page. Only once
1041
+ * mounted, on a protected route, after auth resolved, when unauthenticated and
1042
+ * not already redirecting. This is the branch that must fire for a dead session.
1043
+ */
1044
+ declare function shouldRedirectToAuth(s: GuardInput): boolean;
1045
+ /** The effective authenticated flag the guard exposes to consumers. */
1046
+ declare function resolveGuardIsAuthenticated(s: Pick<GuardInput, 'requireAuth' | 'isAuthenticated'>): boolean;
1047
+ /**
1048
+ * Delay (ms from `now`) until `isAuthenticated` should next be re-evaluated,
1049
+ * given the access/refresh expiry timestamps (ms epoch, or null if absent).
1050
+ *
1051
+ * `isAuthenticated` is derived from token `exp`, so it can change with no event
1052
+ * to trigger a recompute — a tab left open silently crosses an expiry boundary.
1053
+ * We schedule a wake-up at the SOONEST future expiry (the next moment the answer
1054
+ * could flip), plus a 1s cushion so we re-check strictly after the inclusive
1055
+ * boundary. Returns null when nothing live remains to expire (no timer needed).
1056
+ * Callers should clamp the result to the platform setTimeout ceiling.
1057
+ */
1058
+ declare function nextAuthEvaluationDelay(accessExp: number | null, refreshExp: number | null, now: number): number | null;
1059
+
1060
+ /**
1061
+ * JWT inspection helpers (browser-safe, dependency-free).
1062
+ *
1063
+ * These decode ONLY the `exp` claim to reason about token freshness on the
1064
+ * client. They do NOT verify the signature — that is the server's job. The
1065
+ * point is purely to avoid firing doomed requests with a token we can already
1066
+ * see is malformed or expired, and to let auth bootstrap collapse a dead
1067
+ * session to the login form instead of hanging on a preloader.
1068
+ *
1069
+ * A JWT that cannot be decoded is treated as EXPIRED/invalid (fail-closed):
1070
+ * better to re-authenticate than to loop on 401s with garbage in storage.
1071
+ */
1072
+ /**
1073
+ * Extract the `exp` claim (epoch ms), or null if the token is undecodable or
1074
+ * has no numeric `exp`.
1075
+ */
1076
+ declare function getTokenExpiry(token: string | null | undefined): number | null;
1077
+ /**
1078
+ * True when the token is missing, malformed, or already past its `exp`.
1079
+ * `skewMs` treats a token expiring within the skew window as already expired
1080
+ * (default 0). Fail-closed: an undecodable token is considered expired.
1081
+ */
1082
+ declare function isTokenExpired(token: string | null | undefined, skewMs?: number, now?: number): boolean;
1083
+ /**
1084
+ * True when a valid token exists but expires within `thresholdMs` (used by the
1085
+ * proactive refresher). A missing/undecodable token returns false here — that
1086
+ * case is handled by the "expired" path, not the "expiring soon" path.
1087
+ */
1088
+ declare function isTokenExpiringSoon(token: string | null | undefined, thresholdMs: number, now?: number): boolean;
1089
+
1090
+ /**
1091
+ * Path normalization for auth routing.
1092
+ *
1093
+ * next-intl may keep a 2-letter locale prefix in the URL (e.g. `/ru/auth`), and
1094
+ * paths arrive with or without trailing slashes. To match a pathname against a
1095
+ * fixed allow-list (like `['/auth']`) reliably, strip the locale segment and
1096
+ * trailing slashes first. Extracted from useAutoAuth so it can be unit-tested
1097
+ * and reused.
1098
+ */
1099
+ /**
1100
+ * Strip a leading 2-letter locale segment and trailing slashes.
1101
+ * '/ru/auth/' → '/auth', '/auth' → '/auth', '/' → '/', '' → ''.
1102
+ */
1103
+ declare const normalizePath: (path: string | null | undefined) => string;
1104
+ /**
1105
+ * True when `pathname` (after locale/slash normalization) is one of the allowed
1106
+ * paths, or nested under one of them (`/auth` matches `/auth/callback`).
1107
+ */
1108
+ declare const isAllowedAuthPath: (pathname: string | null | undefined, allowedPaths: string[]) => boolean;
1109
+
1007
1110
  declare const logger: consola.ConsolaInstance;
1008
1111
  /**
1009
1112
  * Auth-specific logger
@@ -1053,4 +1156,4 @@ declare const Analytics: {
1053
1156
  setUser(userId: string): void;
1054
1157
  };
1055
1158
 
1056
- export { AUTH_CONSTANTS, type AccountsContextValue, AccountsProvider, Analytics, AnalyticsCategory, type AnalyticsCategoryType, AnalyticsEvent, type AnalyticsEventType, type AuthConfig, AuthContext, type AuthContextType, type AuthFormAutoSubmit, type AuthFormReturn, type AuthFormState, type AuthFormStateHandlers, type AuthFormSubmitHandlers, type AuthFormValidation, AuthProvider, type AuthProviderProps, type AuthStep, type DeleteAccountResult, type OTPRequestResult, type PatchedCfgUserUpdateRequest, PatchedCfgUserUpdateRequestSchema, type ProfileCacheOptions, type TwoFactorDevice, type TwoFactorSetupData, type UseAuthFormOptions, type UseAuthFormStateReturn, type UseAutoAuthOptions, type UseDeleteAccountReturn, type UseGithubAuthOptions, type UseGithubAuthReturn, type UseTwoFactorOptions, type UseTwoFactorReturn, type UseTwoFactorSetupOptions, type UseTwoFactorSetupReturn, type UseTwoFactorStatusReturn, type UserProfile, type WebmailLink, authLogger, clearProfileCache, decodeBase64, encodeBase64, formatAuthError, getCacheMetadata, getCachedProfile, hasValidCache, logger, setCachedProfile, useAccountsContext, useAuth, useAuthForm, useAuthFormState, useAuthGuard, useAuthRedirectManager, useAuthValidation, useAutoAuth, useBase64, useCfgRouter, useDeleteAccount, useGithubAuth, useLocalStorage, useQueryParams, useSessionStorage, useTokenRefresh, useTwoFactor, useTwoFactorSetup, useTwoFactorStatus, validateEmail, validateIdentifier };
1159
+ export { AUTH_CONSTANTS, type AccountsContextValue, AccountsProvider, Analytics, AnalyticsCategory, type AnalyticsCategoryType, AnalyticsEvent, type AnalyticsEventType, type AuthConfig, AuthContext, type AuthContextType, type AuthFormAutoSubmit, type AuthFormReturn, type AuthFormState, type AuthFormStateHandlers, type AuthFormSubmitHandlers, type AuthFormValidation, AuthProvider, type AuthProviderProps, type AuthStep, type DeleteAccountResult, type GuardInput, type OTPRequestResult, type PatchedCfgUserUpdateRequest, PatchedCfgUserUpdateRequestSchema, type ProfileCacheOptions, type TwoFactorDevice, type TwoFactorSetupData, type UseAuthFormOptions, type UseAuthFormStateReturn, type UseAutoAuthOptions, type UseDeleteAccountReturn, type UseGithubAuthOptions, type UseGithubAuthReturn, type UseTwoFactorOptions, type UseTwoFactorReturn, type UseTwoFactorSetupOptions, type UseTwoFactorSetupReturn, type UseTwoFactorStatusReturn, type UserProfile, type WebmailLink, authLogger, clearProfileCache, decodeBase64, encodeBase64, formatAuthError, getCacheMetadata, getCachedProfile, getTokenExpiry, hasValidCache, isAllowedAuthPath, isTokenExpired, isTokenExpiringSoon, logger, nextAuthEvaluationDelay, normalizePath, resolveGuardIsAuthenticated, resolveGuardIsLoading, setCachedProfile, shouldRedirectToAuth, useAccountsContext, useAuth, useAuthForm, useAuthFormState, useAuthGuard, useAuthRedirectManager, useAuthValidation, useAutoAuth, useBase64, useCfgRouter, useDeleteAccount, useGithubAuth, useLocalStorage, useQueryParams, useSessionStorage, useTokenRefresh, useTwoFactor, useTwoFactorSetup, useTwoFactorStatus, validateEmail, validateIdentifier };
package/dist/auth.mjs CHANGED
@@ -286,21 +286,28 @@ var authLogger = logger.withTag("auth");
286
286
  // src/auth/hooks/useAutoAuth.ts
287
287
  import { usePathname as usePathname2 } from "next/navigation";
288
288
  import { useEffect as useEffect3, useRef as useRef3 } from "react";
289
+
290
+ // src/auth/utils/path.ts
289
291
  var normalizePath = /* @__PURE__ */ __name((path) => {
290
292
  if (!path) return "";
291
293
  const withoutLocale = path.replace(/^\/[a-z]{2}(?=\/|$)/, "");
292
294
  const trimmed = withoutLocale.replace(/\/+$/, "");
293
295
  return trimmed || "/";
294
296
  }, "normalizePath");
297
+ var isAllowedAuthPath = /* @__PURE__ */ __name((pathname, allowedPaths) => {
298
+ const normalized = normalizePath(pathname);
299
+ return allowedPaths.some(
300
+ (p) => normalized === p || normalized.startsWith(p + "/")
301
+ );
302
+ }, "isAllowedAuthPath");
303
+
304
+ // src/auth/hooks/useAutoAuth.ts
295
305
  var useAutoAuth = /* @__PURE__ */ __name((options = {}) => {
296
306
  const { onOTPDetected, cleanupUrl = true, allowedPaths = ["/auth"] } = options;
297
307
  const queryParams = useQueryParams();
298
308
  const pathname = usePathname2();
299
309
  const router = useCfgRouter();
300
- const normalizedPath = normalizePath(pathname);
301
- const isAllowedPath = allowedPaths.some(
302
- (path) => normalizedPath === path || normalizedPath.startsWith(path + "/")
303
- );
310
+ const isAllowedPath = isAllowedAuthPath(pathname, allowedPaths);
304
311
  const queryOtp = queryParams.get("otp") || "";
305
312
  const queryEmail = queryParams.get("email") || void 0;
306
313
  const hasOTP = !!queryOtp;
@@ -3940,6 +3947,30 @@ var useDeleteAccount = /* @__PURE__ */ __name(() => {
3940
3947
  };
3941
3948
  }, "useDeleteAccount");
3942
3949
 
3950
+ // src/auth/utils/guard.ts
3951
+ function resolveGuardIsLoading(s) {
3952
+ if (!s.mounted) return true;
3953
+ if (!s.requireAuth) return false;
3954
+ return s.authLoading || s.isRedirecting || !s.isAuthenticated;
3955
+ }
3956
+ __name(resolveGuardIsLoading, "resolveGuardIsLoading");
3957
+ function shouldRedirectToAuth(s) {
3958
+ return s.mounted && s.requireAuth && !s.authLoading && !s.isAuthenticated && !s.isRedirecting;
3959
+ }
3960
+ __name(shouldRedirectToAuth, "shouldRedirectToAuth");
3961
+ function resolveGuardIsAuthenticated(s) {
3962
+ return !s.requireAuth || s.isAuthenticated;
3963
+ }
3964
+ __name(resolveGuardIsAuthenticated, "resolveGuardIsAuthenticated");
3965
+ function nextAuthEvaluationDelay(accessExp, refreshExp, now) {
3966
+ const future = [accessExp, refreshExp].filter(
3967
+ (d) => d !== null && d > now
3968
+ );
3969
+ if (future.length === 0) return null;
3970
+ return Math.max(0, Math.min(...future) - now) + 1e3;
3971
+ }
3972
+ __name(nextAuthEvaluationDelay, "nextAuthEvaluationDelay");
3973
+
3943
3974
  // src/auth/context/AccountsContext.tsx
3944
3975
  import {
3945
3976
  createContext,
@@ -4710,6 +4741,14 @@ var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken
4710
4741
  const refreshDead = isTokenExpired(refreshToken, 0, now);
4711
4742
  return refreshDead;
4712
4743
  }, "isSessionDeadOnBootstrap");
4744
+ var isSessionAlive = /* @__PURE__ */ __name((now = Date.now()) => {
4745
+ if (typeof window === "undefined") return false;
4746
+ return !isSessionDeadOnBootstrap(
4747
+ CfgAccountsApi.getToken(),
4748
+ CfgAccountsApi.getRefreshToken(),
4749
+ now
4750
+ );
4751
+ }, "isSessionAlive");
4713
4752
  var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4714
4753
  const accounts = useAccountsContext();
4715
4754
  ensureRefreshHandler();
@@ -4727,6 +4766,17 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4727
4766
  const [initialized, setInitialized] = useState12(false);
4728
4767
  const [authTick, setAuthTick] = useState12(0);
4729
4768
  const bumpAuthTick = useCallback12(() => setAuthTick((t) => t + 1), []);
4769
+ useEffect9(() => {
4770
+ if (typeof window === "undefined") return;
4771
+ const delay = nextAuthEvaluationDelay(
4772
+ getTokenExpiry(CfgAccountsApi.getToken()),
4773
+ getTokenExpiry(CfgAccountsApi.getRefreshToken()),
4774
+ Date.now()
4775
+ );
4776
+ if (delay === null) return;
4777
+ const timer = setTimeout(bumpAuthTick, Math.min(delay, 2e9));
4778
+ return () => clearTimeout(timer);
4779
+ }, [authTick, bumpAuthTick]);
4730
4780
  const onUnauthorizedRef = useRef6(false);
4731
4781
  const router = useCfgRouter();
4732
4782
  const pathname = usePathname3();
@@ -5116,10 +5166,13 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5116
5166
  () => ({
5117
5167
  user,
5118
5168
  isLoading,
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.
5122
- isAuthenticated: CfgAccountsApi.isAuthenticated(),
5169
+ // Authenticated iff the session is locally alive (access valid OR a usable
5170
+ // refresh token) validated by `exp`, NOT by mere token presence. This is
5171
+ // what makes the guard self-sufficient: an expired session reads as
5172
+ // unauthenticated WITHOUT waiting for a server 401 (which CSP / a hung
5173
+ // request / offline can swallow). `authTick` (in deps) forces recompute
5174
+ // after tokens are cleared/set, since token state lives in storage.
5175
+ isAuthenticated: isSessionAlive(),
5123
5176
  isAdminUser,
5124
5177
  loadCurrentProfile,
5125
5178
  checkAuthAndRedirect,
@@ -5268,9 +5321,18 @@ export {
5268
5321
  formatAuthError,
5269
5322
  getCacheMetadata,
5270
5323
  getCachedProfile,
5324
+ getTokenExpiry,
5271
5325
  hasValidCache,
5326
+ isAllowedAuthPath,
5327
+ isTokenExpired,
5328
+ isTokenExpiringSoon,
5272
5329
  logger,
5330
+ nextAuthEvaluationDelay,
5331
+ normalizePath,
5332
+ resolveGuardIsAuthenticated,
5333
+ resolveGuardIsLoading,
5273
5334
  setCachedProfile,
5335
+ shouldRedirectToAuth,
5274
5336
  useAccountsContext,
5275
5337
  useAuth,
5276
5338
  useAuthForm,