@djangocfg/api 2.1.455 → 2.1.457

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.
Files changed (89) hide show
  1. package/dist/auth.cjs +1611 -2148
  2. package/dist/auth.cjs.map +1 -1
  3. package/dist/auth.d.cts +37 -82
  4. package/dist/auth.d.ts +37 -82
  5. package/dist/auth.mjs +1581 -2118
  6. package/dist/auth.mjs.map +1 -1
  7. package/dist/chunk-2G67QRNU.mjs +2096 -0
  8. package/dist/chunk-2G67QRNU.mjs.map +1 -0
  9. package/dist/chunk-32SRQGAC.mjs +2109 -0
  10. package/dist/chunk-32SRQGAC.mjs.map +1 -0
  11. package/dist/chunk-4BPRCONN.mjs +2098 -0
  12. package/dist/chunk-4BPRCONN.mjs.map +1 -0
  13. package/dist/chunk-5UZ2Z323.mjs +2021 -0
  14. package/dist/chunk-5UZ2Z323.mjs.map +1 -0
  15. package/dist/chunk-7KFTJXNM.mjs +2097 -0
  16. package/dist/chunk-7KFTJXNM.mjs.map +1 -0
  17. package/dist/chunk-BK4K5CVT.mjs +2019 -0
  18. package/dist/chunk-BK4K5CVT.mjs.map +1 -0
  19. package/dist/chunk-JLPJZ6WB.mjs +2020 -0
  20. package/dist/chunk-JLPJZ6WB.mjs.map +1 -0
  21. package/dist/chunk-TVU6PYJH.mjs +2032 -0
  22. package/dist/chunk-TVU6PYJH.mjs.map +1 -0
  23. package/dist/clients.cjs +292 -47
  24. package/dist/clients.cjs.map +1 -1
  25. package/dist/clients.d.cts +23 -0
  26. package/dist/clients.d.ts +23 -0
  27. package/dist/clients.mjs +292 -47
  28. package/dist/clients.mjs.map +1 -1
  29. package/dist/hooks.cjs +200 -32
  30. package/dist/hooks.cjs.map +1 -1
  31. package/dist/hooks.mjs +200 -32
  32. package/dist/hooks.mjs.map +1 -1
  33. package/dist/index.cjs +292 -47
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +81 -4
  36. package/dist/index.d.ts +81 -4
  37. package/dist/index.mjs +292 -47
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/sdk.gen-2SQOPTWJ.mjs +26 -0
  40. package/dist/sdk.gen-2SQOPTWJ.mjs.map +1 -0
  41. package/dist/sdk.gen-HSGK4C5S.mjs +26 -0
  42. package/dist/sdk.gen-HSGK4C5S.mjs.map +1 -0
  43. package/dist/sdk.gen-NUK2VGHO.mjs +25 -0
  44. package/dist/sdk.gen-NUK2VGHO.mjs.map +1 -0
  45. package/dist/sdk.gen-O4KAQUZB.mjs +26 -0
  46. package/dist/sdk.gen-O4KAQUZB.mjs.map +1 -0
  47. package/dist/sdk.gen-PPAVSBNT.mjs +26 -0
  48. package/dist/sdk.gen-PPAVSBNT.mjs.map +1 -0
  49. package/dist/sdk.gen-XLHLOCJ2.mjs +25 -0
  50. package/dist/sdk.gen-XLHLOCJ2.mjs.map +1 -0
  51. package/dist/sdk.gen-ZOE6NQAL.mjs +26 -0
  52. package/dist/sdk.gen-ZOE6NQAL.mjs.map +1 -0
  53. package/dist/sdk.gen-ZT7LGJVO.mjs +26 -0
  54. package/dist/sdk.gen-ZT7LGJVO.mjs.map +1 -0
  55. package/package.json +2 -2
  56. package/src/_api/generated/_cfg_accounts/hooks/index.ts +1 -0
  57. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenBlacklistCreate.ts +28 -0
  58. package/src/_api/generated/_cfg_accounts/openapi.json +99 -0
  59. package/src/_api/generated/_cfg_accounts/schemas/TokenBlacklistRequest.ts +11 -0
  60. package/src/_api/generated/_cfg_accounts/schemas/index.ts +1 -0
  61. package/src/_api/generated/_cfg_centrifugo/openapi.json +9 -0
  62. package/src/_api/generated/_cfg_totp/openapi.json +33 -0
  63. package/src/_api/generated/helpers/auth.ts +288 -31
  64. package/src/_api/generated/openapi.json +129 -0
  65. package/src/_api/generated/sdk.gen.ts +94 -16
  66. package/src/_api/generated/types.gen.ts +18 -0
  67. package/src/auth/__tests__/guard.test.ts +0 -31
  68. package/src/auth/constants.ts +6 -0
  69. package/src/auth/context/AccountsContext.tsx +14 -24
  70. package/src/auth/context/AuthContext.tsx +263 -607
  71. package/src/auth/context/types.ts +13 -0
  72. package/src/auth/hooks/index.ts +8 -5
  73. package/src/auth/hooks/useAuthRedirect.ts +17 -0
  74. package/src/auth/hooks/useGithubAuth.ts +3 -3
  75. package/src/auth/hooks/useSession.ts +22 -0
  76. package/src/auth/hooks/useTwoFactor.ts +8 -6
  77. package/src/auth/middlewares/index.ts +5 -6
  78. package/src/auth/utils/guard.ts +0 -22
  79. package/src/auth/utils/index.ts +0 -2
  80. package/src/_api/generated/_cfg_accounts/events.ts +0 -198
  81. package/src/_api/generated/_cfg_centrifugo/events.ts +0 -198
  82. package/src/_api/generated/_cfg_totp/events.ts +0 -198
  83. package/src/auth/__tests__/jwt.test.ts +0 -119
  84. package/src/auth/__tests__/sessionBootstrap.test.ts +0 -111
  85. package/src/auth/__tests__/useTokenRefresh.dom.test.tsx +0 -167
  86. package/src/auth/hooks/useAuthGuard.ts +0 -35
  87. package/src/auth/hooks/useTokenRefresh.ts +0 -131
  88. package/src/auth/refreshHandler.ts +0 -79
  89. package/src/auth/utils/jwt.ts +0 -66
package/dist/auth.mjs CHANGED
@@ -12,905 +12,1077 @@ var AUTH_CONSTANTS = {
12
12
  BACKUP_CODE_MAX_LENGTH: 12
13
13
  };
14
14
 
15
- // src/auth/utils/env.ts
16
- var isDev = process.env.NODE_ENV === "development";
17
- var isBrowser = typeof window !== "undefined";
18
- var isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === "true";
19
- var dpopEnabled = process.env.NEXT_PUBLIC_DPOP_ENABLED === "true";
20
-
21
15
  // src/auth/context/AuthContext.tsx
22
16
  import { usePathname as usePathname3 } from "next/navigation";
23
17
  import {
24
18
  createContext as createContext2,
25
19
  memo as memo2,
26
- useCallback as useCallback12,
20
+ useCallback as useCallback11,
27
21
  useContext as useContext2,
28
- useEffect as useEffect9,
22
+ useEffect as useEffect7,
29
23
  useMemo as useMemo3,
30
24
  useRef as useRef6,
31
- useState as useState12
25
+ useState as useState11
32
26
  } from "react";
33
- import { SWRConfig } from "swr";
34
27
 
35
- // src/auth/hooks/useCfgRouter.ts
36
- import { useRouter as useNextRouter } from "next/navigation";
37
- import { useCallback, useMemo } from "react";
38
- function getBasePath() {
39
- if (typeof process === "undefined") {
40
- return "";
28
+ // src/_api/generated/helpers/errors.ts
29
+ var APIError = class extends Error {
30
+ constructor(statusCode, statusText, response, url, message) {
31
+ super(message || `HTTP ${statusCode}: ${statusText}`);
32
+ this.statusCode = statusCode;
33
+ this.statusText = statusText;
34
+ this.response = response;
35
+ this.url = url;
36
+ this.name = "APIError";
41
37
  }
42
- return process.env.NEXT_PUBLIC_BASE_PATH || "";
43
- }
44
- __name(getBasePath, "getBasePath");
45
- function withBasePath(path, basePath) {
46
- if (!basePath) {
47
- return path;
38
+ static {
39
+ __name(this, "APIError");
48
40
  }
49
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
50
- const normalizedBasePath = basePath.replace(/\/$/, "");
51
- return `${normalizedBasePath}${normalizedPath}`;
41
+ get details() {
42
+ if (typeof this.response === "object" && this.response !== null) {
43
+ return this.response;
44
+ }
45
+ return null;
46
+ }
47
+ get fieldErrors() {
48
+ const details = this.details;
49
+ if (!details) return null;
50
+ const fieldErrors = {};
51
+ for (const [key, value] of Object.entries(details)) {
52
+ if (Array.isArray(value)) fieldErrors[key] = value;
53
+ }
54
+ return Object.keys(fieldErrors).length > 0 ? fieldErrors : null;
55
+ }
56
+ get errorMessage() {
57
+ const details = this.details;
58
+ if (!details) return this.message;
59
+ if (details.detail) {
60
+ return Array.isArray(details.detail) ? details.detail.join(", ") : String(details.detail);
61
+ }
62
+ if (details.error) return String(details.error);
63
+ if (details.message) return String(details.message);
64
+ const fieldErrors = this.fieldErrors;
65
+ if (fieldErrors) {
66
+ const firstField = Object.keys(fieldErrors)[0];
67
+ if (firstField) return `${firstField}: ${fieldErrors[firstField]?.join(", ")}`;
68
+ }
69
+ return this.message;
70
+ }
71
+ get isValidationError() {
72
+ return this.statusCode === 400;
73
+ }
74
+ get isAuthError() {
75
+ return this.statusCode === 401;
76
+ }
77
+ get isPermissionError() {
78
+ return this.statusCode === 403;
79
+ }
80
+ get isNotFoundError() {
81
+ return this.statusCode === 404;
82
+ }
83
+ get isServerError() {
84
+ return this.statusCode >= 500 && this.statusCode < 600;
85
+ }
86
+ };
87
+
88
+ // src/_api/generated/helpers/auth.ts
89
+ var ACCESS_KEY = "cfg.access_token";
90
+ var REFRESH_KEY = "cfg.refresh_token";
91
+ var API_KEY_KEY = "cfg.api_key";
92
+ var isBrowser = typeof window !== "undefined";
93
+ var localStorageBackend = {
94
+ get(key) {
95
+ if (!isBrowser) return null;
96
+ try {
97
+ return window.localStorage.getItem(key);
98
+ } catch {
99
+ return null;
100
+ }
101
+ },
102
+ set(key, value) {
103
+ if (!isBrowser) return;
104
+ try {
105
+ if (value === null) window.localStorage.removeItem(key);
106
+ else window.localStorage.setItem(key, value);
107
+ } catch {
108
+ }
109
+ }
110
+ };
111
+ var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
112
+ var cookieBackend = {
113
+ get(key) {
114
+ if (!isBrowser) return null;
115
+ try {
116
+ const re = new RegExp(`(?:^|;\\s*)${encodeURIComponent(key)}=([^;]*)`);
117
+ const m = document.cookie.match(re);
118
+ return m ? decodeURIComponent(m[1]) : null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ },
123
+ set(key, value) {
124
+ if (!isBrowser) return;
125
+ try {
126
+ const k = encodeURIComponent(key);
127
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
128
+ if (value === null) {
129
+ document.cookie = `${k}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
130
+ } else {
131
+ const v = encodeURIComponent(value);
132
+ document.cookie = `${k}=${v}; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax${secure}`;
133
+ }
134
+ } catch {
135
+ }
136
+ }
137
+ };
138
+ var _storage = localStorageBackend;
139
+ var _storageMode = "localStorage";
140
+ function detectLocale() {
141
+ try {
142
+ if (typeof document !== "undefined") {
143
+ const m = document.cookie.match(/(?:^|;\s*)NEXT_LOCALE=([^;]*)/);
144
+ if (m) return decodeURIComponent(m[1]);
145
+ }
146
+ if (typeof navigator !== "undefined" && navigator.language) {
147
+ return navigator.language;
148
+ }
149
+ } catch {
150
+ }
151
+ return null;
52
152
  }
53
- __name(withBasePath, "withBasePath");
54
- function useCfgRouter() {
55
- const router = useNextRouter();
56
- const basePath = useMemo(() => getBasePath(), []);
57
- const isStaticBuild2 = useMemo(() => {
58
- return typeof process !== "undefined" && process.env.NEXT_PUBLIC_STATIC_BUILD === "true";
59
- }, []);
60
- const push = useCallback((href, options) => {
61
- if (basePath) {
62
- window.location.href = withBasePath(href, basePath);
63
- } else {
64
- router.push(href, options);
153
+ __name(detectLocale, "detectLocale");
154
+ function defaultBaseUrl() {
155
+ if (typeof window !== "undefined") {
156
+ try {
157
+ if (typeof process !== "undefined" && process.env) {
158
+ if (process.env.NEXT_PUBLIC_STATIC_BUILD === "true") return "";
159
+ if (process.env.NEXT_PUBLIC_API_PROXY_URL !== void 0)
160
+ return process.env.NEXT_PUBLIC_API_PROXY_URL;
161
+ return process.env.NEXT_PUBLIC_API_URL || "";
162
+ }
163
+ } catch {
65
164
  }
66
- }, [router, basePath]);
67
- const replace = useCallback((href, options) => {
68
- if (basePath) {
69
- window.location.replace(withBasePath(href, basePath));
70
- } else {
71
- router.replace(href, options);
165
+ return "";
166
+ }
167
+ try {
168
+ if (typeof process !== "undefined" && process.env) {
169
+ if (process.env.NEXT_PUBLIC_STATIC_BUILD === "true") return "";
170
+ return process.env.NEXT_PUBLIC_API_URL || "";
72
171
  }
73
- }, [router, basePath]);
74
- const hardPush = useCallback((href) => {
75
- window.location.href = withBasePath(href, basePath);
76
- }, [basePath]);
77
- const hardReplace = useCallback((href) => {
78
- window.location.replace(withBasePath(href, basePath));
79
- }, [basePath]);
80
- const prefetch = useCallback((href) => {
81
- router.prefetch(href);
82
- }, [router]);
83
- const back = useCallback(() => {
84
- router.back();
85
- }, [router]);
86
- const forward = useCallback(() => {
87
- router.forward();
88
- }, [router]);
89
- const refresh = useCallback(() => {
90
- router.refresh();
91
- }, [router]);
172
+ } catch {
173
+ }
174
+ return "";
175
+ }
176
+ __name(defaultBaseUrl, "defaultBaseUrl");
177
+ function defaultApiKey() {
178
+ try {
179
+ if (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_KEY) {
180
+ return process.env.NEXT_PUBLIC_API_KEY;
181
+ }
182
+ } catch {
183
+ }
184
+ return null;
185
+ }
186
+ __name(defaultApiKey, "defaultApiKey");
187
+ var _localeOverride = null;
188
+ var _apiKeyOverride = null;
189
+ var _baseUrlOverride = null;
190
+ var _withCredentials = true;
191
+ var _onUnauthorized = null;
192
+ var _refreshHandler = null;
193
+ var _refreshInflight = null;
194
+ var RETRY_MARKER = "X-Auth-Retry";
195
+ function jwtExpMs(token) {
196
+ try {
197
+ const payload = token.split(".")[1];
198
+ if (!payload) return null;
199
+ const json = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
200
+ return typeof json.exp === "number" ? json.exp * 1e3 : null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ __name(jwtExpMs, "jwtExpMs");
206
+ function computeSnapshot() {
207
+ const access = _storage.get(ACCESS_KEY);
208
+ const refresh = _storage.get(REFRESH_KEY);
209
+ const now = Date.now();
210
+ const accessExp = access ? jwtExpMs(access) : null;
211
+ const accessAlive = access !== null && (accessExp === null || accessExp > now);
212
+ const refreshExp = refresh ? jwtExpMs(refresh) : null;
213
+ const refreshAlive = refresh !== null && (refreshExp === null || refreshExp > now);
92
214
  return {
93
- push,
94
- replace,
95
- hardPush,
96
- hardReplace,
97
- prefetch,
98
- back,
99
- forward,
100
- refresh
215
+ status: accessAlive || refreshAlive ? "authenticated" : "anonymous",
216
+ accessExpiresAt: accessExp
101
217
  };
102
218
  }
103
- __name(useCfgRouter, "useCfgRouter");
104
-
105
- // src/auth/hooks/useQueryParams.ts
106
- import { usePathname } from "next/navigation";
107
- import { useEffect, useRef, useState } from "react";
108
- function useQueryParams() {
109
- const pathname = usePathname();
110
- const [queryParams, setQueryParams] = useState(() => {
111
- if (typeof window === "undefined") {
112
- return new URLSearchParams();
219
+ __name(computeSnapshot, "computeSnapshot");
220
+ var SERVER_SNAPSHOT = { status: "anonymous", accessExpiresAt: null };
221
+ var SESSION_SYNC_EVENT = `cfg-auth:changed:${ACCESS_KEY}`;
222
+ var _snapshot = computeSnapshot();
223
+ var _sessionListeners = /* @__PURE__ */ new Set();
224
+ var _expiryTimer = null;
225
+ var _storageListenerInstalled = false;
226
+ function scheduleExpiryFlip() {
227
+ if (!isBrowser) return;
228
+ if (_expiryTimer !== null) {
229
+ clearTimeout(_expiryTimer);
230
+ _expiryTimer = null;
231
+ }
232
+ if (_sessionListeners.size === 0) return;
233
+ const now = Date.now();
234
+ const exps = [];
235
+ const access = _storage.get(ACCESS_KEY);
236
+ const refresh = _storage.get(REFRESH_KEY);
237
+ const accessExp = access ? jwtExpMs(access) : null;
238
+ const refreshExp = refresh ? jwtExpMs(refresh) : null;
239
+ if (accessExp !== null && accessExp > now) exps.push(accessExp);
240
+ if (refreshExp !== null && refreshExp > now) exps.push(refreshExp);
241
+ if (!exps.length) return;
242
+ const delay = Math.min(Math.min(...exps) - now + 250, 2147483647);
243
+ _expiryTimer = setTimeout(notifySessionChanged, delay);
244
+ }
245
+ __name(scheduleExpiryFlip, "scheduleExpiryFlip");
246
+ function notifySessionChanged() {
247
+ const next = computeSnapshot();
248
+ const changed = next.status !== _snapshot.status || next.accessExpiresAt !== _snapshot.accessExpiresAt;
249
+ if (changed) _snapshot = next;
250
+ scheduleExpiryFlip();
251
+ if (!changed) return;
252
+ for (const listener of Array.from(_sessionListeners)) {
253
+ try {
254
+ listener();
255
+ } catch {
256
+ }
257
+ }
258
+ if (isBrowser) {
259
+ try {
260
+ window.dispatchEvent(new Event(SESSION_SYNC_EVENT));
261
+ } catch {
262
+ }
263
+ }
264
+ }
265
+ __name(notifySessionChanged, "notifySessionChanged");
266
+ function ensureStorageSync() {
267
+ if (!isBrowser || _storageListenerInstalled) return;
268
+ _storageListenerInstalled = true;
269
+ window.addEventListener("storage", (e) => {
270
+ if (e.key === null || e.key === ACCESS_KEY || e.key === REFRESH_KEY) {
271
+ notifySessionChanged();
113
272
  }
114
- return new URLSearchParams(window.location.search);
115
273
  });
116
- const lastSearchRef = useRef("");
117
- useEffect(() => {
118
- if (typeof window === "undefined") return;
119
- const updateQueryParams = /* @__PURE__ */ __name(() => {
120
- const currentSearch = window.location.search;
121
- if (currentSearch !== lastSearchRef.current) {
122
- lastSearchRef.current = currentSearch;
123
- setQueryParams(new URLSearchParams(currentSearch));
274
+ window.addEventListener(SESSION_SYNC_EVENT, () => notifySessionChanged());
275
+ }
276
+ __name(ensureStorageSync, "ensureStorageSync");
277
+ var _sessionExpiredHandlers = /* @__PURE__ */ new Set();
278
+ var _client = null;
279
+ function pushClientConfig() {
280
+ if (!_client) return;
281
+ _client.setConfig({
282
+ baseUrl: auth.getBaseUrl(),
283
+ credentials: _withCredentials ? "include" : "same-origin"
284
+ });
285
+ }
286
+ __name(pushClientConfig, "pushClientConfig");
287
+ var auth = {
288
+ // ── Storage mode ──────────────────────────────────────────────────
289
+ getStorageMode() {
290
+ return _storageMode;
291
+ },
292
+ setStorageMode(mode) {
293
+ _storageMode = mode;
294
+ _storage = mode === "cookie" ? cookieBackend : localStorageBackend;
295
+ notifySessionChanged();
296
+ },
297
+ // ── Bearer token ──────────────────────────────────────────────────
298
+ getToken() {
299
+ return _storage.get(ACCESS_KEY);
300
+ },
301
+ setToken(token) {
302
+ _storage.set(ACCESS_KEY, token);
303
+ notifySessionChanged();
304
+ },
305
+ getRefreshToken() {
306
+ return _storage.get(REFRESH_KEY);
307
+ },
308
+ setRefreshToken(token) {
309
+ _storage.set(REFRESH_KEY, token);
310
+ notifySessionChanged();
311
+ },
312
+ clearTokens() {
313
+ _storage.set(ACCESS_KEY, null);
314
+ _storage.set(REFRESH_KEY, null);
315
+ notifySessionChanged();
316
+ },
317
+ /** Session-aware: token PRESENT and not past its `exp` (or a live refresh
318
+ * token exists that can mint one). Prefer `getSnapshot().status` in React. */
319
+ isAuthenticated() {
320
+ return computeSnapshot().status === "authenticated";
321
+ },
322
+ // ── Session (the ONE write path for login/logout flows) ──────────
323
+ /**
324
+ * Persist a token pair atomically. Every login flow (OTP verify, 2FA,
325
+ * OAuth callback) and the refresh handler should end here — do NOT
326
+ * scatter `setToken`/`setRefreshToken` pairs through app code.
327
+ * `refresh: undefined` keeps the current refresh token (access-only
328
+ * rotation); `refresh: null` explicitly drops it.
329
+ */
330
+ setSession(tokens) {
331
+ _storage.set(ACCESS_KEY, tokens.access);
332
+ if (tokens.refresh !== void 0) _storage.set(REFRESH_KEY, tokens.refresh);
333
+ notifySessionChanged();
334
+ },
335
+ clearSession() {
336
+ auth.clearTokens();
337
+ },
338
+ // ── Reactive snapshot (for useSyncExternalStore) ──────────────────
339
+ /**
340
+ * @example React:
341
+ * const session = useSyncExternalStore(
342
+ * auth.subscribe, auth.getSnapshot, auth.getServerSnapshot,
343
+ * );
344
+ * const isAuthenticated = session.status === 'authenticated';
345
+ */
346
+ getSnapshot() {
347
+ return _snapshot;
348
+ },
349
+ getServerSnapshot() {
350
+ return SERVER_SNAPSHOT;
351
+ },
352
+ subscribe(listener) {
353
+ ensureStorageSync();
354
+ _sessionListeners.add(listener);
355
+ notifySessionChanged();
356
+ return () => {
357
+ _sessionListeners.delete(listener);
358
+ if (_sessionListeners.size === 0 && _expiryTimer !== null) {
359
+ clearTimeout(_expiryTimer);
360
+ _expiryTimer = null;
124
361
  }
125
- }, "updateQueryParams");
126
- updateQueryParams();
127
- window.addEventListener("popstate", updateQueryParams);
128
- const origPush = window.history.pushState;
129
- const origReplace = window.history.replaceState;
130
- window.history.pushState = function(...args) {
131
- const result = origPush.apply(this, args);
132
- updateQueryParams();
133
- return result;
134
- };
135
- window.history.replaceState = function(...args) {
136
- const result = origReplace.apply(this, args);
137
- updateQueryParams();
138
- return result;
139
362
  };
363
+ },
364
+ /**
365
+ * Fired on TERMINAL 401 — after the refresh+retry path is exhausted.
366
+ * The store has already cleared the session before calling back, so
367
+ * handlers only need to route to login (no clear-then-redirect
368
+ * ordering to get wrong). Returns an unsubscribe function; multiple
369
+ * handlers compose (unlike the legacy single-slot `onUnauthorized`).
370
+ */
371
+ onSessionExpired(cb) {
372
+ _sessionExpiredHandlers.add(cb);
140
373
  return () => {
141
- window.removeEventListener("popstate", updateQueryParams);
142
- window.history.pushState = origPush;
143
- window.history.replaceState = origReplace;
374
+ _sessionExpiredHandlers.delete(cb);
144
375
  };
145
- }, [pathname]);
146
- return queryParams;
376
+ },
377
+ // ── API key ───────────────────────────────────────────────────────
378
+ getApiKey() {
379
+ return _apiKeyOverride ?? _storage.get(API_KEY_KEY) ?? defaultApiKey();
380
+ },
381
+ setApiKey(key) {
382
+ _apiKeyOverride = key;
383
+ },
384
+ setApiKeyPersist(key) {
385
+ _apiKeyOverride = key;
386
+ _storage.set(API_KEY_KEY, key);
387
+ },
388
+ clearApiKey() {
389
+ _apiKeyOverride = null;
390
+ _storage.set(API_KEY_KEY, null);
391
+ },
392
+ // ── Locale ────────────────────────────────────────────────────────
393
+ getLocale() {
394
+ return _localeOverride ?? detectLocale();
395
+ },
396
+ setLocale(locale) {
397
+ _localeOverride = locale;
398
+ },
399
+ // ── Base URL ──────────────────────────────────────────────────────
400
+ getBaseUrl() {
401
+ const url = _baseUrlOverride ?? defaultBaseUrl();
402
+ return url.replace(/\/$/, "");
403
+ },
404
+ setBaseUrl(url) {
405
+ _baseUrlOverride = url ? url.replace(/\/$/, "") : null;
406
+ pushClientConfig();
407
+ },
408
+ // ── Credentials toggle ────────────────────────────────────────────
409
+ getWithCredentials() {
410
+ return _withCredentials;
411
+ },
412
+ setWithCredentials(value) {
413
+ _withCredentials = value;
414
+ pushClientConfig();
415
+ },
416
+ // ── 401 handler ───────────────────────────────────────────────────
417
+ /**
418
+ * Fired when the server returns 401 AND no refresh path recovers it
419
+ * (no refresh token, no refresh handler, refresh failed, or retry
420
+ * still 401). The app should clear local state and redirect to login.
421
+ *
422
+ * NOT fired for 401 that gets transparently recovered by the refresh
423
+ * handler — those are invisible to callers.
424
+ */
425
+ onUnauthorized(cb) {
426
+ _onUnauthorized = cb;
427
+ },
428
+ /**
429
+ * Register the refresh strategy. The handler receives the current
430
+ * refresh token and must call your refresh endpoint, returning
431
+ * `{ access, refresh? }` on success or `null` on failure.
432
+ *
433
+ * @example
434
+ * auth.setRefreshHandler(async (refresh) => {
435
+ * const { data } = await Auth.tokenRefreshCreate({ body: { refresh } });
436
+ * return data ? { access: data.access, refresh: data.refresh } : null;
437
+ * });
438
+ */
439
+ setRefreshHandler(fn) {
440
+ _refreshHandler = fn;
441
+ },
442
+ /**
443
+ * Proactively run the registered refresh handler right now, reusing the
444
+ * SAME single-flight promise as the 401-recovery interceptor. Callers
445
+ * (e.g. an expiry timer / focus / reconnect) get token rotation,
446
+ * de-duplication and rotated-token persistence for free — they must NOT
447
+ * re-implement any of it. Returns the fresh access token, or null if
448
+ * there is no handler / no refresh token / the refresh failed.
449
+ */
450
+ refreshNow() {
451
+ return tryRefresh();
452
+ }
453
+ };
454
+ async function tryRefresh() {
455
+ if (_refreshInflight) return _refreshInflight;
456
+ if (!_refreshHandler) return null;
457
+ const runRefresh = /* @__PURE__ */ __name(async () => {
458
+ const refresh = auth.getRefreshToken();
459
+ if (!refresh) return null;
460
+ const result = await _refreshHandler(refresh);
461
+ if (!result?.access) return null;
462
+ auth.setToken(result.access);
463
+ if (result.refresh) auth.setRefreshToken(result.refresh);
464
+ return result.access;
465
+ }, "runRefresh");
466
+ _refreshInflight = (async () => {
467
+ try {
468
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
469
+ if (locks?.request) {
470
+ return await locks.request(`${REFRESH_KEY}:refresh`, runRefresh);
471
+ }
472
+ return await runRefresh();
473
+ } catch {
474
+ return null;
475
+ } finally {
476
+ _refreshInflight = null;
477
+ }
478
+ })();
479
+ return _refreshInflight;
147
480
  }
148
- __name(useQueryParams, "useQueryParams");
149
-
150
- // src/auth/hooks/useAuthFormState.ts
151
- import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
152
- var formatCountdown = /* @__PURE__ */ __name((s) => {
153
- if (s <= 0) return "";
154
- const m = Math.floor(s / 60);
155
- return m > 0 ? `${m}:${String(s % 60).padStart(2, "0")}` : `${s}s`;
156
- }, "formatCountdown");
157
- var useAuthFormState = /* @__PURE__ */ __name((initialIdentifier = "") => {
158
- const [identifier, setIdentifier] = useState2(initialIdentifier);
159
- const [otp, setOtp] = useState2("");
160
- const [isLoading, setIsLoading] = useState2(false);
161
- const [acceptedTerms, setAcceptedTerms] = useState2(true);
162
- const [step, setStep] = useState2("identifier");
163
- const [error, setError] = useState2("");
164
- const [webmail, setWebmail] = useState2(null);
165
- const [twoFactorSessionId, setTwoFactorSessionId] = useState2(null);
166
- const [shouldPrompt2FA, setShouldPrompt2FA] = useState2(false);
167
- const [twoFactorCode, setTwoFactorCode] = useState2("");
168
- const [useBackupCode, setUseBackupCode] = useState2(false);
169
- const [rateLimitSeconds, setRateLimitSeconds] = useState2(0);
170
- const rateLimitTimerRef = useRef2(null);
171
- const startRateLimitCountdown = useCallback2((seconds) => {
172
- if (rateLimitTimerRef.current) clearInterval(rateLimitTimerRef.current);
173
- setRateLimitSeconds(seconds);
174
- rateLimitTimerRef.current = setInterval(() => {
175
- setRateLimitSeconds((prev) => {
176
- if (prev <= 1) {
177
- clearInterval(rateLimitTimerRef.current);
178
- rateLimitTimerRef.current = null;
179
- return 0;
180
- }
181
- return prev - 1;
182
- });
183
- }, 1e3);
184
- }, []);
185
- useEffect2(() => () => {
186
- if (rateLimitTimerRef.current) clearInterval(rateLimitTimerRef.current);
187
- }, []);
188
- const clearError = useCallback2(() => setError(""), []);
189
- return {
190
- // State
191
- identifier,
192
- otp,
193
- isLoading,
194
- acceptedTerms,
195
- step,
196
- error,
197
- twoFactorSessionId,
198
- shouldPrompt2FA,
199
- twoFactorCode,
200
- useBackupCode,
201
- rateLimitSeconds,
202
- isRateLimited: rateLimitSeconds > 0,
203
- rateLimitLabel: formatCountdown(rateLimitSeconds),
204
- webmail,
205
- // Handlers
206
- setIdentifier,
207
- setOtp,
208
- setAcceptedTerms,
209
- setError,
210
- clearError,
211
- setStep,
212
- setIsLoading,
213
- setTwoFactorSessionId,
214
- setShouldPrompt2FA,
215
- setTwoFactorCode,
216
- setUseBackupCode,
217
- startRateLimitCountdown,
218
- setWebmail
219
- };
220
- }, "useAuthFormState");
221
-
222
- // src/auth/hooks/useAuthValidation.ts
223
- import { useCallback as useCallback3 } from "react";
224
- var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
225
- var useAuthValidation = /* @__PURE__ */ __name(() => {
226
- const validateIdentifier2 = useCallback3((id) => {
227
- return EMAIL_REGEX.test(id);
228
- }, []);
229
- return { validateIdentifier: validateIdentifier2 };
230
- }, "useAuthValidation");
231
- var validateIdentifier = /* @__PURE__ */ __name((id) => EMAIL_REGEX.test(id), "validateIdentifier");
232
-
233
- // src/auth/hooks/useAuthForm.ts
234
- import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4 } from "react";
235
-
236
- // src/auth/utils/logger.ts
237
- import { createConsola } from "consola";
238
-
239
- // src/log-control.ts
240
- var verboseByDefault = isDev || isStaticBuild;
241
- var DEFAULT_LEVEL = verboseByDefault ? 4 : 1;
242
- var _level = DEFAULT_LEVEL;
243
- var _subscribers = /* @__PURE__ */ new Set();
244
- function getLogLevel() {
245
- return _level;
246
- }
247
- __name(getLogLevel, "getLogLevel");
248
- function setLogLevel(level) {
249
- _level = level;
250
- for (const fn of _subscribers) {
481
+ __name(tryRefresh, "tryRefresh");
482
+ function expireSession(response) {
483
+ auth.clearTokens();
484
+ for (const cb of Array.from(_sessionExpiredHandlers)) {
251
485
  try {
252
- fn(level);
486
+ cb(response);
487
+ } catch {
488
+ }
489
+ }
490
+ if (_onUnauthorized) {
491
+ try {
492
+ _onUnauthorized(response);
253
493
  } catch {
254
494
  }
255
495
  }
256
496
  }
257
- __name(setLogLevel, "setLogLevel");
258
- function onLogLevelChange(fn) {
259
- _subscribers.add(fn);
497
+ __name(expireSession, "expireSession");
498
+ function dpopEnabled() {
260
499
  try {
261
- fn(_level);
500
+ return typeof process !== "undefined" && process.env?.NEXT_PUBLIC_DPOP_ENABLED === "true";
262
501
  } catch {
502
+ return false;
263
503
  }
264
- return () => _subscribers.delete(fn);
265
504
  }
266
- __name(onLogLevelChange, "onLogLevelChange");
267
- function applyRoleLogPolicy(opts) {
268
- const devMode = opts.isDev ?? verboseByDefault;
269
- if (opts.isAdmin || devMode) {
270
- setLogLevel(4);
271
- } else {
272
- setLogLevel(1);
273
- }
505
+ __name(dpopEnabled, "dpopEnabled");
506
+ var _DPOP_DB = "cfg-auth";
507
+ var _DPOP_STORE = "keys";
508
+ var _DPOP_KEY_ID = "dpop-ec-p256";
509
+ function _idbOpen() {
510
+ return new Promise((resolve, reject) => {
511
+ const req = indexedDB.open(_DPOP_DB, 1);
512
+ req.onupgradeneeded = () => req.result.createObjectStore(_DPOP_STORE);
513
+ req.onsuccess = () => resolve(req.result);
514
+ req.onerror = () => reject(req.error);
515
+ });
274
516
  }
275
- __name(applyRoleLogPolicy, "applyRoleLogPolicy");
276
-
277
- // src/auth/utils/logger.ts
278
- var logger = createConsola({
279
- level: getLogLevel()
280
- }).withTag("api");
281
- onLogLevelChange((level) => {
282
- logger.level = level;
283
- });
284
- var authLogger = logger.withTag("auth");
285
-
286
- // src/auth/hooks/useAutoAuth.ts
287
- import { usePathname as usePathname2 } from "next/navigation";
288
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
289
-
290
- // src/auth/utils/path.ts
291
- var normalizePath = /* @__PURE__ */ __name((path) => {
292
- if (!path) return "";
293
- const withoutLocale = path.replace(/^\/[a-z]{2}(?=\/|$)/, "");
294
- const trimmed = withoutLocale.replace(/\/+$/, "");
295
- return trimmed || "/";
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
305
- var useAutoAuth = /* @__PURE__ */ __name((options = {}) => {
306
- const { onOTPDetected, cleanupUrl = true, allowedPaths = ["/auth"] } = options;
307
- const queryParams = useQueryParams();
308
- const pathname = usePathname2();
309
- const router = useCfgRouter();
310
- const isAllowedPath = isAllowedAuthPath(pathname, allowedPaths);
311
- const queryOtp = queryParams.get("otp") || "";
312
- const queryEmail = queryParams.get("email") || void 0;
313
- const hasOTP = !!queryOtp;
314
- const isReady = !!pathname && hasOTP && isAllowedPath;
315
- const otpValid = queryOtp.length === AUTH_CONSTANTS.EMAIL_OTP_LENGTH;
316
- const onOTPDetectedRef = useRef3(onOTPDetected);
317
- onOTPDetectedRef.current = onOTPDetected;
318
- useEffect3(() => {
319
- if (!isReady || !otpValid) return;
320
- authLogger.info("OTP detected in URL on auth page:", queryOtp);
321
- onOTPDetectedRef.current?.(queryOtp, queryEmail);
322
- }, [isReady, otpValid, queryOtp, queryEmail]);
323
- useEffect3(() => {
324
- if (!isReady || !cleanupUrl || !queryOtp) return;
325
- const cleanQuery = Object.fromEntries(queryParams.entries());
326
- delete cleanQuery.otp;
327
- delete cleanQuery.email;
328
- const queryString = new URLSearchParams(cleanQuery).toString();
329
- const realPathname = typeof window !== "undefined" ? window.location.pathname : pathname;
330
- router.replace(queryString ? `${realPathname}?${queryString}` : realPathname);
331
- }, [isReady, cleanupUrl, queryOtp]);
332
- return {
333
- isReady,
334
- hasOTP,
335
- isAllowedPath
336
- };
337
- }, "useAutoAuth");
338
-
339
- // src/auth/hooks/useTwoFactor.ts
340
- import { useCallback as useCallback4, useState as useState3 } from "react";
341
-
342
- // src/_api/generated/helpers/errors.ts
343
- var APIError = class extends Error {
344
- constructor(statusCode, statusText, response, url, message) {
345
- super(message || `HTTP ${statusCode}: ${statusText}`);
346
- this.statusCode = statusCode;
347
- this.statusText = statusText;
348
- this.response = response;
349
- this.url = url;
350
- this.name = "APIError";
351
- }
352
- static {
353
- __name(this, "APIError");
354
- }
355
- get details() {
356
- if (typeof this.response === "object" && this.response !== null) {
357
- return this.response;
358
- }
517
+ __name(_idbOpen, "_idbOpen");
518
+ function _idbGet(key) {
519
+ return _idbOpen().then((db) => new Promise((resolve, reject) => {
520
+ const tx = db.transaction(_DPOP_STORE, "readonly");
521
+ const req = tx.objectStore(_DPOP_STORE).get(key);
522
+ req.onsuccess = () => resolve(req.result);
523
+ req.onerror = () => reject(req.error);
524
+ }));
525
+ }
526
+ __name(_idbGet, "_idbGet");
527
+ function _idbPut(key, value) {
528
+ return _idbOpen().then((db) => new Promise((resolve, reject) => {
529
+ const tx = db.transaction(_DPOP_STORE, "readwrite");
530
+ tx.objectStore(_DPOP_STORE).put(value, key);
531
+ tx.oncomplete = () => resolve();
532
+ tx.onerror = () => reject(tx.error);
533
+ }));
534
+ }
535
+ __name(_idbPut, "_idbPut");
536
+ var _dpopKeyPromise = null;
537
+ function _getDpopKeyPair() {
538
+ if (_dpopKeyPromise) return _dpopKeyPromise;
539
+ _dpopKeyPromise = (async () => {
540
+ const existing = await _idbGet(_DPOP_KEY_ID).catch(() => void 0);
541
+ if (existing) return existing;
542
+ const pair = await crypto.subtle.generateKey(
543
+ { name: "ECDSA", namedCurve: "P-256" },
544
+ false,
545
+ // extractable:false — JS can sign but never export the private key
546
+ ["sign"]
547
+ );
548
+ await _idbPut(_DPOP_KEY_ID, pair).catch(() => {
549
+ });
550
+ return pair;
551
+ })();
552
+ return _dpopKeyPromise;
553
+ }
554
+ __name(_getDpopKeyPair, "_getDpopKeyPair");
555
+ function _b64urlFromBytes(bytes) {
556
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
557
+ let s = "";
558
+ for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
559
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
560
+ }
561
+ __name(_b64urlFromBytes, "_b64urlFromBytes");
562
+ function _b64urlFromString(str) {
563
+ return _b64urlFromBytes(new TextEncoder().encode(str));
564
+ }
565
+ __name(_b64urlFromString, "_b64urlFromString");
566
+ async function _publicJwk(pub) {
567
+ const jwk = await crypto.subtle.exportKey("jwk", pub);
568
+ return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
569
+ }
570
+ __name(_publicJwk, "_publicJwk");
571
+ async function _makeDpopProof(method, url) {
572
+ try {
573
+ const pair = await _getDpopKeyPair();
574
+ const jwk = await _publicJwk(pair.publicKey);
575
+ const header = { typ: "dpop+jwt", alg: "ES256", jwk };
576
+ const htu = url.split("#")[0].split("?")[0];
577
+ const jti = crypto.randomUUID && crypto.randomUUID() || _b64urlFromBytes(crypto.getRandomValues(new Uint8Array(16)));
578
+ const payload = { htm: method.toUpperCase(), htu, iat: Math.floor(Date.now() / 1e3), jti };
579
+ const signingInput = `${_b64urlFromString(JSON.stringify(header))}.${_b64urlFromString(JSON.stringify(payload))}`;
580
+ const sig = await crypto.subtle.sign(
581
+ { name: "ECDSA", hash: "SHA-256" },
582
+ pair.privateKey,
583
+ new TextEncoder().encode(signingInput)
584
+ );
585
+ return `${signingInput}.${_b64urlFromBytes(sig)}`;
586
+ } catch {
359
587
  return null;
360
588
  }
361
- get fieldErrors() {
362
- const details = this.details;
363
- if (!details) return null;
364
- const fieldErrors = {};
365
- for (const [key, value] of Object.entries(details)) {
366
- if (Array.isArray(value)) fieldErrors[key] = value;
589
+ }
590
+ __name(_makeDpopProof, "_makeDpopProof");
591
+ function installAuthOnClient(client2) {
592
+ if (_client) return;
593
+ _client = client2;
594
+ client2.setConfig({
595
+ baseUrl: auth.getBaseUrl(),
596
+ credentials: _withCredentials ? "include" : "same-origin"
597
+ });
598
+ client2.interceptors.request.use(async (request) => {
599
+ const token = auth.getToken();
600
+ if (token) request.headers.set("Authorization", `Bearer ${token}`);
601
+ const locale = auth.getLocale();
602
+ if (locale) request.headers.set("Accept-Language", locale);
603
+ const apiKey = auth.getApiKey();
604
+ if (apiKey && !request.headers.has("X-API-Key")) request.headers.set("X-API-Key", apiKey);
605
+ try {
606
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
607
+ if (tz) request.headers.set("X-Timezone", tz);
608
+ } catch {
367
609
  }
368
- return Object.keys(fieldErrors).length > 0 ? fieldErrors : null;
369
- }
370
- get errorMessage() {
371
- const details = this.details;
372
- if (!details) return this.message;
373
- if (details.detail) {
374
- return Array.isArray(details.detail) ? details.detail.join(", ") : String(details.detail);
610
+ request.headers.set("X-Client-Time", (/* @__PURE__ */ new Date()).toISOString());
611
+ if (dpopEnabled() && typeof window !== "undefined") {
612
+ const proof = await _makeDpopProof(request.method, request.url);
613
+ if (proof) request.headers.set("DPoP", proof);
375
614
  }
376
- if (details.error) return String(details.error);
377
- if (details.message) return String(details.message);
378
- const fieldErrors = this.fieldErrors;
379
- if (fieldErrors) {
380
- const firstField = Object.keys(fieldErrors)[0];
381
- if (firstField) return `${firstField}: ${fieldErrors[firstField]?.join(", ")}`;
615
+ return request;
616
+ });
617
+ client2.interceptors.error.use((err, res, req) => {
618
+ if (err instanceof APIError) return err;
619
+ const url = req?.url ?? "";
620
+ const status = res?.status ?? 0;
621
+ const statusText = res?.statusText ?? "";
622
+ return new APIError(status, statusText, err, url);
623
+ });
624
+ client2.interceptors.response.use(async (response, request) => {
625
+ if (response.status !== 401) return response;
626
+ if (request.headers.get(RETRY_MARKER)) {
627
+ expireSession(response);
628
+ return response;
382
629
  }
383
- return this.message;
384
- }
385
- get isValidationError() {
386
- return this.statusCode === 400;
387
- }
388
- get isAuthError() {
389
- return this.statusCode === 401;
390
- }
391
- get isPermissionError() {
392
- return this.statusCode === 403;
393
- }
394
- get isNotFoundError() {
395
- return this.statusCode === 404;
396
- }
397
- get isServerError() {
398
- return this.statusCode >= 500 && this.statusCode < 600;
399
- }
400
- };
401
-
402
- // src/_api/generated/helpers/auth.ts
403
- var ACCESS_KEY = "cfg.access_token";
404
- var REFRESH_KEY = "cfg.refresh_token";
405
- var API_KEY_KEY = "cfg.api_key";
406
- var isBrowser2 = typeof window !== "undefined";
407
- var localStorageBackend = {
408
- get(key) {
409
- if (!isBrowser2) return null;
410
- try {
411
- return window.localStorage.getItem(key);
412
- } catch {
413
- return null;
630
+ const newToken = await tryRefresh();
631
+ if (!newToken) {
632
+ expireSession(response);
633
+ return response;
414
634
  }
415
- },
416
- set(key, value) {
417
- if (!isBrowser2) return;
418
- try {
419
- if (value === null) window.localStorage.removeItem(key);
420
- else window.localStorage.setItem(key, value);
421
- } catch {
635
+ const retry = request.clone();
636
+ retry.headers.set("Authorization", `Bearer ${newToken}`);
637
+ retry.headers.set(RETRY_MARKER, "1");
638
+ if (dpopEnabled() && typeof window !== "undefined") {
639
+ const proof = await _makeDpopProof(retry.method, retry.url);
640
+ if (proof) retry.headers.set("DPoP", proof);
422
641
  }
423
- }
424
- };
425
- var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
426
- var cookieBackend = {
427
- get(key) {
428
- if (!isBrowser2) return null;
429
642
  try {
430
- const re = new RegExp(`(?:^|;\\s*)${encodeURIComponent(key)}=([^;]*)`);
431
- const m = document.cookie.match(re);
432
- return m ? decodeURIComponent(m[1]) : null;
643
+ const retried = await fetch(retry);
644
+ if (retried.status === 401) expireSession(retried);
645
+ return retried;
433
646
  } catch {
434
- return null;
647
+ return response;
435
648
  }
436
- },
437
- set(key, value) {
438
- if (!isBrowser2) return;
439
- try {
440
- const k = encodeURIComponent(key);
441
- const secure = window.location.protocol === "https:" ? "; Secure" : "";
442
- if (value === null) {
443
- document.cookie = `${k}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
444
- } else {
445
- const v = encodeURIComponent(value);
446
- document.cookie = `${k}=${v}; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax${secure}`;
447
- }
448
- } catch {
649
+ });
650
+ }
651
+ __name(installAuthOnClient, "installAuthOnClient");
652
+ var REFRESH_ENDPOINT = "/cfg/accounts/token/refresh/";
653
+ auth.setRefreshHandler(async (refresh) => {
654
+ try {
655
+ const url = `${auth.getBaseUrl()}${REFRESH_ENDPOINT}`;
656
+ const headers = { "Content-Type": "application/json" };
657
+ if (dpopEnabled() && typeof window !== "undefined") {
658
+ const proof = await _makeDpopProof("POST", url);
659
+ if (proof) headers["DPoP"] = proof;
449
660
  }
661
+ const res = await fetch(url, {
662
+ method: "POST",
663
+ headers,
664
+ credentials: auth.getWithCredentials() ? "include" : "same-origin",
665
+ body: JSON.stringify({ refresh })
666
+ });
667
+ if (!res.ok) return null;
668
+ const data = await res.json();
669
+ return data?.access ? { access: data.access, refresh: data.refresh ?? refresh } : null;
670
+ } catch {
671
+ return null;
450
672
  }
673
+ });
674
+
675
+ // src/_api/generated/helpers/logger.ts
676
+ import { createConsola } from "consola";
677
+ var DEFAULT_CONFIG = {
678
+ enabled: typeof process !== "undefined" && process.env.NODE_ENV !== "production",
679
+ logRequests: true,
680
+ logResponses: true,
681
+ logErrors: true,
682
+ logBodies: true,
683
+ logHeaders: false
451
684
  };
452
- var _storage = localStorageBackend;
453
- var _storageMode = "localStorage";
454
- function detectLocale() {
455
- try {
456
- if (typeof document !== "undefined") {
457
- const m = document.cookie.match(/(?:^|;\s*)NEXT_LOCALE=([^;]*)/);
458
- if (m) return decodeURIComponent(m[1]);
459
- }
460
- if (typeof navigator !== "undefined" && navigator.language) {
461
- return navigator.language;
685
+ var SENSITIVE_HEADERS = [
686
+ "authorization",
687
+ "cookie",
688
+ "set-cookie",
689
+ "x-api-key",
690
+ "x-csrf-token"
691
+ ];
692
+ var APILogger = class {
693
+ static {
694
+ __name(this, "APILogger");
695
+ }
696
+ config;
697
+ consola;
698
+ constructor(config = {}) {
699
+ this.config = { ...DEFAULT_CONFIG, ...config };
700
+ this.consola = config.consola || createConsola({
701
+ level: this.config.enabled ? 4 : 0
702
+ });
703
+ }
704
+ enable() {
705
+ this.config.enabled = true;
706
+ }
707
+ disable() {
708
+ this.config.enabled = false;
709
+ }
710
+ setConfig(config) {
711
+ this.config = { ...this.config, ...config };
712
+ }
713
+ filterHeaders(headers) {
714
+ if (!headers) return {};
715
+ const filtered = {};
716
+ Object.keys(headers).forEach((key) => {
717
+ filtered[key] = SENSITIVE_HEADERS.includes(key.toLowerCase()) ? "***" : headers[key] || "";
718
+ });
719
+ return filtered;
720
+ }
721
+ logRequest(request) {
722
+ if (!this.config.enabled || !this.config.logRequests) return;
723
+ const { method, url, headers, body } = request;
724
+ this.consola.start(`${method} ${url}`);
725
+ if (this.config.logHeaders && headers) this.consola.debug("Headers:", this.filterHeaders(headers));
726
+ if (this.config.logBodies && body) this.consola.debug("Body:", body);
727
+ }
728
+ logResponse(request, response) {
729
+ if (!this.config.enabled || !this.config.logResponses) return;
730
+ const { method, url } = request;
731
+ const { status, statusText, data, duration } = response;
732
+ this.consola.success(`${method} ${url} ${status} ${statusText} (${duration}ms)`);
733
+ if (this.config.logBodies && data) this.consola.debug("Response:", data);
734
+ }
735
+ logError(request, error) {
736
+ if (!this.config.enabled || !this.config.logErrors) return;
737
+ const { method, url } = request;
738
+ const { message, statusCode, fieldErrors, duration } = error;
739
+ this.consola.error(`${method} ${url} ${statusCode || "Network"} Error (${duration}ms)`);
740
+ this.consola.error("Message:", message);
741
+ if (fieldErrors && Object.keys(fieldErrors).length > 0) {
742
+ this.consola.error("Field Errors:");
743
+ Object.entries(fieldErrors).forEach(([field, errors]) => {
744
+ errors.forEach((err) => this.consola.error(` \u2022 ${field}: ${err}`));
745
+ });
462
746
  }
463
- } catch {
464
747
  }
465
- return null;
748
+ info(message, ...args) {
749
+ if (this.config.enabled) this.consola.info(message, ...args);
750
+ }
751
+ warn(message, ...args) {
752
+ if (this.config.enabled) this.consola.warn(message, ...args);
753
+ }
754
+ error(message, ...args) {
755
+ if (this.config.enabled) this.consola.error(message, ...args);
756
+ }
757
+ debug(message, ...args) {
758
+ if (this.config.enabled) this.consola.debug(message, ...args);
759
+ }
760
+ success(message, ...args) {
761
+ if (this.config.enabled) this.consola.success(message, ...args);
762
+ }
763
+ withTag(tag) {
764
+ return this.consola.withTag(tag);
765
+ }
766
+ };
767
+ var defaultLogger = new APILogger();
768
+
769
+ // src/auth/utils/env.ts
770
+ var isDev = process.env.NODE_ENV === "development";
771
+ var isBrowser2 = typeof window !== "undefined";
772
+ var isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === "true";
773
+ var dpopEnabled2 = process.env.NEXT_PUBLIC_DPOP_ENABLED === "true";
774
+
775
+ // src/log-control.ts
776
+ var verboseByDefault = isDev || isStaticBuild;
777
+ var DEFAULT_LEVEL = verboseByDefault ? 4 : 1;
778
+ var _level = DEFAULT_LEVEL;
779
+ var _subscribers = /* @__PURE__ */ new Set();
780
+ function getLogLevel() {
781
+ return _level;
466
782
  }
467
- __name(detectLocale, "detectLocale");
468
- function defaultBaseUrl() {
469
- if (typeof window !== "undefined") {
783
+ __name(getLogLevel, "getLogLevel");
784
+ function setLogLevel(level) {
785
+ _level = level;
786
+ for (const fn of _subscribers) {
470
787
  try {
471
- if (typeof process !== "undefined" && process.env) {
472
- if (process.env.NEXT_PUBLIC_STATIC_BUILD === "true") return "";
473
- if (process.env.NEXT_PUBLIC_API_PROXY_URL !== void 0)
474
- return process.env.NEXT_PUBLIC_API_PROXY_URL;
475
- return process.env.NEXT_PUBLIC_API_URL || "";
476
- }
788
+ fn(level);
477
789
  } catch {
478
790
  }
479
- return "";
480
791
  }
792
+ }
793
+ __name(setLogLevel, "setLogLevel");
794
+ function onLogLevelChange(fn) {
795
+ _subscribers.add(fn);
481
796
  try {
482
- if (typeof process !== "undefined" && process.env) {
483
- if (process.env.NEXT_PUBLIC_STATIC_BUILD === "true") return "";
484
- return process.env.NEXT_PUBLIC_API_URL || "";
485
- }
797
+ fn(_level);
486
798
  } catch {
487
799
  }
488
- return "";
800
+ return () => _subscribers.delete(fn);
489
801
  }
490
- __name(defaultBaseUrl, "defaultBaseUrl");
491
- function defaultApiKey() {
492
- try {
493
- if (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_API_KEY) {
494
- return process.env.NEXT_PUBLIC_API_KEY;
495
- }
496
- } catch {
802
+ __name(onLogLevelChange, "onLogLevelChange");
803
+ function applyRoleLogPolicy(opts) {
804
+ const devMode = opts.isDev ?? verboseByDefault;
805
+ if (opts.isAdmin || devMode) {
806
+ setLogLevel(4);
807
+ } else {
808
+ setLogLevel(1);
497
809
  }
498
- return null;
499
810
  }
500
- __name(defaultApiKey, "defaultApiKey");
501
- var _localeOverride = null;
502
- var _apiKeyOverride = null;
503
- var _baseUrlOverride = null;
504
- var _withCredentials = true;
505
- var _onUnauthorized = null;
506
- var _refreshHandler = null;
507
- var _refreshInflight = null;
508
- var RETRY_MARKER = "X-Auth-Retry";
509
- var _client = null;
510
- function pushClientConfig() {
511
- if (!_client) return;
512
- _client.setConfig({
513
- baseUrl: auth.getBaseUrl(),
514
- credentials: _withCredentials ? "include" : "same-origin"
515
- });
811
+ __name(applyRoleLogPolicy, "applyRoleLogPolicy");
812
+
813
+ // src/auth/hooks/useSession.ts
814
+ import { useSyncExternalStore } from "react";
815
+ function useSession() {
816
+ return useSyncExternalStore(auth.subscribe, auth.getSnapshot, auth.getServerSnapshot);
516
817
  }
517
- __name(pushClientConfig, "pushClientConfig");
518
- var auth = {
519
- // ── Storage mode ──────────────────────────────────────────────────
520
- getStorageMode() {
521
- return _storageMode;
522
- },
523
- setStorageMode(mode) {
524
- _storageMode = mode;
525
- _storage = mode === "cookie" ? cookieBackend : localStorageBackend;
526
- },
527
- // ── Bearer token ──────────────────────────────────────────────────
528
- getToken() {
529
- return _storage.get(ACCESS_KEY);
530
- },
531
- setToken(token) {
532
- _storage.set(ACCESS_KEY, token);
533
- },
534
- getRefreshToken() {
535
- return _storage.get(REFRESH_KEY);
536
- },
537
- setRefreshToken(token) {
538
- _storage.set(REFRESH_KEY, token);
539
- },
540
- clearTokens() {
541
- _storage.set(ACCESS_KEY, null);
542
- _storage.set(REFRESH_KEY, null);
543
- },
544
- isAuthenticated() {
545
- return _storage.get(ACCESS_KEY) !== null;
546
- },
547
- // ── API key ───────────────────────────────────────────────────────
548
- getApiKey() {
549
- return _apiKeyOverride ?? _storage.get(API_KEY_KEY) ?? defaultApiKey();
550
- },
551
- setApiKey(key) {
552
- _apiKeyOverride = key;
553
- },
554
- setApiKeyPersist(key) {
555
- _apiKeyOverride = key;
556
- _storage.set(API_KEY_KEY, key);
557
- },
558
- clearApiKey() {
559
- _apiKeyOverride = null;
560
- _storage.set(API_KEY_KEY, null);
561
- },
562
- // ── Locale ────────────────────────────────────────────────────────
563
- getLocale() {
564
- return _localeOverride ?? detectLocale();
565
- },
566
- setLocale(locale) {
567
- _localeOverride = locale;
568
- },
569
- // ── Base URL ──────────────────────────────────────────────────────
570
- getBaseUrl() {
571
- const url = _baseUrlOverride ?? defaultBaseUrl();
572
- return url.replace(/\/$/, "");
573
- },
574
- setBaseUrl(url) {
575
- _baseUrlOverride = url ? url.replace(/\/$/, "") : null;
576
- pushClientConfig();
577
- },
578
- // ── Credentials toggle ────────────────────────────────────────────
579
- getWithCredentials() {
580
- return _withCredentials;
581
- },
582
- setWithCredentials(value) {
583
- _withCredentials = value;
584
- pushClientConfig();
585
- },
586
- // ── 401 handler ───────────────────────────────────────────────────
587
- /**
588
- * Fired when the server returns 401 AND no refresh path recovers it
589
- * (no refresh token, no refresh handler, refresh failed, or retry
590
- * still 401). The app should clear local state and redirect to login.
591
- *
592
- * NOT fired for 401 that gets transparently recovered by the refresh
593
- * handler — those are invisible to callers.
594
- */
595
- onUnauthorized(cb) {
596
- _onUnauthorized = cb;
597
- },
598
- /**
599
- * Register the refresh strategy. The handler receives the current
600
- * refresh token and must call your refresh endpoint, returning
601
- * `{ access, refresh? }` on success or `null` on failure.
602
- *
603
- * @example
604
- * auth.setRefreshHandler(async (refresh) => {
605
- * const { data } = await Auth.tokenRefreshCreate({ body: { refresh } });
606
- * return data ? { access: data.access, refresh: data.refresh } : null;
607
- * });
608
- */
609
- setRefreshHandler(fn) {
610
- _refreshHandler = fn;
611
- },
612
- /**
613
- * Proactively run the registered refresh handler right now, reusing the
614
- * SAME single-flight promise as the 401-recovery interceptor. Callers
615
- * (e.g. an expiry timer / focus / reconnect) get token rotation,
616
- * de-duplication and rotated-token persistence for free — they must NOT
617
- * re-implement any of it. Returns the fresh access token, or null if
618
- * there is no handler / no refresh token / the refresh failed.
619
- */
620
- refreshNow() {
621
- return tryRefresh();
622
- }
623
- };
624
- async function tryRefresh() {
625
- if (_refreshInflight) return _refreshInflight;
626
- if (!_refreshHandler) return null;
627
- const refresh = auth.getRefreshToken();
628
- if (!refresh) return null;
629
- _refreshInflight = (async () => {
630
- try {
631
- const result = await _refreshHandler(refresh);
632
- if (!result?.access) return null;
633
- auth.setToken(result.access);
634
- if (result.refresh) auth.setRefreshToken(result.refresh);
635
- return result.access;
636
- } catch {
637
- return null;
638
- } finally {
639
- _refreshInflight = null;
640
- }
641
- })();
642
- return _refreshInflight;
643
- }
644
- __name(tryRefresh, "tryRefresh");
645
- function dpopEnabled2() {
646
- try {
647
- return typeof process !== "undefined" && process.env?.NEXT_PUBLIC_DPOP_ENABLED === "true";
648
- } catch {
649
- return false;
818
+ __name(useSession, "useSession");
819
+
820
+ // src/auth/hooks/useCfgRouter.ts
821
+ import { useRouter as useNextRouter } from "next/navigation";
822
+ import { useCallback, useMemo } from "react";
823
+ function getBasePath() {
824
+ if (typeof process === "undefined") {
825
+ return "";
650
826
  }
827
+ return process.env.NEXT_PUBLIC_BASE_PATH || "";
651
828
  }
652
- __name(dpopEnabled2, "dpopEnabled");
653
- var _DPOP_DB = "cfg-auth";
654
- var _DPOP_STORE = "keys";
655
- var _DPOP_KEY_ID = "dpop-ec-p256";
656
- function _idbOpen() {
657
- return new Promise((resolve, reject) => {
658
- const req = indexedDB.open(_DPOP_DB, 1);
659
- req.onupgradeneeded = () => req.result.createObjectStore(_DPOP_STORE);
660
- req.onsuccess = () => resolve(req.result);
661
- req.onerror = () => reject(req.error);
662
- });
663
- }
664
- __name(_idbOpen, "_idbOpen");
665
- function _idbGet(key) {
666
- return _idbOpen().then((db) => new Promise((resolve, reject) => {
667
- const tx = db.transaction(_DPOP_STORE, "readonly");
668
- const req = tx.objectStore(_DPOP_STORE).get(key);
669
- req.onsuccess = () => resolve(req.result);
670
- req.onerror = () => reject(req.error);
671
- }));
672
- }
673
- __name(_idbGet, "_idbGet");
674
- function _idbPut(key, value) {
675
- return _idbOpen().then((db) => new Promise((resolve, reject) => {
676
- const tx = db.transaction(_DPOP_STORE, "readwrite");
677
- tx.objectStore(_DPOP_STORE).put(value, key);
678
- tx.oncomplete = () => resolve();
679
- tx.onerror = () => reject(tx.error);
680
- }));
681
- }
682
- __name(_idbPut, "_idbPut");
683
- var _dpopKeyPromise = null;
684
- function _getDpopKeyPair() {
685
- if (_dpopKeyPromise) return _dpopKeyPromise;
686
- _dpopKeyPromise = (async () => {
687
- const existing = await _idbGet(_DPOP_KEY_ID).catch(() => void 0);
688
- if (existing) return existing;
689
- const pair = await crypto.subtle.generateKey(
690
- { name: "ECDSA", namedCurve: "P-256" },
691
- false,
692
- // extractable:false — JS can sign but never export the private key
693
- ["sign"]
694
- );
695
- await _idbPut(_DPOP_KEY_ID, pair).catch(() => {
696
- });
697
- return pair;
698
- })();
699
- return _dpopKeyPromise;
700
- }
701
- __name(_getDpopKeyPair, "_getDpopKeyPair");
702
- function _b64urlFromBytes(bytes) {
703
- const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
704
- let s = "";
705
- for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
706
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
707
- }
708
- __name(_b64urlFromBytes, "_b64urlFromBytes");
709
- function _b64urlFromString(str) {
710
- return _b64urlFromBytes(new TextEncoder().encode(str));
711
- }
712
- __name(_b64urlFromString, "_b64urlFromString");
713
- async function _publicJwk(pub) {
714
- const jwk = await crypto.subtle.exportKey("jwk", pub);
715
- return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
716
- }
717
- __name(_publicJwk, "_publicJwk");
718
- async function _makeDpopProof(method, url) {
719
- try {
720
- const pair = await _getDpopKeyPair();
721
- const jwk = await _publicJwk(pair.publicKey);
722
- const header = { typ: "dpop+jwt", alg: "ES256", jwk };
723
- const htu = url.split("#")[0].split("?")[0];
724
- const jti = crypto.randomUUID && crypto.randomUUID() || _b64urlFromBytes(crypto.getRandomValues(new Uint8Array(16)));
725
- const payload = { htm: method.toUpperCase(), htu, iat: Math.floor(Date.now() / 1e3), jti };
726
- const signingInput = `${_b64urlFromString(JSON.stringify(header))}.${_b64urlFromString(JSON.stringify(payload))}`;
727
- const sig = await crypto.subtle.sign(
728
- { name: "ECDSA", hash: "SHA-256" },
729
- pair.privateKey,
730
- new TextEncoder().encode(signingInput)
731
- );
732
- return `${signingInput}.${_b64urlFromBytes(sig)}`;
733
- } catch {
734
- return null;
829
+ __name(getBasePath, "getBasePath");
830
+ function withBasePath(path, basePath) {
831
+ if (!basePath) {
832
+ return path;
735
833
  }
834
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
835
+ const normalizedBasePath = basePath.replace(/\/$/, "");
836
+ return `${normalizedBasePath}${normalizedPath}`;
736
837
  }
737
- __name(_makeDpopProof, "_makeDpopProof");
738
- function installAuthOnClient(client2) {
739
- if (_client) return;
740
- _client = client2;
741
- client2.setConfig({
742
- baseUrl: auth.getBaseUrl(),
743
- credentials: _withCredentials ? "include" : "same-origin"
744
- });
745
- client2.interceptors.request.use(async (request) => {
746
- const token = auth.getToken();
747
- if (token) request.headers.set("Authorization", `Bearer ${token}`);
748
- const locale = auth.getLocale();
749
- if (locale) request.headers.set("Accept-Language", locale);
750
- const apiKey = auth.getApiKey();
751
- if (apiKey && !request.headers.has("X-API-Key")) request.headers.set("X-API-Key", apiKey);
752
- try {
753
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
754
- if (tz) request.headers.set("X-Timezone", tz);
755
- } catch {
838
+ __name(withBasePath, "withBasePath");
839
+ function useCfgRouter() {
840
+ const router = useNextRouter();
841
+ const basePath = useMemo(() => getBasePath(), []);
842
+ const isStaticBuild2 = useMemo(() => {
843
+ return typeof process !== "undefined" && process.env.NEXT_PUBLIC_STATIC_BUILD === "true";
844
+ }, []);
845
+ const push = useCallback((href, options) => {
846
+ if (basePath) {
847
+ window.location.href = withBasePath(href, basePath);
848
+ } else {
849
+ router.push(href, options);
756
850
  }
757
- request.headers.set("X-Client-Time", (/* @__PURE__ */ new Date()).toISOString());
758
- if (dpopEnabled2() && typeof window !== "undefined") {
759
- const proof = await _makeDpopProof(request.method, request.url);
760
- if (proof) request.headers.set("DPoP", proof);
851
+ }, [router, basePath]);
852
+ const replace = useCallback((href, options) => {
853
+ if (basePath) {
854
+ window.location.replace(withBasePath(href, basePath));
855
+ } else {
856
+ router.replace(href, options);
761
857
  }
762
- return request;
763
- });
764
- client2.interceptors.error.use((err, res, req) => {
765
- if (err instanceof APIError) return err;
766
- const url = req?.url ?? "";
767
- const status = res?.status ?? 0;
768
- const statusText = res?.statusText ?? "";
769
- return new APIError(status, statusText, err, url);
770
- });
771
- client2.interceptors.response.use(async (response, request) => {
772
- if (response.status !== 401) return response;
773
- if (request.headers.get(RETRY_MARKER)) {
774
- if (_onUnauthorized) {
775
- try {
776
- _onUnauthorized(response);
777
- } catch {
778
- }
779
- }
780
- return response;
781
- }
782
- const newToken = await tryRefresh();
783
- if (!newToken) {
784
- if (_onUnauthorized) {
785
- try {
786
- _onUnauthorized(response);
787
- } catch {
788
- }
789
- }
790
- return response;
791
- }
792
- const retry = request.clone();
793
- retry.headers.set("Authorization", `Bearer ${newToken}`);
794
- retry.headers.set(RETRY_MARKER, "1");
795
- if (dpopEnabled2() && typeof window !== "undefined") {
796
- const proof = await _makeDpopProof(retry.method, retry.url);
797
- if (proof) retry.headers.set("DPoP", proof);
798
- }
799
- try {
800
- const retried = await fetch(retry);
801
- if (retried.status === 401 && _onUnauthorized) {
802
- try {
803
- _onUnauthorized(retried);
804
- } catch {
805
- }
806
- }
807
- return retried;
808
- } catch {
809
- if (_onUnauthorized) {
810
- try {
811
- _onUnauthorized(response);
812
- } catch {
813
- }
814
- }
815
- return response;
858
+ }, [router, basePath]);
859
+ const hardPush = useCallback((href) => {
860
+ window.location.href = withBasePath(href, basePath);
861
+ }, [basePath]);
862
+ const hardReplace = useCallback((href) => {
863
+ window.location.replace(withBasePath(href, basePath));
864
+ }, [basePath]);
865
+ const prefetch = useCallback((href) => {
866
+ router.prefetch(href);
867
+ }, [router]);
868
+ const back = useCallback(() => {
869
+ router.back();
870
+ }, [router]);
871
+ const forward = useCallback(() => {
872
+ router.forward();
873
+ }, [router]);
874
+ const refresh = useCallback(() => {
875
+ router.refresh();
876
+ }, [router]);
877
+ return {
878
+ push,
879
+ replace,
880
+ hardPush,
881
+ hardReplace,
882
+ prefetch,
883
+ back,
884
+ forward,
885
+ refresh
886
+ };
887
+ }
888
+ __name(useCfgRouter, "useCfgRouter");
889
+
890
+ // src/auth/hooks/useQueryParams.ts
891
+ import { usePathname } from "next/navigation";
892
+ import { useEffect, useRef, useState } from "react";
893
+ function useQueryParams() {
894
+ const pathname = usePathname();
895
+ const [queryParams, setQueryParams] = useState(() => {
896
+ if (typeof window === "undefined") {
897
+ return new URLSearchParams();
816
898
  }
899
+ return new URLSearchParams(window.location.search);
817
900
  });
901
+ const lastSearchRef = useRef("");
902
+ useEffect(() => {
903
+ if (typeof window === "undefined") return;
904
+ const updateQueryParams = /* @__PURE__ */ __name(() => {
905
+ const currentSearch = window.location.search;
906
+ if (currentSearch !== lastSearchRef.current) {
907
+ lastSearchRef.current = currentSearch;
908
+ setQueryParams(new URLSearchParams(currentSearch));
909
+ }
910
+ }, "updateQueryParams");
911
+ updateQueryParams();
912
+ window.addEventListener("popstate", updateQueryParams);
913
+ const origPush = window.history.pushState;
914
+ const origReplace = window.history.replaceState;
915
+ window.history.pushState = function(...args) {
916
+ const result = origPush.apply(this, args);
917
+ updateQueryParams();
918
+ return result;
919
+ };
920
+ window.history.replaceState = function(...args) {
921
+ const result = origReplace.apply(this, args);
922
+ updateQueryParams();
923
+ return result;
924
+ };
925
+ return () => {
926
+ window.removeEventListener("popstate", updateQueryParams);
927
+ window.history.pushState = origPush;
928
+ window.history.replaceState = origReplace;
929
+ };
930
+ }, [pathname]);
931
+ return queryParams;
818
932
  }
819
- __name(installAuthOnClient, "installAuthOnClient");
933
+ __name(useQueryParams, "useQueryParams");
820
934
 
821
- // src/_api/generated/helpers/logger.ts
822
- import { createConsola as createConsola2 } from "consola";
823
- var DEFAULT_CONFIG = {
824
- enabled: typeof process !== "undefined" && process.env.NODE_ENV !== "production",
825
- logRequests: true,
826
- logResponses: true,
827
- logErrors: true,
828
- logBodies: true,
829
- logHeaders: false
830
- };
831
- var SENSITIVE_HEADERS = [
832
- "authorization",
833
- "cookie",
834
- "set-cookie",
835
- "x-api-key",
836
- "x-csrf-token"
837
- ];
838
- var APILogger = class {
839
- static {
840
- __name(this, "APILogger");
841
- }
842
- config;
843
- consola;
844
- constructor(config = {}) {
845
- this.config = { ...DEFAULT_CONFIG, ...config };
846
- this.consola = config.consola || createConsola2({
847
- level: this.config.enabled ? 4 : 0
848
- });
849
- }
850
- enable() {
851
- this.config.enabled = true;
852
- }
853
- disable() {
854
- this.config.enabled = false;
855
- }
856
- setConfig(config) {
857
- this.config = { ...this.config, ...config };
858
- }
859
- filterHeaders(headers) {
860
- if (!headers) return {};
861
- const filtered = {};
862
- Object.keys(headers).forEach((key) => {
863
- filtered[key] = SENSITIVE_HEADERS.includes(key.toLowerCase()) ? "***" : headers[key] || "";
864
- });
865
- return filtered;
866
- }
867
- logRequest(request) {
868
- if (!this.config.enabled || !this.config.logRequests) return;
869
- const { method, url, headers, body } = request;
870
- this.consola.start(`${method} ${url}`);
871
- if (this.config.logHeaders && headers) this.consola.debug("Headers:", this.filterHeaders(headers));
872
- if (this.config.logBodies && body) this.consola.debug("Body:", body);
873
- }
874
- logResponse(request, response) {
875
- if (!this.config.enabled || !this.config.logResponses) return;
876
- const { method, url } = request;
877
- const { status, statusText, data, duration } = response;
878
- this.consola.success(`${method} ${url} ${status} ${statusText} (${duration}ms)`);
879
- if (this.config.logBodies && data) this.consola.debug("Response:", data);
880
- }
881
- logError(request, error) {
882
- if (!this.config.enabled || !this.config.logErrors) return;
883
- const { method, url } = request;
884
- const { message, statusCode, fieldErrors, duration } = error;
885
- this.consola.error(`${method} ${url} ${statusCode || "Network"} Error (${duration}ms)`);
886
- this.consola.error("Message:", message);
887
- if (fieldErrors && Object.keys(fieldErrors).length > 0) {
888
- this.consola.error("Field Errors:");
889
- Object.entries(fieldErrors).forEach(([field, errors]) => {
890
- errors.forEach((err) => this.consola.error(` \u2022 ${field}: ${err}`));
935
+ // src/auth/hooks/useAuthFormState.ts
936
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
937
+ var formatCountdown = /* @__PURE__ */ __name((s) => {
938
+ if (s <= 0) return "";
939
+ const m = Math.floor(s / 60);
940
+ return m > 0 ? `${m}:${String(s % 60).padStart(2, "0")}` : `${s}s`;
941
+ }, "formatCountdown");
942
+ var useAuthFormState = /* @__PURE__ */ __name((initialIdentifier = "") => {
943
+ const [identifier, setIdentifier] = useState2(initialIdentifier);
944
+ const [otp, setOtp] = useState2("");
945
+ const [isLoading, setIsLoading] = useState2(false);
946
+ const [acceptedTerms, setAcceptedTerms] = useState2(true);
947
+ const [step, setStep] = useState2("identifier");
948
+ const [error, setError] = useState2("");
949
+ const [webmail, setWebmail] = useState2(null);
950
+ const [twoFactorSessionId, setTwoFactorSessionId] = useState2(null);
951
+ const [shouldPrompt2FA, setShouldPrompt2FA] = useState2(false);
952
+ const [twoFactorCode, setTwoFactorCode] = useState2("");
953
+ const [useBackupCode, setUseBackupCode] = useState2(false);
954
+ const [rateLimitSeconds, setRateLimitSeconds] = useState2(0);
955
+ const rateLimitTimerRef = useRef2(null);
956
+ const startRateLimitCountdown = useCallback2((seconds) => {
957
+ if (rateLimitTimerRef.current) clearInterval(rateLimitTimerRef.current);
958
+ setRateLimitSeconds(seconds);
959
+ rateLimitTimerRef.current = setInterval(() => {
960
+ setRateLimitSeconds((prev) => {
961
+ if (prev <= 1) {
962
+ clearInterval(rateLimitTimerRef.current);
963
+ rateLimitTimerRef.current = null;
964
+ return 0;
965
+ }
966
+ return prev - 1;
891
967
  });
892
- }
893
- }
894
- info(message, ...args) {
895
- if (this.config.enabled) this.consola.info(message, ...args);
896
- }
897
- warn(message, ...args) {
898
- if (this.config.enabled) this.consola.warn(message, ...args);
899
- }
900
- error(message, ...args) {
901
- if (this.config.enabled) this.consola.error(message, ...args);
902
- }
903
- debug(message, ...args) {
904
- if (this.config.enabled) this.consola.debug(message, ...args);
905
- }
906
- success(message, ...args) {
907
- if (this.config.enabled) this.consola.success(message, ...args);
908
- }
909
- withTag(tag) {
910
- return this.consola.withTag(tag);
911
- }
912
- };
913
- var defaultLogger = new APILogger();
968
+ }, 1e3);
969
+ }, []);
970
+ useEffect2(() => () => {
971
+ if (rateLimitTimerRef.current) clearInterval(rateLimitTimerRef.current);
972
+ }, []);
973
+ const clearError = useCallback2(() => setError(""), []);
974
+ return {
975
+ // State
976
+ identifier,
977
+ otp,
978
+ isLoading,
979
+ acceptedTerms,
980
+ step,
981
+ error,
982
+ twoFactorSessionId,
983
+ shouldPrompt2FA,
984
+ twoFactorCode,
985
+ useBackupCode,
986
+ rateLimitSeconds,
987
+ isRateLimited: rateLimitSeconds > 0,
988
+ rateLimitLabel: formatCountdown(rateLimitSeconds),
989
+ webmail,
990
+ // Handlers
991
+ setIdentifier,
992
+ setOtp,
993
+ setAcceptedTerms,
994
+ setError,
995
+ clearError,
996
+ setStep,
997
+ setIsLoading,
998
+ setTwoFactorSessionId,
999
+ setShouldPrompt2FA,
1000
+ setTwoFactorCode,
1001
+ setUseBackupCode,
1002
+ startRateLimitCountdown,
1003
+ setWebmail
1004
+ };
1005
+ }, "useAuthFormState");
1006
+
1007
+ // src/auth/hooks/useAuthValidation.ts
1008
+ import { useCallback as useCallback3 } from "react";
1009
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1010
+ var useAuthValidation = /* @__PURE__ */ __name(() => {
1011
+ const validateIdentifier2 = useCallback3((id) => {
1012
+ return EMAIL_REGEX.test(id);
1013
+ }, []);
1014
+ return { validateIdentifier: validateIdentifier2 };
1015
+ }, "useAuthValidation");
1016
+ var validateIdentifier = /* @__PURE__ */ __name((id) => EMAIL_REGEX.test(id), "validateIdentifier");
1017
+
1018
+ // src/auth/hooks/useAuthForm.ts
1019
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4 } from "react";
1020
+
1021
+ // src/auth/utils/logger.ts
1022
+ import { createConsola as createConsola2 } from "consola";
1023
+ var logger = createConsola2({
1024
+ level: getLogLevel()
1025
+ }).withTag("api");
1026
+ onLogLevelChange((level) => {
1027
+ logger.level = level;
1028
+ });
1029
+ var authLogger = logger.withTag("auth");
1030
+
1031
+ // src/auth/hooks/useAutoAuth.ts
1032
+ import { usePathname as usePathname2 } from "next/navigation";
1033
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
1034
+
1035
+ // src/auth/utils/path.ts
1036
+ var normalizePath = /* @__PURE__ */ __name((path) => {
1037
+ if (!path) return "";
1038
+ const withoutLocale = path.replace(/^\/[a-z]{2}(?=\/|$)/, "");
1039
+ const trimmed = withoutLocale.replace(/\/+$/, "");
1040
+ return trimmed || "/";
1041
+ }, "normalizePath");
1042
+ var isAllowedAuthPath = /* @__PURE__ */ __name((pathname, allowedPaths) => {
1043
+ const normalized = normalizePath(pathname);
1044
+ return allowedPaths.some(
1045
+ (p) => normalized === p || normalized.startsWith(p + "/")
1046
+ );
1047
+ }, "isAllowedAuthPath");
1048
+
1049
+ // src/auth/hooks/useAutoAuth.ts
1050
+ var useAutoAuth = /* @__PURE__ */ __name((options = {}) => {
1051
+ const { onOTPDetected, cleanupUrl = true, allowedPaths = ["/auth"] } = options;
1052
+ const queryParams = useQueryParams();
1053
+ const pathname = usePathname2();
1054
+ const router = useCfgRouter();
1055
+ const isAllowedPath = isAllowedAuthPath(pathname, allowedPaths);
1056
+ const queryOtp = queryParams.get("otp") || "";
1057
+ const queryEmail = queryParams.get("email") || void 0;
1058
+ const hasOTP = !!queryOtp;
1059
+ const isReady = !!pathname && hasOTP && isAllowedPath;
1060
+ const otpValid = queryOtp.length === AUTH_CONSTANTS.EMAIL_OTP_LENGTH;
1061
+ const onOTPDetectedRef = useRef3(onOTPDetected);
1062
+ onOTPDetectedRef.current = onOTPDetected;
1063
+ useEffect3(() => {
1064
+ if (!isReady || !otpValid) return;
1065
+ authLogger.info("OTP detected in URL on auth page:", queryOtp);
1066
+ onOTPDetectedRef.current?.(queryOtp, queryEmail);
1067
+ }, [isReady, otpValid, queryOtp, queryEmail]);
1068
+ useEffect3(() => {
1069
+ if (!isReady || !cleanupUrl || !queryOtp) return;
1070
+ const cleanQuery = Object.fromEntries(queryParams.entries());
1071
+ delete cleanQuery.otp;
1072
+ delete cleanQuery.email;
1073
+ const queryString = new URLSearchParams(cleanQuery).toString();
1074
+ const realPathname = typeof window !== "undefined" ? window.location.pathname : pathname;
1075
+ router.replace(queryString ? `${realPathname}?${queryString}` : realPathname);
1076
+ }, [isReady, cleanupUrl, queryOtp]);
1077
+ return {
1078
+ isReady,
1079
+ hasOTP,
1080
+ isAllowedPath
1081
+ };
1082
+ }, "useAutoAuth");
1083
+
1084
+ // src/auth/hooks/useTwoFactor.ts
1085
+ import { useCallback as useCallback4, useState as useState4 } from "react";
914
1086
 
915
1087
  // src/_api/generated/core/bodySerializer.gen.ts
916
1088
  var serializeFormDataPair = /* @__PURE__ */ __name((data, key, value) => {
@@ -1733,103 +1905,6 @@ var client = createClient(createConfig({ baseUrl: "http://localhost:8000" }));
1733
1905
  installAuthOnClient(client);
1734
1906
 
1735
1907
  // src/_api/generated/sdk.gen.ts
1736
- var CfgAccountsApiKey = class {
1737
- static {
1738
- __name(this, "CfgAccountsApiKey");
1739
- }
1740
- /**
1741
- * Get API key details
1742
- *
1743
- * Retrieve the current user's API key (masked) and metadata.
1744
- */
1745
- static cfgAccountsApiKeyRetrieve(options) {
1746
- return (options?.client ?? client).get({
1747
- security: [
1748
- { scheme: "bearer", type: "http" },
1749
- {
1750
- in: "cookie",
1751
- name: "sessionid",
1752
- type: "apiKey"
1753
- },
1754
- { name: "X-API-Key", type: "apiKey" }
1755
- ],
1756
- url: "/cfg/accounts/api-key/",
1757
- ...options
1758
- });
1759
- }
1760
- /**
1761
- * Regenerate API key
1762
- *
1763
- * Generate a new API key. The full key is returned only once.
1764
- */
1765
- static cfgAccountsApiKeyRegenerateCreate(options) {
1766
- return (options.client ?? client).post({
1767
- security: [
1768
- { scheme: "bearer", type: "http" },
1769
- {
1770
- in: "cookie",
1771
- name: "sessionid",
1772
- type: "apiKey"
1773
- },
1774
- { name: "X-API-Key", type: "apiKey" }
1775
- ],
1776
- url: "/cfg/accounts/api-key/regenerate/",
1777
- ...options,
1778
- headers: {
1779
- "Content-Type": "application/json",
1780
- ...options.headers
1781
- }
1782
- });
1783
- }
1784
- /**
1785
- * Reveal API key
1786
- *
1787
- * Return the current full API key WITHOUT rotating it. The same durable value every one of the user's agents uses — for the signed-in user to copy and paste into agent onboarding. Default GET stays masked; this is the explicit reveal action.
1788
- */
1789
- static cfgAccountsApiKeyRevealCreate(options) {
1790
- return (options.client ?? client).post({
1791
- security: [
1792
- { scheme: "bearer", type: "http" },
1793
- {
1794
- in: "cookie",
1795
- name: "sessionid",
1796
- type: "apiKey"
1797
- },
1798
- { name: "X-API-Key", type: "apiKey" }
1799
- ],
1800
- url: "/cfg/accounts/api-key/reveal/",
1801
- ...options,
1802
- headers: {
1803
- "Content-Type": "application/json",
1804
- ...options.headers
1805
- }
1806
- });
1807
- }
1808
- /**
1809
- * Test API key
1810
- *
1811
- * Test whether an API key is valid without consuming it.
1812
- */
1813
- static cfgAccountsApiKeyTestCreate(options) {
1814
- return (options.client ?? client).post({
1815
- security: [
1816
- { scheme: "bearer", type: "http" },
1817
- {
1818
- in: "cookie",
1819
- name: "sessionid",
1820
- type: "apiKey"
1821
- },
1822
- { name: "X-API-Key", type: "apiKey" }
1823
- ],
1824
- url: "/cfg/accounts/api-key/test/",
1825
- ...options,
1826
- headers: {
1827
- "Content-Type": "application/json",
1828
- ...options.headers
1829
- }
1830
- });
1831
- }
1832
- };
1833
1908
  var CfgAccountsOauth = class {
1834
1909
  static {
1835
1910
  __name(this, "CfgAccountsOauth");
@@ -1841,7 +1916,11 @@ var CfgAccountsOauth = class {
1841
1916
  */
1842
1917
  static cfgAccountsOauthConnectionsList(options) {
1843
1918
  return (options?.client ?? client).get({
1844
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
1919
+ security: [
1920
+ { name: "X-API-Key", type: "apiKey" },
1921
+ { scheme: "bearer", type: "http" },
1922
+ { name: "Authorization", type: "apiKey" }
1923
+ ],
1845
1924
  url: "/cfg/accounts/oauth/connections/",
1846
1925
  ...options
1847
1926
  });
@@ -1853,7 +1932,11 @@ var CfgAccountsOauth = class {
1853
1932
  */
1854
1933
  static cfgAccountsOauthDisconnectCreate(options) {
1855
1934
  return (options.client ?? client).post({
1856
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
1935
+ security: [
1936
+ { name: "X-API-Key", type: "apiKey" },
1937
+ { scheme: "bearer", type: "http" },
1938
+ { name: "Authorization", type: "apiKey" }
1939
+ ],
1857
1940
  url: "/cfg/accounts/oauth/disconnect/",
1858
1941
  ...options,
1859
1942
  headers: {
@@ -1910,7 +1993,11 @@ var CfgAccounts = class {
1910
1993
  */
1911
1994
  static cfgAccountsOtpRequestCreate(options) {
1912
1995
  return (options.client ?? client).post({
1913
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
1996
+ security: [
1997
+ { name: "X-API-Key", type: "apiKey" },
1998
+ { scheme: "bearer", type: "http" },
1999
+ { name: "Authorization", type: "apiKey" }
2000
+ ],
1914
2001
  url: "/cfg/accounts/otp/request/",
1915
2002
  ...options,
1916
2003
  headers: {
@@ -1931,7 +2018,11 @@ var CfgAccounts = class {
1931
2018
  */
1932
2019
  static cfgAccountsOtpVerifyCreate(options) {
1933
2020
  return (options.client ?? client).post({
1934
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2021
+ security: [
2022
+ { name: "X-API-Key", type: "apiKey" },
2023
+ { scheme: "bearer", type: "http" },
2024
+ { name: "Authorization", type: "apiKey" }
2025
+ ],
1935
2026
  url: "/cfg/accounts/otp/verify/",
1936
2027
  ...options,
1937
2028
  headers: {
@@ -1969,7 +2060,11 @@ var CfgAccountsProfile = class {
1969
2060
  static cfgAccountsProfileAvatarCreate(options) {
1970
2061
  return (options?.client ?? client).post({
1971
2062
  ...formDataBodySerializer,
1972
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2063
+ security: [
2064
+ { name: "X-API-Key", type: "apiKey" },
2065
+ { scheme: "bearer", type: "http" },
2066
+ { name: "Authorization", type: "apiKey" }
2067
+ ],
1973
2068
  url: "/cfg/accounts/profile/avatar/",
1974
2069
  ...options,
1975
2070
  headers: {
@@ -2090,17 +2185,15 @@ var CfgAccountsAuth = class {
2090
2185
  __name(this, "CfgAccountsAuth");
2091
2186
  }
2092
2187
  /**
2093
- * Refresh JWT token.
2188
+ * Revoke a refresh token (logout).
2094
2189
  *
2095
- * DPoP-aware: when the incoming refresh token is key-bound (`cnf.jkt`), the
2096
- * rotated access/refresh in the response are re-stamped with the same `cnf`
2097
- * (stock SimpleJWT drops it from the derived access), and a matching DPoP
2098
- * proof is required on the refresh request — so a stolen refresh token can't
2099
- * be used to mint fresh tokens.
2190
+ * Blacklists the posted refresh token so it can never mint another access
2191
+ * token. Called best-effort by the client's logout without it, "logout"
2192
+ * is purely client-side and a stolen refresh token survives until expiry.
2100
2193
  */
2101
- static cfgAccountsTokenRefreshCreate(options) {
2194
+ static cfgAccountsTokenBlacklistCreate(options) {
2102
2195
  return (options.client ?? client).post({
2103
- url: "/cfg/accounts/token/refresh/",
2196
+ url: "/cfg/accounts/token/blacklist/",
2104
2197
  ...options,
2105
2198
  headers: {
2106
2199
  "Content-Type": "application/json",
@@ -2108,48 +2201,18 @@ var CfgAccountsAuth = class {
2108
2201
  }
2109
2202
  });
2110
2203
  }
2111
- };
2112
- var CfgCentrifugo = class {
2113
- static {
2114
- __name(this, "CfgCentrifugo");
2115
- }
2116
- /**
2117
- * Get Centrifugo connection token
2118
- *
2119
- * Generate JWT token for WebSocket connection to Centrifugo. Token includes user's allowed channels based on their permissions. Requires authentication.
2120
- */
2121
- static cfgCentrifugoAuthTokenRetrieve(options) {
2122
- return (options?.client ?? client).get({
2123
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2124
- url: "/cfg/centrifugo/auth/token/",
2125
- ...options
2126
- });
2127
- }
2128
- };
2129
- var CfgTotpBackupCodes = class {
2130
- static {
2131
- __name(this, "CfgTotpBackupCodes");
2132
- }
2133
- /**
2134
- * Get backup codes status for user.
2135
- */
2136
- static cfgTotpBackupCodesRetrieve(options) {
2137
- return (options?.client ?? client).get({
2138
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2139
- url: "/cfg/totp/backup-codes/",
2140
- ...options
2141
- });
2142
- }
2143
2204
  /**
2144
- * Regenerate backup codes.
2205
+ * Refresh JWT token.
2145
2206
  *
2146
- * Requires TOTP code for verification.
2147
- * Invalidates all existing codes.
2207
+ * DPoP-aware: when the incoming refresh token is key-bound (`cnf.jkt`), the
2208
+ * rotated access/refresh in the response are re-stamped with the same `cnf`
2209
+ * (stock SimpleJWT drops it from the derived access), and a matching DPoP
2210
+ * proof is required on the refresh request — so a stolen refresh token can't
2211
+ * be used to mint fresh tokens.
2148
2212
  */
2149
- static cfgTotpBackupCodesRegenerateCreate(options) {
2213
+ static cfgAccountsTokenRefreshCreate(options) {
2150
2214
  return (options.client ?? client).post({
2151
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2152
- url: "/cfg/totp/backup-codes/regenerate/",
2215
+ url: "/cfg/accounts/token/refresh/",
2153
2216
  ...options,
2154
2217
  headers: {
2155
2218
  "Content-Type": "application/json",
@@ -2167,7 +2230,11 @@ var CfgTotp = class {
2167
2230
  */
2168
2231
  static cfgTotpDevicesRetrieve(options) {
2169
2232
  return (options?.client ?? client).get({
2170
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2233
+ security: [
2234
+ { name: "X-API-Key", type: "apiKey" },
2235
+ { scheme: "bearer", type: "http" },
2236
+ { name: "Authorization", type: "apiKey" }
2237
+ ],
2171
2238
  url: "/cfg/totp/devices/",
2172
2239
  ...options
2173
2240
  });
@@ -2179,7 +2246,11 @@ var CfgTotp = class {
2179
2246
  */
2180
2247
  static cfgTotpDevicesDestroy(options) {
2181
2248
  return (options.client ?? client).delete({
2182
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2249
+ security: [
2250
+ { name: "X-API-Key", type: "apiKey" },
2251
+ { scheme: "bearer", type: "http" },
2252
+ { name: "Authorization", type: "apiKey" }
2253
+ ],
2183
2254
  url: "/cfg/totp/devices/{id}/",
2184
2255
  ...options
2185
2256
  });
@@ -2191,7 +2262,11 @@ var CfgTotp = class {
2191
2262
  */
2192
2263
  static cfgTotpDisableCreate(options) {
2193
2264
  return (options.client ?? client).post({
2194
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2265
+ security: [
2266
+ { name: "X-API-Key", type: "apiKey" },
2267
+ { scheme: "bearer", type: "http" },
2268
+ { name: "Authorization", type: "apiKey" }
2269
+ ],
2195
2270
  url: "/cfg/totp/disable/",
2196
2271
  ...options,
2197
2272
  headers: {
@@ -2212,298 +2287,86 @@ var CfgTotpSetup = class {
2212
2287
  */
2213
2288
  static cfgTotpSetupCreate(options) {
2214
2289
  return (options?.client ?? client).post({
2215
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2216
- url: "/cfg/totp/setup/",
2217
- ...options,
2218
- headers: {
2219
- "Content-Type": "application/json",
2220
- ...options?.headers
2221
- }
2222
- });
2223
- }
2224
- /**
2225
- * Confirm 2FA setup with first valid code.
2226
- *
2227
- * Activates the device and generates backup codes.
2228
- */
2229
- static cfgTotpSetupConfirmCreate(options) {
2230
- return (options.client ?? client).post({
2231
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2232
- url: "/cfg/totp/setup/confirm/",
2233
- ...options,
2234
- headers: {
2235
- "Content-Type": "application/json",
2236
- ...options.headers
2237
- }
2238
- });
2239
- }
2240
- };
2241
- var CfgTotpVerify = class {
2242
- static {
2243
- __name(this, "CfgTotpVerify");
2244
- }
2245
- /**
2246
- * Verify TOTP code for 2FA session.
2247
- *
2248
- * Completes authentication and returns JWT tokens on success.
2249
- */
2250
- static cfgTotpVerifyCreate(options) {
2251
- return (options.client ?? client).post({
2252
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2253
- url: "/cfg/totp/verify/",
2254
- ...options,
2255
- headers: {
2256
- "Content-Type": "application/json",
2257
- ...options.headers
2258
- }
2259
- });
2260
- }
2261
- /**
2262
- * Verify backup recovery code for 2FA session.
2263
- *
2264
- * Alternative verification method when TOTP device unavailable.
2265
- */
2266
- static cfgTotpVerifyBackupCreate(options) {
2267
- return (options.client ?? client).post({
2268
- security: [{ name: "X-API-Key", type: "apiKey" }, { scheme: "bearer", type: "http" }],
2269
- url: "/cfg/totp/verify/backup/",
2270
- ...options,
2271
- headers: {
2272
- "Content-Type": "application/json",
2273
- ...options.headers
2274
- }
2275
- });
2276
- }
2277
- };
2278
-
2279
- // src/_api/generated/_cfg_accounts/api.ts
2280
- var API = class {
2281
- static {
2282
- __name(this, "API");
2283
- }
2284
- logger;
2285
- cfgAccountsApiKey = CfgAccountsApiKey;
2286
- cfgAccountsOauth = CfgAccountsOauth;
2287
- cfgAccounts = CfgAccounts;
2288
- cfgAccountsProfile = CfgAccountsProfile;
2289
- cfgAccountsAuth = CfgAccountsAuth;
2290
- constructor(_baseUrl, opts = {}) {
2291
- this.logger = new APILogger(opts.logger);
2292
- if (_baseUrl) auth.setBaseUrl(_baseUrl);
2293
- if (opts.locale !== void 0) auth.setLocale(opts.locale);
2294
- if (opts.apiKey !== void 0) auth.setApiKey(opts.apiKey);
2295
- if (opts.withCredentials !== void 0) auth.setWithCredentials(opts.withCredentials);
2296
- }
2297
- // ── Base URL ────────────────────────────────────────────────────────────
2298
- getBaseUrl() {
2299
- return auth.getBaseUrl();
2300
- }
2301
- setBaseUrl(url) {
2302
- auth.setBaseUrl(url);
2303
- }
2304
- // ── Tokens ──────────────────────────────────────────────────────────────
2305
- getToken() {
2306
- return auth.getToken();
2307
- }
2308
- setToken(token) {
2309
- auth.setToken(token);
2310
- }
2311
- getRefreshToken() {
2312
- return auth.getRefreshToken();
2313
- }
2314
- setRefreshToken(token) {
2315
- auth.setRefreshToken(token);
2316
- }
2317
- clearToken() {
2318
- auth.clearTokens();
2319
- }
2320
- isAuthenticated() {
2321
- return auth.isAuthenticated();
2322
- }
2323
- // ── Locale / API key ────────────────────────────────────────────────────
2324
- getLocale() {
2325
- return auth.getLocale();
2326
- }
2327
- setLocale(locale) {
2328
- auth.setLocale(locale);
2329
- }
2330
- getApiKey() {
2331
- return auth.getApiKey();
2332
- }
2333
- setApiKey(key) {
2334
- auth.setApiKey(key);
2335
- }
2336
- // ── 401 handling ────────────────────────────────────────────────────────
2337
- /** Fired only on terminal 401 (after refresh+retry path is exhausted). */
2338
- onUnauthorized(cb) {
2339
- auth.onUnauthorized(cb);
2340
- }
2341
- /** Provide a refresh strategy. See `auth.setRefreshHandler` for the contract. */
2342
- setRefreshHandler(fn) {
2343
- auth.setRefreshHandler(fn);
2344
- }
2345
- /**
2346
- * Proactively refresh now via the registered handler, sharing the single
2347
- * 401-recovery flight (rotation + dedup + rotated-token persistence).
2348
- * See `auth.refreshNow`.
2349
- */
2350
- refreshNow() {
2351
- return auth.refreshNow();
2352
- }
2353
- };
2354
-
2355
- // src/_api/generated/_cfg_centrifugo/api.ts
2356
- var API2 = class {
2357
- static {
2358
- __name(this, "API");
2359
- }
2360
- logger;
2361
- cfgCentrifugo = CfgCentrifugo;
2362
- constructor(_baseUrl, opts = {}) {
2363
- this.logger = new APILogger(opts.logger);
2364
- if (_baseUrl) auth.setBaseUrl(_baseUrl);
2365
- if (opts.locale !== void 0) auth.setLocale(opts.locale);
2366
- if (opts.apiKey !== void 0) auth.setApiKey(opts.apiKey);
2367
- if (opts.withCredentials !== void 0) auth.setWithCredentials(opts.withCredentials);
2368
- }
2369
- // ── Base URL ────────────────────────────────────────────────────────────
2370
- getBaseUrl() {
2371
- return auth.getBaseUrl();
2372
- }
2373
- setBaseUrl(url) {
2374
- auth.setBaseUrl(url);
2375
- }
2376
- // ── Tokens ──────────────────────────────────────────────────────────────
2377
- getToken() {
2378
- return auth.getToken();
2379
- }
2380
- setToken(token) {
2381
- auth.setToken(token);
2382
- }
2383
- getRefreshToken() {
2384
- return auth.getRefreshToken();
2385
- }
2386
- setRefreshToken(token) {
2387
- auth.setRefreshToken(token);
2388
- }
2389
- clearToken() {
2390
- auth.clearTokens();
2391
- }
2392
- isAuthenticated() {
2393
- return auth.isAuthenticated();
2394
- }
2395
- // ── Locale / API key ────────────────────────────────────────────────────
2396
- getLocale() {
2397
- return auth.getLocale();
2398
- }
2399
- setLocale(locale) {
2400
- auth.setLocale(locale);
2401
- }
2402
- getApiKey() {
2403
- return auth.getApiKey();
2404
- }
2405
- setApiKey(key) {
2406
- auth.setApiKey(key);
2407
- }
2408
- // ── 401 handling ────────────────────────────────────────────────────────
2409
- /** Fired only on terminal 401 (after refresh+retry path is exhausted). */
2410
- onUnauthorized(cb) {
2411
- auth.onUnauthorized(cb);
2412
- }
2413
- /** Provide a refresh strategy. See `auth.setRefreshHandler` for the contract. */
2414
- setRefreshHandler(fn) {
2415
- auth.setRefreshHandler(fn);
2290
+ security: [
2291
+ { name: "X-API-Key", type: "apiKey" },
2292
+ { scheme: "bearer", type: "http" },
2293
+ { name: "Authorization", type: "apiKey" }
2294
+ ],
2295
+ url: "/cfg/totp/setup/",
2296
+ ...options,
2297
+ headers: {
2298
+ "Content-Type": "application/json",
2299
+ ...options?.headers
2300
+ }
2301
+ });
2416
2302
  }
2417
2303
  /**
2418
- * Proactively refresh now via the registered handler, sharing the single
2419
- * 401-recovery flight (rotation + dedup + rotated-token persistence).
2420
- * See `auth.refreshNow`.
2304
+ * Confirm 2FA setup with first valid code.
2305
+ *
2306
+ * Activates the device and generates backup codes.
2421
2307
  */
2422
- refreshNow() {
2423
- return auth.refreshNow();
2308
+ static cfgTotpSetupConfirmCreate(options) {
2309
+ return (options.client ?? client).post({
2310
+ security: [
2311
+ { name: "X-API-Key", type: "apiKey" },
2312
+ { scheme: "bearer", type: "http" },
2313
+ { name: "Authorization", type: "apiKey" }
2314
+ ],
2315
+ url: "/cfg/totp/setup/confirm/",
2316
+ ...options,
2317
+ headers: {
2318
+ "Content-Type": "application/json",
2319
+ ...options.headers
2320
+ }
2321
+ });
2424
2322
  }
2425
2323
  };
2426
-
2427
- // src/_api/generated/_cfg_totp/api.ts
2428
- var API3 = class {
2324
+ var CfgTotpVerify = class {
2429
2325
  static {
2430
- __name(this, "API");
2431
- }
2432
- logger;
2433
- cfgTotpBackupCodes = CfgTotpBackupCodes;
2434
- cfgTotp = CfgTotp;
2435
- cfgTotpSetup = CfgTotpSetup;
2436
- cfgTotpVerify = CfgTotpVerify;
2437
- constructor(_baseUrl, opts = {}) {
2438
- this.logger = new APILogger(opts.logger);
2439
- if (_baseUrl) auth.setBaseUrl(_baseUrl);
2440
- if (opts.locale !== void 0) auth.setLocale(opts.locale);
2441
- if (opts.apiKey !== void 0) auth.setApiKey(opts.apiKey);
2442
- if (opts.withCredentials !== void 0) auth.setWithCredentials(opts.withCredentials);
2443
- }
2444
- // ── Base URL ────────────────────────────────────────────────────────────
2445
- getBaseUrl() {
2446
- return auth.getBaseUrl();
2447
- }
2448
- setBaseUrl(url) {
2449
- auth.setBaseUrl(url);
2450
- }
2451
- // ── Tokens ──────────────────────────────────────────────────────────────
2452
- getToken() {
2453
- return auth.getToken();
2454
- }
2455
- setToken(token) {
2456
- auth.setToken(token);
2457
- }
2458
- getRefreshToken() {
2459
- return auth.getRefreshToken();
2460
- }
2461
- setRefreshToken(token) {
2462
- auth.setRefreshToken(token);
2463
- }
2464
- clearToken() {
2465
- auth.clearTokens();
2466
- }
2467
- isAuthenticated() {
2468
- return auth.isAuthenticated();
2469
- }
2470
- // ── Locale / API key ────────────────────────────────────────────────────
2471
- getLocale() {
2472
- return auth.getLocale();
2473
- }
2474
- setLocale(locale) {
2475
- auth.setLocale(locale);
2476
- }
2477
- getApiKey() {
2478
- return auth.getApiKey();
2479
- }
2480
- setApiKey(key) {
2481
- auth.setApiKey(key);
2482
- }
2483
- // ── 401 handling ────────────────────────────────────────────────────────
2484
- /** Fired only on terminal 401 (after refresh+retry path is exhausted). */
2485
- onUnauthorized(cb) {
2486
- auth.onUnauthorized(cb);
2326
+ __name(this, "CfgTotpVerify");
2487
2327
  }
2488
- /** Provide a refresh strategy. See `auth.setRefreshHandler` for the contract. */
2489
- setRefreshHandler(fn) {
2490
- auth.setRefreshHandler(fn);
2328
+ /**
2329
+ * Verify TOTP code for 2FA session.
2330
+ *
2331
+ * Completes authentication and returns JWT tokens on success.
2332
+ */
2333
+ static cfgTotpVerifyCreate(options) {
2334
+ return (options.client ?? client).post({
2335
+ security: [
2336
+ { name: "X-API-Key", type: "apiKey" },
2337
+ { scheme: "bearer", type: "http" },
2338
+ { name: "Authorization", type: "apiKey" }
2339
+ ],
2340
+ url: "/cfg/totp/verify/",
2341
+ ...options,
2342
+ headers: {
2343
+ "Content-Type": "application/json",
2344
+ ...options.headers
2345
+ }
2346
+ });
2491
2347
  }
2492
2348
  /**
2493
- * Proactively refresh now via the registered handler, sharing the single
2494
- * 401-recovery flight (rotation + dedup + rotated-token persistence).
2495
- * See `auth.refreshNow`.
2349
+ * Verify backup recovery code for 2FA session.
2350
+ *
2351
+ * Alternative verification method when TOTP device unavailable.
2496
2352
  */
2497
- refreshNow() {
2498
- return auth.refreshNow();
2353
+ static cfgTotpVerifyBackupCreate(options) {
2354
+ return (options.client ?? client).post({
2355
+ security: [
2356
+ { name: "X-API-Key", type: "apiKey" },
2357
+ { scheme: "bearer", type: "http" },
2358
+ { name: "Authorization", type: "apiKey" }
2359
+ ],
2360
+ url: "/cfg/totp/verify/backup/",
2361
+ ...options,
2362
+ headers: {
2363
+ "Content-Type": "application/json",
2364
+ ...options.headers
2365
+ }
2366
+ });
2499
2367
  }
2500
2368
  };
2501
2369
 
2502
- // src/_api/generated/index.ts
2503
- var CfgAccountsApi = new API();
2504
- var CfgCentrifugoApi = new API2();
2505
- var CfgTotpApi = new API3();
2506
-
2507
2370
  // src/auth/utils/analytics.ts
2508
2371
  var AnalyticsEvent = /* @__PURE__ */ ((AnalyticsEvent2) => {
2509
2372
  AnalyticsEvent2["AUTH_OTP_REQUEST"] = "auth_otp_request";
@@ -2537,24 +2400,229 @@ var Analytics = {
2537
2400
  if (isDev) {
2538
2401
  console.log("[Analytics] Set user:", userId);
2539
2402
  }
2540
- }
2541
- };
2403
+ }
2404
+ };
2405
+
2406
+ // src/auth/hooks/useSessionStorage.ts
2407
+ import { useState as useState3 } from "react";
2408
+ function useSessionStorage(key, initialValue) {
2409
+ const [storedValue, setStoredValue] = useState3(() => {
2410
+ if (typeof window === "undefined") {
2411
+ return initialValue;
2412
+ }
2413
+ try {
2414
+ const item = window.sessionStorage.getItem(key);
2415
+ return item ? JSON.parse(item) : initialValue;
2416
+ } catch (error) {
2417
+ authLogger.error(`Error reading sessionStorage key "${key}":`, error);
2418
+ return initialValue;
2419
+ }
2420
+ });
2421
+ const checkDataSize = /* @__PURE__ */ __name((data) => {
2422
+ try {
2423
+ const jsonString = JSON.stringify(data);
2424
+ const sizeInBytes = new Blob([jsonString]).size;
2425
+ const sizeInKB = sizeInBytes / 1024;
2426
+ if (sizeInKB > 1024) {
2427
+ authLogger.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
2428
+ return false;
2429
+ }
2430
+ return true;
2431
+ } catch (error) {
2432
+ authLogger.error(`Error checking data size for key "${key}":`, error);
2433
+ return false;
2434
+ }
2435
+ }, "checkDataSize");
2436
+ const clearOldData = /* @__PURE__ */ __name(() => {
2437
+ try {
2438
+ const keys = Object.keys(sessionStorage).filter((key2) => key2 && typeof key2 === "string");
2439
+ if (keys.length > 50) {
2440
+ const itemsToRemove = Math.ceil(keys.length * 0.2);
2441
+ for (let i = 0; i < itemsToRemove; i++) {
2442
+ try {
2443
+ const key2 = keys[i];
2444
+ if (key2) {
2445
+ sessionStorage.removeItem(key2);
2446
+ sessionStorage.removeItem(`${key2}_timestamp`);
2447
+ }
2448
+ } catch {
2449
+ }
2450
+ }
2451
+ }
2452
+ } catch (error) {
2453
+ authLogger.error("Error clearing old sessionStorage data:", error);
2454
+ }
2455
+ }, "clearOldData");
2456
+ const forceClearAll = /* @__PURE__ */ __name(() => {
2457
+ try {
2458
+ const keys = Object.keys(sessionStorage);
2459
+ for (const key2 of keys) {
2460
+ try {
2461
+ sessionStorage.removeItem(key2);
2462
+ } catch {
2463
+ }
2464
+ }
2465
+ } catch (error) {
2466
+ authLogger.error("Error force clearing sessionStorage:", error);
2467
+ }
2468
+ }, "forceClearAll");
2469
+ const setValue = /* @__PURE__ */ __name((value) => {
2470
+ try {
2471
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
2472
+ if (!checkDataSize(valueToStore)) {
2473
+ authLogger.warn(`Data size too large for key "${key}", removing key`);
2474
+ try {
2475
+ window.sessionStorage.removeItem(key);
2476
+ window.sessionStorage.removeItem(`${key}_timestamp`);
2477
+ } catch {
2478
+ }
2479
+ setStoredValue(valueToStore);
2480
+ return;
2481
+ }
2482
+ setStoredValue(valueToStore);
2483
+ if (typeof window !== "undefined") {
2484
+ try {
2485
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
2486
+ window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
2487
+ } catch (storageError) {
2488
+ if (storageError.name === "QuotaExceededError" || storageError.code === 22 || storageError.message?.includes("quota")) {
2489
+ authLogger.warn("sessionStorage quota exceeded, clearing old data...");
2490
+ clearOldData();
2491
+ try {
2492
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
2493
+ window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
2494
+ } catch (retryError) {
2495
+ authLogger.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
2496
+ try {
2497
+ forceClearAll();
2498
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
2499
+ window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
2500
+ } catch (finalError) {
2501
+ authLogger.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
2502
+ setStoredValue(valueToStore);
2503
+ }
2504
+ }
2505
+ } else {
2506
+ throw storageError;
2507
+ }
2508
+ }
2509
+ }
2510
+ } catch (error) {
2511
+ authLogger.error(`Error setting sessionStorage key "${key}":`, error);
2512
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
2513
+ setStoredValue(valueToStore);
2514
+ }
2515
+ }, "setValue");
2516
+ const removeValue = /* @__PURE__ */ __name(() => {
2517
+ try {
2518
+ setStoredValue(initialValue);
2519
+ if (typeof window !== "undefined") {
2520
+ try {
2521
+ window.sessionStorage.removeItem(key);
2522
+ window.sessionStorage.removeItem(`${key}_timestamp`);
2523
+ } catch (removeError) {
2524
+ if (removeError.name === "QuotaExceededError" || removeError.code === 22 || removeError.message?.includes("quota")) {
2525
+ authLogger.warn("sessionStorage quota exceeded during removal, clearing old data...");
2526
+ clearOldData();
2527
+ try {
2528
+ window.sessionStorage.removeItem(key);
2529
+ window.sessionStorage.removeItem(`${key}_timestamp`);
2530
+ } catch (retryError) {
2531
+ authLogger.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
2532
+ forceClearAll();
2533
+ }
2534
+ } else {
2535
+ throw removeError;
2536
+ }
2537
+ }
2538
+ }
2539
+ } catch (error) {
2540
+ authLogger.error(`Error removing sessionStorage key "${key}":`, error);
2541
+ }
2542
+ }, "removeValue");
2543
+ return [storedValue, setValue, removeValue];
2544
+ }
2545
+ __name(useSessionStorage, "useSessionStorage");
2546
+
2547
+ // src/auth/hooks/useAuthRedirect.ts
2548
+ var AUTH_REDIRECT_KEY = "auth_redirect_url";
2549
+ var getRedirectFromStorage = /* @__PURE__ */ __name(() => {
2550
+ if (typeof window === "undefined") return "";
2551
+ try {
2552
+ const item = window.sessionStorage.getItem(AUTH_REDIRECT_KEY);
2553
+ return item ? JSON.parse(item) : "";
2554
+ } catch {
2555
+ return "";
2556
+ }
2557
+ }, "getRedirectFromStorage");
2558
+ var clearRedirectFromStorage = /* @__PURE__ */ __name(() => {
2559
+ if (typeof window === "undefined") return;
2560
+ try {
2561
+ window.sessionStorage.removeItem(AUTH_REDIRECT_KEY);
2562
+ window.sessionStorage.removeItem(`${AUTH_REDIRECT_KEY}_timestamp`);
2563
+ } catch {
2564
+ }
2565
+ }, "clearRedirectFromStorage");
2566
+ var peekSavedRedirect = /* @__PURE__ */ __name(() => getRedirectFromStorage() || null, "peekSavedRedirect");
2567
+ var consumeSavedRedirect = /* @__PURE__ */ __name(() => {
2568
+ const stored = getRedirectFromStorage();
2569
+ if (stored) clearRedirectFromStorage();
2570
+ return stored || null;
2571
+ }, "consumeSavedRedirect");
2572
+ var useAuthRedirectManager = /* @__PURE__ */ __name((options = {}) => {
2573
+ const { fallbackUrl = "/dashboard", clearOnUse = true } = options;
2574
+ const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage(AUTH_REDIRECT_KEY, "");
2575
+ const setRedirect = /* @__PURE__ */ __name((url) => {
2576
+ setRedirectUrl(url);
2577
+ }, "setRedirect");
2578
+ const getRedirect = /* @__PURE__ */ __name(() => {
2579
+ return getRedirectFromStorage() || redirectUrl;
2580
+ }, "getRedirect");
2581
+ const clearRedirect = /* @__PURE__ */ __name(() => {
2582
+ removeRedirectUrl();
2583
+ clearRedirectFromStorage();
2584
+ }, "clearRedirect");
2585
+ const hasRedirect = /* @__PURE__ */ __name(() => {
2586
+ const stored = getRedirectFromStorage();
2587
+ return stored.length > 0 || redirectUrl.length > 0;
2588
+ }, "hasRedirect");
2589
+ const getFinalRedirectUrl = /* @__PURE__ */ __name(() => {
2590
+ const stored = getRedirectFromStorage();
2591
+ return stored || redirectUrl || fallbackUrl;
2592
+ }, "getFinalRedirectUrl");
2593
+ const useAndClearRedirect = /* @__PURE__ */ __name(() => {
2594
+ const stored = getRedirectFromStorage();
2595
+ const finalUrl = stored || redirectUrl || fallbackUrl;
2596
+ if (clearOnUse) {
2597
+ clearRedirect();
2598
+ }
2599
+ return finalUrl;
2600
+ }, "useAndClearRedirect");
2601
+ return {
2602
+ redirectUrl,
2603
+ setRedirect,
2604
+ getRedirect,
2605
+ clearRedirect,
2606
+ hasRedirect,
2607
+ getFinalRedirectUrl,
2608
+ useAndClearRedirect
2609
+ };
2610
+ }, "useAuthRedirectManager");
2542
2611
 
2543
2612
  // src/auth/hooks/useTwoFactor.ts
2544
2613
  var useTwoFactor = /* @__PURE__ */ __name((options = {}) => {
2545
2614
  const { onSuccess, onError, redirectUrl, skipRedirect = false } = options;
2546
2615
  const router = useCfgRouter();
2547
- const [isLoading, setIsLoading] = useState3(false);
2548
- const [error, setError] = useState3(null);
2549
- const [warning, setWarning] = useState3(null);
2550
- const [remainingBackupCodes, setRemainingBackupCodes] = useState3(null);
2551
- const [attemptsRemaining, setAttemptsRemaining] = useState3(null);
2616
+ const [isLoading, setIsLoading] = useState4(false);
2617
+ const [error, setError] = useState4(null);
2618
+ const [warning, setWarning] = useState4(null);
2619
+ const [remainingBackupCodes, setRemainingBackupCodes] = useState4(null);
2620
+ const [attemptsRemaining, setAttemptsRemaining] = useState4(null);
2552
2621
  const clearError = useCallback4(() => {
2553
2622
  setError(null);
2554
2623
  }, []);
2555
2624
  const handleSuccess = useCallback4((response) => {
2556
- CfgAccountsApi.setToken(response.access_token);
2557
- CfgAccountsApi.setRefreshToken(response.refresh_token);
2625
+ auth.setSession({ access: response.access_token, refresh: response.refresh_token });
2558
2626
  if (response.warning) {
2559
2627
  setWarning(response.warning);
2560
2628
  }
@@ -2570,7 +2638,7 @@ var useTwoFactor = /* @__PURE__ */ __name((options = {}) => {
2570
2638
  }
2571
2639
  onSuccess?.(response.user);
2572
2640
  if (!skipRedirect) {
2573
- const finalRedirectUrl = redirectUrl || "/dashboard";
2641
+ const finalRedirectUrl = consumeSavedRedirect() || redirectUrl || "/";
2574
2642
  authLogger.info("2FA successful, redirecting to:", finalRedirectUrl);
2575
2643
  router.hardPush(finalRedirectUrl);
2576
2644
  }
@@ -2960,12 +3028,12 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
2960
3028
  }, "useAuthForm");
2961
3029
 
2962
3030
  // src/auth/hooks/useGithubAuth.ts
2963
- import { useCallback as useCallback6, useState as useState4 } from "react";
3031
+ import { useCallback as useCallback6, useState as useState5 } from "react";
2964
3032
  var useGithubAuth = /* @__PURE__ */ __name((options = {}) => {
2965
3033
  const { sourceUrl, onSuccess, onError, onRequires2FA, redirectUrl, skipRedirect = false } = options;
2966
3034
  const router = useCfgRouter();
2967
- const [isLoading, setIsLoading] = useState4(false);
2968
- const [error, setError] = useState4(null);
3035
+ const [isLoading, setIsLoading] = useState5(false);
3036
+ const [error, setError] = useState5(null);
2969
3037
  const startGithubAuth = useCallback6(async () => {
2970
3038
  setIsLoading(true);
2971
3039
  setError(null);
@@ -3035,8 +3103,7 @@ var useGithubAuth = /* @__PURE__ */ __name((options = {}) => {
3035
3103
  throw new Error("Invalid response from OAuth callback");
3036
3104
  }
3037
3105
  authLogger.info("GitHub OAuth successful, user:", response.user);
3038
- CfgAccountsApi.setToken(response.access);
3039
- CfgAccountsApi.setRefreshToken(response.refresh);
3106
+ auth.setSession({ access: response.access, refresh: response.refresh });
3040
3107
  Analytics.event("auth_login_success" /* AUTH_LOGIN_SUCCESS */, {
3041
3108
  category: "auth" /* AUTH */,
3042
3109
  label: "github"
@@ -3071,15 +3138,15 @@ var useGithubAuth = /* @__PURE__ */ __name((options = {}) => {
3071
3138
  }, "useGithubAuth");
3072
3139
 
3073
3140
  // src/auth/hooks/useTwoFactorSetup.ts
3074
- import { useCallback as useCallback7, useState as useState5 } from "react";
3141
+ import { useCallback as useCallback7, useState as useState6 } from "react";
3075
3142
  var useTwoFactorSetup = /* @__PURE__ */ __name((options = {}) => {
3076
3143
  const { onComplete, onError } = options;
3077
- const [isLoading, setIsLoading] = useState5(false);
3078
- const [error, setError] = useState5(null);
3079
- const [setupData, setSetupData] = useState5(null);
3080
- const [backupCodes, setBackupCodes] = useState5(null);
3081
- const [backupCodesWarning, setBackupCodesWarning] = useState5(null);
3082
- const [setupStep, setSetupStep] = useState5("idle");
3144
+ const [isLoading, setIsLoading] = useState6(false);
3145
+ const [error, setError] = useState6(null);
3146
+ const [setupData, setSetupData] = useState6(null);
3147
+ const [backupCodes, setBackupCodes] = useState6(null);
3148
+ const [backupCodesWarning, setBackupCodesWarning] = useState6(null);
3149
+ const [setupStep, setSetupStep] = useState6("idle");
3083
3150
  const clearError = useCallback7(() => {
3084
3151
  setError(null);
3085
3152
  }, []);
@@ -3178,7 +3245,7 @@ var useTwoFactorSetup = /* @__PURE__ */ __name((options = {}) => {
3178
3245
  }, "useTwoFactorSetup");
3179
3246
 
3180
3247
  // src/auth/hooks/useTwoFactorStatus.ts
3181
- import { useCallback as useCallback8, useState as useState6 } from "react";
3248
+ import { useCallback as useCallback8, useState as useState7 } from "react";
3182
3249
  function extractErrorMessage(err, fallback) {
3183
3250
  if (err instanceof APIError) {
3184
3251
  const body = err.response;
@@ -3191,10 +3258,10 @@ function extractErrorMessage(err, fallback) {
3191
3258
  }
3192
3259
  __name(extractErrorMessage, "extractErrorMessage");
3193
3260
  var useTwoFactorStatus = /* @__PURE__ */ __name(() => {
3194
- const [isLoading, setIsLoading] = useState6(false);
3195
- const [error, setError] = useState6(null);
3196
- const [has2FAEnabled, setHas2FAEnabled] = useState6(null);
3197
- const [devices, setDevices] = useState6([]);
3261
+ const [isLoading, setIsLoading] = useState7(false);
3262
+ const [error, setError] = useState7(null);
3263
+ const [has2FAEnabled, setHas2FAEnabled] = useState7(null);
3264
+ const [devices, setDevices] = useState7([]);
3198
3265
  const clearError = useCallback8(() => {
3199
3266
  setError(null);
3200
3267
  }, []);
@@ -3229,258 +3296,38 @@ var useTwoFactorStatus = /* @__PURE__ */ __name(() => {
3229
3296
  return false;
3230
3297
  }
3231
3298
  setIsLoading(true);
3232
- setError(null);
3233
- try {
3234
- authLogger.info("Disabling 2FA...");
3235
- await CfgTotp.cfgTotpDisableCreate({ body: { code }, throwOnError: true });
3236
- setHas2FAEnabled(false);
3237
- setDevices([]);
3238
- authLogger.info("2FA disabled successfully");
3239
- return true;
3240
- } catch (err) {
3241
- const errorMessage = extractErrorMessage(err, "Invalid verification code");
3242
- authLogger.error("Failed to disable 2FA:", err);
3243
- setError(errorMessage);
3244
- return false;
3245
- } finally {
3246
- setIsLoading(false);
3247
- }
3248
- }, []);
3249
- return {
3250
- isLoading,
3251
- error,
3252
- has2FAEnabled,
3253
- devices,
3254
- fetchStatus,
3255
- disable2FA,
3256
- clearError
3257
- };
3258
- }, "useTwoFactorStatus");
3259
-
3260
- // src/auth/hooks/useSessionStorage.ts
3261
- import { useState as useState7 } from "react";
3262
- function useSessionStorage(key, initialValue) {
3263
- const [storedValue, setStoredValue] = useState7(() => {
3264
- if (typeof window === "undefined") {
3265
- return initialValue;
3266
- }
3267
- try {
3268
- const item = window.sessionStorage.getItem(key);
3269
- return item ? JSON.parse(item) : initialValue;
3270
- } catch (error) {
3271
- authLogger.error(`Error reading sessionStorage key "${key}":`, error);
3272
- return initialValue;
3273
- }
3274
- });
3275
- const checkDataSize = /* @__PURE__ */ __name((data) => {
3276
- try {
3277
- const jsonString = JSON.stringify(data);
3278
- const sizeInBytes = new Blob([jsonString]).size;
3279
- const sizeInKB = sizeInBytes / 1024;
3280
- if (sizeInKB > 1024) {
3281
- authLogger.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
3282
- return false;
3283
- }
3284
- return true;
3285
- } catch (error) {
3286
- authLogger.error(`Error checking data size for key "${key}":`, error);
3287
- return false;
3288
- }
3289
- }, "checkDataSize");
3290
- const clearOldData = /* @__PURE__ */ __name(() => {
3291
- try {
3292
- const keys = Object.keys(sessionStorage).filter((key2) => key2 && typeof key2 === "string");
3293
- if (keys.length > 50) {
3294
- const itemsToRemove = Math.ceil(keys.length * 0.2);
3295
- for (let i = 0; i < itemsToRemove; i++) {
3296
- try {
3297
- const key2 = keys[i];
3298
- if (key2) {
3299
- sessionStorage.removeItem(key2);
3300
- sessionStorage.removeItem(`${key2}_timestamp`);
3301
- }
3302
- } catch {
3303
- }
3304
- }
3305
- }
3306
- } catch (error) {
3307
- authLogger.error("Error clearing old sessionStorage data:", error);
3308
- }
3309
- }, "clearOldData");
3310
- const forceClearAll = /* @__PURE__ */ __name(() => {
3311
- try {
3312
- const keys = Object.keys(sessionStorage);
3313
- for (const key2 of keys) {
3314
- try {
3315
- sessionStorage.removeItem(key2);
3316
- } catch {
3317
- }
3318
- }
3319
- } catch (error) {
3320
- authLogger.error("Error force clearing sessionStorage:", error);
3321
- }
3322
- }, "forceClearAll");
3323
- const setValue = /* @__PURE__ */ __name((value) => {
3324
- try {
3325
- const valueToStore = value instanceof Function ? value(storedValue) : value;
3326
- if (!checkDataSize(valueToStore)) {
3327
- authLogger.warn(`Data size too large for key "${key}", removing key`);
3328
- try {
3329
- window.sessionStorage.removeItem(key);
3330
- window.sessionStorage.removeItem(`${key}_timestamp`);
3331
- } catch {
3332
- }
3333
- setStoredValue(valueToStore);
3334
- return;
3335
- }
3336
- setStoredValue(valueToStore);
3337
- if (typeof window !== "undefined") {
3338
- try {
3339
- window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
3340
- window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
3341
- } catch (storageError) {
3342
- if (storageError.name === "QuotaExceededError" || storageError.code === 22 || storageError.message?.includes("quota")) {
3343
- authLogger.warn("sessionStorage quota exceeded, clearing old data...");
3344
- clearOldData();
3345
- try {
3346
- window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
3347
- window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
3348
- } catch (retryError) {
3349
- authLogger.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
3350
- try {
3351
- forceClearAll();
3352
- window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
3353
- window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
3354
- } catch (finalError) {
3355
- authLogger.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
3356
- setStoredValue(valueToStore);
3357
- }
3358
- }
3359
- } else {
3360
- throw storageError;
3361
- }
3362
- }
3363
- }
3364
- } catch (error) {
3365
- authLogger.error(`Error setting sessionStorage key "${key}":`, error);
3366
- const valueToStore = value instanceof Function ? value(storedValue) : value;
3367
- setStoredValue(valueToStore);
3368
- }
3369
- }, "setValue");
3370
- const removeValue = /* @__PURE__ */ __name(() => {
3371
- try {
3372
- setStoredValue(initialValue);
3373
- if (typeof window !== "undefined") {
3374
- try {
3375
- window.sessionStorage.removeItem(key);
3376
- window.sessionStorage.removeItem(`${key}_timestamp`);
3377
- } catch (removeError) {
3378
- if (removeError.name === "QuotaExceededError" || removeError.code === 22 || removeError.message?.includes("quota")) {
3379
- authLogger.warn("sessionStorage quota exceeded during removal, clearing old data...");
3380
- clearOldData();
3381
- try {
3382
- window.sessionStorage.removeItem(key);
3383
- window.sessionStorage.removeItem(`${key}_timestamp`);
3384
- } catch (retryError) {
3385
- authLogger.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
3386
- forceClearAll();
3387
- }
3388
- } else {
3389
- throw removeError;
3390
- }
3391
- }
3392
- }
3393
- } catch (error) {
3394
- authLogger.error(`Error removing sessionStorage key "${key}":`, error);
3395
- }
3396
- }, "removeValue");
3397
- return [storedValue, setValue, removeValue];
3398
- }
3399
- __name(useSessionStorage, "useSessionStorage");
3400
-
3401
- // src/auth/hooks/useAuthRedirect.ts
3402
- var AUTH_REDIRECT_KEY = "auth_redirect_url";
3403
- var getRedirectFromStorage = /* @__PURE__ */ __name(() => {
3404
- if (typeof window === "undefined") return "";
3405
- try {
3406
- const item = window.sessionStorage.getItem(AUTH_REDIRECT_KEY);
3407
- return item ? JSON.parse(item) : "";
3408
- } catch {
3409
- return "";
3410
- }
3411
- }, "getRedirectFromStorage");
3412
- var clearRedirectFromStorage = /* @__PURE__ */ __name(() => {
3413
- if (typeof window === "undefined") return;
3414
- try {
3415
- window.sessionStorage.removeItem(AUTH_REDIRECT_KEY);
3416
- window.sessionStorage.removeItem(`${AUTH_REDIRECT_KEY}_timestamp`);
3417
- } catch {
3418
- }
3419
- }, "clearRedirectFromStorage");
3420
- var useAuthRedirectManager = /* @__PURE__ */ __name((options = {}) => {
3421
- const { fallbackUrl = "/dashboard", clearOnUse = true } = options;
3422
- const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage(AUTH_REDIRECT_KEY, "");
3423
- const setRedirect = /* @__PURE__ */ __name((url) => {
3424
- setRedirectUrl(url);
3425
- }, "setRedirect");
3426
- const getRedirect = /* @__PURE__ */ __name(() => {
3427
- return getRedirectFromStorage() || redirectUrl;
3428
- }, "getRedirect");
3429
- const clearRedirect = /* @__PURE__ */ __name(() => {
3430
- removeRedirectUrl();
3431
- clearRedirectFromStorage();
3432
- }, "clearRedirect");
3433
- const hasRedirect = /* @__PURE__ */ __name(() => {
3434
- const stored = getRedirectFromStorage();
3435
- return stored.length > 0 || redirectUrl.length > 0;
3436
- }, "hasRedirect");
3437
- const getFinalRedirectUrl = /* @__PURE__ */ __name(() => {
3438
- const stored = getRedirectFromStorage();
3439
- return stored || redirectUrl || fallbackUrl;
3440
- }, "getFinalRedirectUrl");
3441
- const useAndClearRedirect = /* @__PURE__ */ __name(() => {
3442
- const stored = getRedirectFromStorage();
3443
- const finalUrl = stored || redirectUrl || fallbackUrl;
3444
- if (clearOnUse) {
3445
- clearRedirect();
3299
+ setError(null);
3300
+ try {
3301
+ authLogger.info("Disabling 2FA...");
3302
+ await CfgTotp.cfgTotpDisableCreate({ body: { code }, throwOnError: true });
3303
+ setHas2FAEnabled(false);
3304
+ setDevices([]);
3305
+ authLogger.info("2FA disabled successfully");
3306
+ return true;
3307
+ } catch (err) {
3308
+ const errorMessage = extractErrorMessage(err, "Invalid verification code");
3309
+ authLogger.error("Failed to disable 2FA:", err);
3310
+ setError(errorMessage);
3311
+ return false;
3312
+ } finally {
3313
+ setIsLoading(false);
3446
3314
  }
3447
- return finalUrl;
3448
- }, "useAndClearRedirect");
3315
+ }, []);
3449
3316
  return {
3450
- redirectUrl,
3451
- setRedirect,
3452
- getRedirect,
3453
- clearRedirect,
3454
- hasRedirect,
3455
- getFinalRedirectUrl,
3456
- useAndClearRedirect
3317
+ isLoading,
3318
+ error,
3319
+ has2FAEnabled,
3320
+ devices,
3321
+ fetchStatus,
3322
+ disable2FA,
3323
+ clearError
3457
3324
  };
3458
- }, "useAuthRedirectManager");
3459
-
3460
- // src/auth/hooks/useAuthGuard.ts
3461
- import { useEffect as useEffect5, useState as useState8 } from "react";
3462
- var useAuthGuard = /* @__PURE__ */ __name((options = {}) => {
3463
- const { redirectTo = "/auth", requireAuth = true, saveRedirectUrl: shouldSaveUrl = true } = options;
3464
- const { isAuthenticated, isLoading, saveRedirectUrl } = useAuth();
3465
- const router = useCfgRouter();
3466
- const [isRedirecting, setIsRedirecting] = useState8(false);
3467
- useEffect5(() => {
3468
- if (!isLoading && requireAuth && !isAuthenticated && !isRedirecting) {
3469
- if (shouldSaveUrl && typeof window !== "undefined") {
3470
- const currentUrl = window.location.pathname + window.location.search;
3471
- saveRedirectUrl(currentUrl);
3472
- }
3473
- setIsRedirecting(true);
3474
- router.push(redirectTo);
3475
- }
3476
- }, [isAuthenticated, isLoading, router, redirectTo, requireAuth, isRedirecting, shouldSaveUrl, saveRedirectUrl]);
3477
- return { isAuthenticated, isLoading, isRedirecting };
3478
- }, "useAuthGuard");
3325
+ }, "useTwoFactorStatus");
3479
3326
 
3480
3327
  // src/auth/hooks/useLocalStorage.ts
3481
- import { useState as useState9 } from "react";
3328
+ import { useState as useState8 } from "react";
3482
3329
  function useLocalStorage(key, initialValue) {
3483
- const [storedValue, setStoredValue] = useState9(() => {
3330
+ const [storedValue, setStoredValue] = useState8(() => {
3484
3331
  if (typeof window === "undefined") {
3485
3332
  return initialValue;
3486
3333
  }
@@ -3775,150 +3622,16 @@ function getCacheMetadata() {
3775
3622
  }
3776
3623
  __name(getCacheMetadata, "getCacheMetadata");
3777
3624
 
3778
- // src/auth/hooks/useTokenRefresh.ts
3779
- import { useCallback as useCallback9, useEffect as useEffect7 } from "react";
3780
-
3781
- // src/auth/refreshHandler.ts
3782
- var _registered = false;
3783
- function ensureRefreshHandler() {
3784
- if (_registered) return;
3785
- _registered = true;
3786
- CfgAccountsApi.setRefreshHandler(async (refresh) => {
3787
- const result = await CfgAccountsAuth.cfgAccountsTokenRefreshCreate({
3788
- body: { refresh },
3789
- throwOnError: true
3790
- });
3791
- const access = result.data?.access;
3792
- if (!access) {
3793
- authLogger.error("Refresh response missing access token");
3794
- return null;
3795
- }
3796
- return { access, refresh: result.data?.refresh ?? refresh };
3797
- });
3798
- authLogger.debug("Canonical refresh handler registered");
3799
- }
3800
- __name(ensureRefreshHandler, "ensureRefreshHandler");
3801
- function triggerRefresh() {
3802
- ensureRefreshHandler();
3803
- const maybe = CfgAccountsApi;
3804
- if (typeof maybe.refreshNow === "function") {
3805
- return maybe.refreshNow();
3806
- }
3807
- authLogger.warn("api.refreshNow() unavailable \u2014 run `make generate` to regenerate the client");
3808
- return Promise.resolve(null);
3809
- }
3810
- __name(triggerRefresh, "triggerRefresh");
3811
-
3812
- // src/auth/utils/jwt.ts
3813
- function decodeSegment(segment) {
3814
- try {
3815
- const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
3816
- if (typeof atob !== "function") return null;
3817
- const json = atob(base64);
3818
- return JSON.parse(json);
3819
- } catch {
3820
- return null;
3821
- }
3822
- }
3823
- __name(decodeSegment, "decodeSegment");
3824
- function getTokenExpiry(token) {
3825
- if (!token || typeof token !== "string") return null;
3826
- const parts = token.split(".");
3827
- if (parts.length !== 3) return null;
3828
- const payload = decodeSegment(parts[1]);
3829
- if (!payload || typeof payload !== "object") return null;
3830
- const exp = payload.exp;
3831
- if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
3832
- return exp * 1e3;
3833
- }
3834
- __name(getTokenExpiry, "getTokenExpiry");
3835
- function isTokenExpired(token, skewMs = 0, now = Date.now()) {
3836
- const expiry = getTokenExpiry(token);
3837
- if (expiry === null) return true;
3838
- return expiry - skewMs <= now;
3839
- }
3840
- __name(isTokenExpired, "isTokenExpired");
3841
- function isTokenExpiringSoon(token, thresholdMs, now = Date.now()) {
3842
- const expiry = getTokenExpiry(token);
3843
- if (expiry === null) return false;
3844
- return expiry - now < thresholdMs;
3845
- }
3846
- __name(isTokenExpiringSoon, "isTokenExpiringSoon");
3847
-
3848
- // src/auth/hooks/useTokenRefresh.ts
3849
- var TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1e3;
3850
- var CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3851
- function useTokenRefresh(options = {}) {
3852
- const { enabled = true, onRefresh, onRefreshError } = options;
3853
- const refreshToken = useCallback9(async () => {
3854
- if (!CfgAccountsApi.getRefreshToken()) {
3855
- authLogger.warn("No refresh token available");
3856
- return false;
3857
- }
3858
- authLogger.info("Refreshing token...");
3859
- try {
3860
- const newAccessToken = await triggerRefresh();
3861
- if (!newAccessToken) {
3862
- throw new Error("Token refresh failed");
3863
- }
3864
- authLogger.info("Token refreshed successfully");
3865
- onRefresh?.(newAccessToken);
3866
- return true;
3867
- } catch (error) {
3868
- authLogger.error("Token refresh error:", error);
3869
- onRefreshError?.(error instanceof Error ? error : new Error(String(error)));
3870
- return false;
3871
- }
3872
- }, [onRefresh, onRefreshError]);
3873
- const checkAndRefresh = useCallback9(async () => {
3874
- const token = CfgAccountsApi.getToken();
3875
- if (!token) return;
3876
- if (isTokenExpiringSoon(token, TOKEN_REFRESH_THRESHOLD_MS)) {
3877
- authLogger.info("Token expiring soon, refreshing proactively");
3878
- await refreshToken();
3879
- }
3880
- }, [refreshToken]);
3881
- useEffect7(() => {
3882
- if (!enabled) return;
3883
- checkAndRefresh();
3884
- const intervalId = setInterval(checkAndRefresh, CHECK_INTERVAL_MS);
3885
- return () => clearInterval(intervalId);
3886
- }, [enabled, checkAndRefresh]);
3887
- useEffect7(() => {
3888
- if (!enabled) return;
3889
- const handleFocus = /* @__PURE__ */ __name(() => {
3890
- authLogger.debug("Window focused, checking token...");
3891
- checkAndRefresh();
3892
- }, "handleFocus");
3893
- window.addEventListener("focus", handleFocus);
3894
- return () => window.removeEventListener("focus", handleFocus);
3895
- }, [enabled, checkAndRefresh]);
3896
- useEffect7(() => {
3897
- if (!enabled) return;
3898
- const handleOnline = /* @__PURE__ */ __name(() => {
3899
- authLogger.info("Network reconnected, checking token...");
3900
- checkAndRefresh();
3901
- }, "handleOnline");
3902
- window.addEventListener("online", handleOnline);
3903
- return () => window.removeEventListener("online", handleOnline);
3904
- }, [enabled, checkAndRefresh]);
3905
- return {
3906
- refreshToken,
3907
- checkAndRefresh
3908
- };
3909
- }
3910
- __name(useTokenRefresh, "useTokenRefresh");
3911
-
3912
3625
  // src/auth/hooks/useDeleteAccount.ts
3913
- import { useCallback as useCallback10, useState as useState10 } from "react";
3626
+ import { useCallback as useCallback9, useState as useState9 } from "react";
3914
3627
  var useDeleteAccount = /* @__PURE__ */ __name(() => {
3915
- const [isLoading, setIsLoading] = useState10(false);
3916
- const [error, setError] = useState10(null);
3628
+ const [isLoading, setIsLoading] = useState9(false);
3629
+ const [error, setError] = useState9(null);
3917
3630
  const { logout } = useAuth();
3918
- const clearError = useCallback10(() => {
3631
+ const clearError = useCallback9(() => {
3919
3632
  setError(null);
3920
3633
  }, []);
3921
- const deleteAccount = useCallback10(async () => {
3634
+ const deleteAccount = useCallback9(async () => {
3922
3635
  setIsLoading(true);
3923
3636
  setError(null);
3924
3637
  try {
@@ -3947,39 +3660,15 @@ var useDeleteAccount = /* @__PURE__ */ __name(() => {
3947
3660
  };
3948
3661
  }, "useDeleteAccount");
3949
3662
 
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
-
3974
3663
  // src/auth/context/AccountsContext.tsx
3975
3664
  import {
3976
3665
  createContext,
3977
- useCallback as useCallback11,
3666
+ useCallback as useCallback10,
3978
3667
  useContext,
3979
- useEffect as useEffect8,
3668
+ useEffect as useEffect6,
3980
3669
  useMemo as useMemo2,
3981
3670
  useRef as useRef5,
3982
- useState as useState11
3671
+ useState as useState10
3983
3672
  } from "react";
3984
3673
 
3985
3674
  // src/_api/generated/_cfg_accounts/hooks/useCfgAccountsApiKeyRegenerateCreate.ts
@@ -4432,9 +4121,12 @@ function useCfgAccountsProfileUpdateUpdate(config) {
4432
4121
  }
4433
4122
  __name(useCfgAccountsProfileUpdateUpdate, "useCfgAccountsProfileUpdateUpdate");
4434
4123
 
4435
- // src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenRefreshCreate.ts
4124
+ // src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenBlacklistCreate.ts
4436
4125
  import useSWRMutation15 from "swr/mutation";
4437
4126
 
4127
+ // src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenRefreshCreate.ts
4128
+ import useSWRMutation16 from "swr/mutation";
4129
+
4438
4130
  // src/_api/generated/_cfg_accounts/schemas/TokenRefresh.ts
4439
4131
  import { z as z19 } from "zod";
4440
4132
  var TokenRefreshSchema = z19.object({
@@ -4442,53 +4134,6 @@ var TokenRefreshSchema = z19.object({
4442
4134
  refresh: z19.string()
4443
4135
  });
4444
4136
 
4445
- // src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenRefreshCreate.ts
4446
- function useCfgAccountsTokenRefreshCreate(config) {
4447
- return useSWRMutation15(
4448
- ["cfg_accounts_token_refresh_create"],
4449
- async (_key, { arg }) => {
4450
- const res = await CfgAccountsAuth.cfgAccountsTokenRefreshCreate({ ...arg, throwOnError: true });
4451
- const data = res.data;
4452
- const parsed = TokenRefreshSchema.safeParse(data);
4453
- if (!parsed.success) {
4454
- console.warn(
4455
- "[zod] response did not match schema",
4456
- {
4457
- operation: "cfg_accounts_token_refresh_create",
4458
- method: "POST",
4459
- path: "/cfg/accounts/token/refresh/",
4460
- issues: parsed.error.issues,
4461
- data
4462
- }
4463
- );
4464
- if (typeof window !== "undefined") {
4465
- try {
4466
- window.dispatchEvent(
4467
- new CustomEvent("zod-validation-error", {
4468
- detail: {
4469
- operation: "cfg_accounts_token_refresh_create",
4470
- method: "POST",
4471
- path: "/cfg/accounts/token/refresh/",
4472
- issues: parsed.error.issues,
4473
- data,
4474
- timestamp: /* @__PURE__ */ new Date()
4475
- },
4476
- bubbles: true,
4477
- cancelable: false
4478
- })
4479
- );
4480
- } catch {
4481
- }
4482
- }
4483
- return data;
4484
- }
4485
- return parsed.data;
4486
- },
4487
- config
4488
- );
4489
- }
4490
- __name(useCfgAccountsTokenRefreshCreate, "useCfgAccountsTokenRefreshCreate");
4491
-
4492
4137
  // src/_api/generated/_cfg_accounts/schemas/APIKeyRequest.ts
4493
4138
  import { z as z20 } from "zod";
4494
4139
  var APIKeyRequestSchema = z20.object({
@@ -4578,28 +4223,34 @@ var PatchedCfgUserUpdateRequestSchema = z30.object({
4578
4223
  timezone: z30.string().max(64).optional()
4579
4224
  });
4580
4225
 
4581
- // src/_api/generated/_cfg_accounts/schemas/TokenRefreshRequest.ts
4226
+ // src/_api/generated/_cfg_accounts/schemas/TokenBlacklistRequest.ts
4582
4227
  import { z as z31 } from "zod";
4583
- var TokenRefreshRequestSchema = z31.object({
4228
+ var TokenBlacklistRequestSchema = z31.object({
4584
4229
  refresh: z31.string().min(1)
4585
4230
  });
4586
4231
 
4232
+ // src/_api/generated/_cfg_accounts/schemas/TokenRefreshRequest.ts
4233
+ import { z as z32 } from "zod";
4234
+ var TokenRefreshRequestSchema = z32.object({
4235
+ refresh: z32.string().min(1)
4236
+ });
4237
+
4587
4238
  // src/auth/context/AccountsContext.tsx
4588
4239
  import { jsx } from "react/jsx-runtime";
4589
4240
  var AccountsContext = createContext(void 0);
4590
4241
  function AccountsProvider({ children }) {
4591
- const [profile, setProfile] = useState11(() => {
4242
+ const [profile, setProfile] = useState10(() => {
4592
4243
  const cached = getCachedProfile();
4593
4244
  return cached || void 0;
4594
4245
  });
4595
- const [isLoadingProfile, setIsLoadingProfile] = useState11(false);
4596
- const [profileError, setProfileError] = useState11(null);
4246
+ const [isLoadingProfile, setIsLoadingProfile] = useState10(false);
4247
+ const [profileError, setProfileError] = useState10(null);
4597
4248
  const profileRef = useRef5(profile);
4598
4249
  const isLoadingRef = useRef5(false);
4599
- useEffect8(() => {
4250
+ useEffect6(() => {
4600
4251
  profileRef.current = profile;
4601
4252
  }, [profile]);
4602
- useEffect8(() => {
4253
+ useEffect6(() => {
4603
4254
  isLoadingRef.current = isLoadingProfile;
4604
4255
  }, [isLoadingProfile]);
4605
4256
  const { trigger: triggerProfileUpdate } = useCfgAccountsProfileUpdateUpdate();
@@ -4607,8 +4258,7 @@ function AccountsProvider({ children }) {
4607
4258
  const { trigger: triggerAvatar } = useCfgAccountsProfileAvatarCreate();
4608
4259
  const { trigger: triggerOtpRequest } = useCfgAccountsOtpRequestCreate();
4609
4260
  const { trigger: triggerOtpVerify } = useCfgAccountsOtpVerifyCreate();
4610
- const { trigger: triggerTokenRefresh } = useCfgAccountsTokenRefreshCreate();
4611
- const refreshProfile = useCallback11(async (options) => {
4261
+ const refreshProfile = useCallback10(async (options) => {
4612
4262
  const { callerId, force } = options || {};
4613
4263
  if (isLoadingRef.current) {
4614
4264
  authLogger.debug(`Profile loading in progress, skipping (caller: ${callerId})`);
@@ -4660,8 +4310,7 @@ function AccountsProvider({ children }) {
4660
4310
  return result;
4661
4311
  }
4662
4312
  if (result.access && result.refresh) {
4663
- CfgAccountsApi.setToken(result.access);
4664
- CfgAccountsApi.setRefreshToken(result.refresh);
4313
+ auth.setSession({ access: result.access, refresh: result.refresh });
4665
4314
  try {
4666
4315
  await refreshProfile({ callerId: "verifyOTP", force: true });
4667
4316
  } catch (profileError2) {
@@ -4670,17 +4319,14 @@ function AccountsProvider({ children }) {
4670
4319
  }
4671
4320
  return result;
4672
4321
  }, "verifyOTP");
4673
- const refreshToken = /* @__PURE__ */ __name(async (refresh) => {
4674
- const body = { refresh };
4675
- const result = await triggerTokenRefresh({ body });
4676
- if (result.access) {
4677
- CfgAccountsApi.setToken(result.access);
4678
- CfgAccountsApi.setRefreshToken(refresh);
4322
+ const logout = useCallback10(() => {
4323
+ const refresh = auth.getRefreshToken();
4324
+ if (refresh) {
4325
+ CfgAccountsAuth.cfgAccountsTokenBlacklistCreate({ body: { refresh } }).catch(() => {
4326
+ authLogger.warn("Refresh-token blacklist failed (logged out locally anyway)");
4327
+ });
4679
4328
  }
4680
- return result;
4681
- }, "refreshToken");
4682
- const logout = useCallback11(() => {
4683
- CfgAccountsApi.clearToken();
4329
+ auth.clearSession();
4684
4330
  setProfile(void 0);
4685
4331
  setProfileError(null);
4686
4332
  clearProfileCache();
@@ -4695,7 +4341,6 @@ function AccountsProvider({ children }) {
4695
4341
  refreshProfile,
4696
4342
  requestOTP,
4697
4343
  verifyOTP,
4698
- refreshToken,
4699
4344
  logout
4700
4345
  }), [
4701
4346
  profile,
@@ -4707,7 +4352,6 @@ function AccountsProvider({ children }) {
4707
4352
  refreshProfile,
4708
4353
  requestOTP,
4709
4354
  verifyOTP,
4710
- refreshToken,
4711
4355
  logout
4712
4356
  ]);
4713
4357
  return /* @__PURE__ */ jsx(AccountsContext.Provider, { value, children });
@@ -4726,281 +4370,127 @@ __name(useAccountsContext, "useAccountsContext");
4726
4370
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
4727
4371
  var defaultRoutes = {
4728
4372
  auth: "/auth",
4729
- defaultCallback: "/dashboard",
4373
+ defaultCallback: "/",
4730
4374
  defaultAuthCallback: "/auth"
4731
4375
  };
4732
- var AuthContext = createContext2(void 0);
4376
+ var resolveAuthRoutes = /* @__PURE__ */ __name((routes) => ({
4377
+ auth: routes?.auth || defaultRoutes.auth,
4378
+ defaultCallback: routes?.defaultCallback || defaultRoutes.defaultCallback,
4379
+ defaultAuthCallback: routes?.defaultAuthCallback || routes?.auth || defaultRoutes.defaultAuthCallback
4380
+ }), "resolveAuthRoutes");
4733
4381
  var EMAIL_STORAGE_KEY = "auth_email";
4734
- var hasValidTokens = /* @__PURE__ */ __name(() => {
4735
- if (typeof window === "undefined") return false;
4736
- return CfgAccountsApi.isAuthenticated();
4737
- }, "hasValidTokens");
4738
- var isSessionDeadOnBootstrap = /* @__PURE__ */ __name((accessToken, refreshToken, now = Date.now()) => {
4739
- const accessDead = isTokenExpired(accessToken, 0, now);
4740
- if (!accessDead) return false;
4741
- const refreshDead = isTokenExpired(refreshToken, 0, now);
4742
- return refreshDead;
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");
4382
+ var AuthContext = createContext2(void 0);
4383
+ var apiErrorMessage = /* @__PURE__ */ __name((error) => {
4384
+ const body = error.response;
4385
+ return body?.error || body?.detail || body?.message || error.errorMessage;
4386
+ }, "apiErrorMessage");
4752
4387
  var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4753
4388
  const accounts = useAccountsContext();
4754
- ensureRefreshHandler();
4755
- const redirectManager = useAuthRedirectManager({
4756
- fallbackUrl: config?.routes?.defaultCallback || defaultRoutes.defaultCallback,
4757
- clearOnUse: true
4758
- });
4759
- const [isLoading, setIsLoading] = useState12(() => {
4760
- if (typeof window !== "undefined") {
4761
- const hasTokens = hasValidTokens();
4762
- return !hasTokens;
4763
- }
4764
- return true;
4765
- });
4766
- const [initialized, setInitialized] = useState12(false);
4767
- const [authTick, setAuthTick] = useState12(0);
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]);
4780
- const onUnauthorizedRef = useRef6(false);
4389
+ const session = useSession();
4390
+ const isAuthenticated = session.status === "authenticated";
4781
4391
  const router = useCfgRouter();
4782
4392
  const pathname = usePathname3();
4783
4393
  const queryParams = useQueryParams();
4784
- const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(EMAIL_STORAGE_KEY, null);
4785
- const user = accounts.profile;
4786
- const userRef = useRef6(user);
4394
+ const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage(
4395
+ EMAIL_STORAGE_KEY,
4396
+ null
4397
+ );
4398
+ const routes = useMemo3(() => resolveAuthRoutes(config?.routes), [config?.routes]);
4399
+ const routesRef = useRef6(routes);
4400
+ useEffect7(() => {
4401
+ routesRef.current = routes;
4402
+ }, [routes]);
4403
+ const redirectManager = useAuthRedirectManager({
4404
+ fallbackUrl: routes.defaultCallback,
4405
+ clearOnUse: true
4406
+ });
4787
4407
  const configRef = useRef6(config);
4788
- const isLoadingProfileRef = useRef6(false);
4789
- useEffect9(() => {
4790
- userRef.current = user;
4791
- }, [user]);
4792
- useEffect9(() => {
4408
+ useEffect7(() => {
4793
4409
  configRef.current = config;
4794
4410
  }, [config]);
4795
- const clearAuthState = useCallback12((caller) => {
4796
- authLogger.info("clearAuthState >> caller", caller);
4797
- CfgAccountsApi.clearToken();
4798
- clearProfileCache();
4799
- setInitialized(true);
4800
- setIsLoading(false);
4801
- bumpAuthTick();
4802
- }, [bumpAuthTick]);
4803
- const handleProactiveRefreshError = useCallback12((error) => {
4804
- authLogger.warn("Proactive token refresh failed \u2014 session is dead, redirecting to login:", error.message);
4805
- if (onUnauthorizedRef.current) return;
4806
- onUnauthorizedRef.current = true;
4807
- clearAuthState("proactiveRefresh:failed");
4808
- const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4809
- router.hardReplace(authCallbackUrl);
4810
- }, [clearAuthState, router]);
4811
- useTokenRefresh({
4812
- enabled: true,
4813
- onRefresh: /* @__PURE__ */ __name(() => {
4814
- authLogger.info("Token auto-refreshed successfully");
4815
- }, "onRefresh"),
4816
- onRefreshError: handleProactiveRefreshError
4817
- });
4818
- const handleGlobalAuthError = useCallback12((error, context = "API Request") => {
4819
- const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
4820
- if (isAuthError) {
4821
- authLogger.warn(`Authentication error in ${context}, clearing tokens`);
4822
- clearAuthState(`globalAuthError:${context}`);
4823
- return true;
4411
+ const accountsRef = useRef6(accounts);
4412
+ useEffect7(() => {
4413
+ accountsRef.current = accounts;
4414
+ }, [accounts]);
4415
+ const pendingManualNavRef = useRef6(false);
4416
+ const [profileSettled, setProfileSettled] = useState11(false);
4417
+ const hasProfile = Boolean(accounts.profile);
4418
+ const profileAttemptedRef = useRef6(false);
4419
+ useEffect7(() => {
4420
+ if (!isAuthenticated) {
4421
+ profileAttemptedRef.current = false;
4422
+ const hasTokens = Boolean(auth.getToken() || auth.getRefreshToken());
4423
+ if (hasTokens && !auth.isAuthenticated()) {
4424
+ authLogger.warn("Dead session in storage \u2014 clearing");
4425
+ auth.clearSession();
4426
+ clearProfileCache();
4427
+ }
4428
+ setProfileSettled(true);
4429
+ return;
4824
4430
  }
4825
- if (error?.success === false) {
4826
- authLogger.warn(`Non-auth error in ${context} (not clearing session):`, error?.message || error);
4431
+ if (hasProfile || profileAttemptedRef.current) {
4432
+ setProfileSettled(true);
4433
+ return;
4827
4434
  }
4828
- return false;
4829
- }, [clearAuthState]);
4830
- useEffect9(() => {
4831
- const handler = /* @__PURE__ */ __name(() => {
4832
- if (onUnauthorizedRef.current) return;
4833
- onUnauthorizedRef.current = true;
4834
- authLogger.warn("Unrecoverable 401 (refresh failed) \u2014 clearing session and redirecting to login");
4835
- clearAuthState("onUnauthorized:401");
4836
- const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4837
- router.hardReplace(authCallbackUrl);
4838
- }, "handler");
4839
- CfgAccountsApi.onUnauthorized(handler);
4435
+ profileAttemptedRef.current = true;
4436
+ let cancelled = false;
4437
+ accountsRef.current.refreshProfile({ callerId: "AuthProvider:bootstrap" }).catch((error) => {
4438
+ authLogger.warn("Profile load failed (session kept):", error);
4439
+ }).finally(() => {
4440
+ if (!cancelled) setProfileSettled(true);
4441
+ });
4840
4442
  return () => {
4841
- CfgAccountsApi.onUnauthorized(null);
4443
+ cancelled = true;
4842
4444
  };
4843
- }, [clearAuthState, router]);
4844
- const isAutoLoggingOutRef = useRef6(false);
4845
- const swrOnError = useCallback12((error) => {
4846
- const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
4847
- if (isAuthError && !isAutoLoggingOutRef.current) {
4848
- isAutoLoggingOutRef.current = true;
4849
- authLogger.warn("SWR 401 error detected, auto-logout");
4850
- clearAuthState("swrOnError:401");
4851
- const authCallbackUrl = configRef.current?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4852
- router.hardReplace(authCallbackUrl);
4853
- }
4854
- }, [clearAuthState, router]);
4855
- const swrConfig = useMemo3(() => ({ onError: swrOnError }), [swrOnError]);
4856
- const loadCurrentProfile = useCallback12(async (callerId) => {
4857
- const finalCallerId = callerId || "AuthContext.loadCurrentProfile";
4858
- if (isLoadingProfileRef.current) {
4859
- authLogger.debug(`Profile loading already in progress, skipping duplicate call from: ${finalCallerId}`);
4860
- return;
4861
- }
4862
- authLogger.debug(`loadCurrentProfile called by: ${finalCallerId}`);
4863
- try {
4864
- isLoadingProfileRef.current = true;
4865
- const isAuth = CfgAccountsApi.isAuthenticated();
4866
- const token = CfgAccountsApi.getToken();
4867
- if (!isAuth) {
4868
- authLogger.warn("No valid authentication token, throwing error");
4869
- throw new Error("No valid authentication token");
4870
- }
4871
- if (accounts.profile && !accounts.isLoadingProfile) {
4872
- authLogger.debug("Profile already loaded in AccountsContext, skipping API call");
4873
- setInitialized(true);
4874
- return;
4875
- }
4876
- const refreshedProfile = await accounts.refreshProfile({ callerId: finalCallerId });
4877
- if (refreshedProfile) {
4878
- authLogger.info("Profile loaded successfully:", refreshedProfile.id);
4879
- } else {
4880
- authLogger.warn("Profile refresh returned undefined - but keeping tokens");
4881
- }
4882
- setInitialized(true);
4883
- } catch (error) {
4884
- authLogger.error("Failed to load profile:", error);
4885
- const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
4886
- if (isAuthError) {
4887
- authLogger.warn("Authentication error, clearing session");
4888
- clearAuthState("loadCurrentProfile:authError");
4889
- } else {
4890
- authLogger.warn("Profile load failed but keeping session (non-auth error)");
4891
- setInitialized(true);
4892
- }
4893
- } finally {
4894
- isLoadingProfileRef.current = false;
4445
+ }, [isAuthenticated, hasProfile]);
4446
+ const isLoading = !profileSettled;
4447
+ const expiredHandledRef = useRef6(false);
4448
+ useEffect7(() => {
4449
+ const unsubscribe = auth.onSessionExpired(() => {
4450
+ if (expiredHandledRef.current) return;
4451
+ expiredHandledRef.current = true;
4452
+ authLogger.warn("Session expired (terminal 401) \u2014 redirecting to login");
4453
+ clearProfileCache();
4454
+ Analytics.event("auth_session_expired" /* AUTH_SESSION_EXPIRED */, {
4455
+ category: "auth" /* AUTH */
4456
+ });
4457
+ router.hardReplace(routesRef.current.defaultAuthCallback);
4458
+ });
4459
+ return unsubscribe;
4460
+ }, [router]);
4461
+ useEffect7(() => {
4462
+ if (!isAuthenticated || pendingManualNavRef.current) return;
4463
+ if (pathname === routes.auth && !queryParams.get("flow")) {
4464
+ const saved = redirectManager.hasRedirect() ? redirectManager.getRedirect() : null;
4465
+ if (saved) redirectManager.clearRedirect();
4466
+ router.push(saved || routes.defaultCallback);
4895
4467
  }
4896
- }, [clearAuthState, handleGlobalAuthError, accounts]);
4897
- useEffect9(() => {
4898
- if (initialized) return;
4899
- const initializeAuth = /* @__PURE__ */ __name(async () => {
4900
- authLogger.info("Initializing auth...");
4901
- const isInIframe = typeof window !== "undefined" && window.self !== window.top;
4902
- authLogger.info("Is in iframe:", isInIframe);
4903
- const token = CfgAccountsApi.getToken();
4904
- const refreshToken2 = CfgAccountsApi.getRefreshToken();
4905
- authLogger.info("Token from API:", token ? `${token.substring(0, 20)}...` : "null");
4906
- authLogger.info("Refresh token from API:", refreshToken2 ? `${refreshToken2.substring(0, 20)}...` : "null");
4907
- authLogger.info("localStorage keys:", Object.keys(localStorage).filter((k) => k.includes("token") || k.includes("auth")));
4908
- if (!isInIframe && isSessionDeadOnBootstrap(token, refreshToken2)) {
4909
- authLogger.warn("Bootstrap: dead session in storage (access + refresh both unusable) \u2014 clearing and showing login");
4910
- clearAuthState("initializeAuth:deadSession");
4911
- return;
4912
- }
4913
- const hasTokens = hasValidTokens();
4914
- authLogger.info("Has tokens:", hasTokens);
4915
- if (userRef.current) {
4916
- authLogger.info("Profile already loaded from AccountsContext cache, skipping API request");
4917
- setInitialized(true);
4918
- setIsLoading(false);
4919
- return;
4920
- }
4921
- const cachedProfile = getCachedProfile();
4922
- if (cachedProfile) {
4923
- authLogger.info("Profile found in localStorage cache, skipping API request");
4924
- setInitialized(true);
4925
- setIsLoading(false);
4926
- return;
4927
- }
4928
- if (isInIframe && !hasTokens) {
4929
- authLogger.info("Running in iframe without tokens - waiting for parent to send via postMessage");
4930
- authLogger.info("AdminLayout will handle auth initialization, skipping AuthContext init");
4931
- setInitialized(true);
4932
- setIsLoading(false);
4933
- return;
4934
- }
4935
- if (hasTokens) {
4936
- setIsLoading(true);
4937
- try {
4938
- authLogger.info("No cached profile found, loading from API...");
4939
- await loadCurrentProfile("AuthContext.initializeAuth");
4940
- } catch (error) {
4941
- authLogger.error("Failed to load profile during initialization:", error);
4942
- const isAuthError = error?.status === 401 || error?.statusCode === 401 || error?.code === "token_not_valid" || error?.code === "authentication_failed";
4943
- if (isAuthError) {
4944
- clearAuthState("initializeAuth:authError");
4945
- } else {
4946
- authLogger.warn("Init profile load failed but keeping session");
4947
- setInitialized(true);
4948
- }
4949
- }
4950
- setIsLoading(false);
4951
- } else {
4952
- setInitialized(true);
4953
- setIsLoading(false);
4954
- }
4955
- }, "initializeAuth");
4956
- initializeAuth();
4957
- }, [initialized]);
4958
- useEffect9(() => {
4959
- if (!initialized) return;
4960
- const isAuthenticated = CfgAccountsApi.isAuthenticated();
4961
- const authRoute = config?.routes?.auth || defaultRoutes.auth;
4962
- const isAuthPage = pathname === authRoute;
4963
- const flowParam = queryParams.get("flow");
4964
- if (isAuthenticated && isAuthPage && !flowParam) {
4965
- const callbackUrl = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
4966
- router.push(callbackUrl);
4468
+ }, [isAuthenticated, pathname, queryParams, routes, router, redirectManager]);
4469
+ const loadCurrentProfile = useCallback11(async (callerId) => {
4470
+ if (!auth.isAuthenticated()) {
4471
+ throw new Error("No valid authentication token");
4967
4472
  }
4968
- }, [initialized, pathname, queryParams, config?.routes]);
4969
- const pushToDefaultCallbackUrl = useCallback12(() => {
4970
- const callbackUrl = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
4971
- router.push(callbackUrl);
4972
- }, [config?.routes, router]);
4973
- const pushToDefaultAuthCallbackUrl = useCallback12(() => {
4974
- const authCallbackUrl = config?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
4975
- router.push(authCallbackUrl);
4976
- }, [config?.routes, router]);
4977
- const checkAuthAndRedirect = useCallback12(async () => {
4978
- try {
4979
- setIsLoading(true);
4980
- const isAuthenticated = CfgAccountsApi.isAuthenticated();
4981
- if (isAuthenticated) {
4982
- await loadCurrentProfile();
4983
- if (userRef.current) {
4984
- pushToDefaultCallbackUrl();
4985
- }
4986
- } else {
4987
- pushToDefaultAuthCallbackUrl();
4988
- }
4989
- } catch (error) {
4990
- authLogger.error("Failed to check authentication:", error);
4991
- if (!handleGlobalAuthError(error, "checkAuthAndRedirect")) {
4992
- clearAuthState("checkAuthAndRedirect");
4473
+ await accountsRef.current.refreshProfile({
4474
+ callerId: callerId || "AuthContext.loadCurrentProfile"
4475
+ });
4476
+ }, []);
4477
+ const checkAuthAndRedirect = useCallback11(async () => {
4478
+ if (auth.isAuthenticated()) {
4479
+ try {
4480
+ await loadCurrentProfile("checkAuthAndRedirect");
4481
+ } catch (error) {
4482
+ authLogger.warn("checkAuthAndRedirect: profile load failed", error);
4993
4483
  }
4994
- pushToDefaultAuthCallbackUrl();
4995
- } finally {
4996
- setIsLoading(false);
4484
+ router.push(routesRef.current.defaultCallback);
4485
+ } else {
4486
+ router.push(routesRef.current.defaultAuthCallback);
4997
4487
  }
4998
- }, [loadCurrentProfile, clearAuthState, pushToDefaultCallbackUrl, pushToDefaultAuthCallbackUrl, handleGlobalAuthError]);
4999
- const requestOTP = useCallback12(
4488
+ }, [loadCurrentProfile, router]);
4489
+ const requestOTP = useCallback11(
5000
4490
  async (identifier, sourceUrl) => {
5001
- CfgAccountsApi.clearToken();
4491
+ auth.clearSession();
5002
4492
  try {
5003
- const result = await accounts.requestOTP({
4493
+ const result = await accountsRef.current.requestOTP({
5004
4494
  identifier,
5005
4495
  source_url: sourceUrl
5006
4496
  });
@@ -5010,33 +4500,31 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5010
4500
  });
5011
4501
  return {
5012
4502
  success: true,
5013
- message: result.message || `OTP code sent to your email address`,
4503
+ message: result.message || "OTP code sent to your email address",
5014
4504
  webmail: result.webmail ?? null
5015
4505
  };
5016
4506
  } catch (error) {
5017
4507
  authLogger.error("Request OTP error:", error);
5018
4508
  if (error instanceof APIError) {
5019
- const retryAfter = error.response?.retry_after ?? error.response?.retryAfter;
5020
- const message = error.response?.error || error.response?.detail || error.response?.message || error.errorMessage;
4509
+ const body = error.response;
4510
+ const retryAfter = body?.retry_after ?? body?.retryAfter;
5021
4511
  return {
5022
4512
  success: false,
5023
4513
  statusCode: error.statusCode,
5024
- message,
4514
+ message: apiErrorMessage(error),
5025
4515
  retryAfter: typeof retryAfter === "number" ? retryAfter : void 0
5026
4516
  };
5027
4517
  }
5028
- return {
5029
- success: false,
5030
- message: "Failed to send OTP"
5031
- };
4518
+ return { success: false, message: "Failed to send OTP" };
5032
4519
  }
5033
4520
  },
5034
- [accounts]
4521
+ []
5035
4522
  );
5036
- const verifyOTP = useCallback12(
4523
+ const verifyOTP = useCallback11(
5037
4524
  async (identifier, otpCode, sourceUrl, redirectUrl, skipRedirect) => {
5038
4525
  try {
5039
- const result = await accounts.verifyOTP({
4526
+ pendingManualNavRef.current = Boolean(skipRedirect);
4527
+ const result = await accountsRef.current.verifyOTP({
5040
4528
  identifier,
5041
4529
  otp: otpCode,
5042
4530
  source_url: sourceUrl
@@ -5053,15 +4541,11 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5053
4541
  }
5054
4542
  if (!result.access || !result.refresh) {
5055
4543
  authLogger.error("Verify OTP returned invalid response:", result);
5056
- return {
5057
- success: false,
5058
- message: "Invalid OTP verification response"
5059
- };
4544
+ return { success: false, message: "Invalid OTP verification response" };
5060
4545
  }
5061
4546
  if (identifier.includes("@")) {
5062
4547
  setStoredEmail(identifier);
5063
4548
  }
5064
- await new Promise((resolve) => setTimeout(resolve, 200));
5065
4549
  Analytics.event("auth_login_success" /* AUTH_LOGIN_SUCCESS */, {
5066
4550
  category: "auth" /* AUTH */,
5067
4551
  label: "email"
@@ -5070,8 +4554,9 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5070
4554
  Analytics.setUser(String(result.user.id));
5071
4555
  }
5072
4556
  if (!skipRedirect) {
5073
- const savedRedirect = redirectManager.useAndClearRedirect();
5074
- const finalRedirectUrl = redirectUrl || savedRedirect || config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
4557
+ const saved = redirectManager.hasRedirect() ? redirectManager.getRedirect() : null;
4558
+ if (saved) redirectManager.clearRedirect();
4559
+ const finalRedirectUrl = saved || redirectUrl || routesRef.current.defaultCallback;
5075
4560
  authLogger.info("Redirecting after auth to:", finalRedirectUrl);
5076
4561
  router.hardPush(finalRedirectUrl);
5077
4562
  }
@@ -5088,96 +4573,62 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5088
4573
  label: "email"
5089
4574
  });
5090
4575
  if (error instanceof APIError) {
5091
- const message = error.response?.error || error.response?.detail || error.response?.message || error.errorMessage;
5092
- return { success: false, message };
4576
+ return { success: false, message: apiErrorMessage(error) };
5093
4577
  }
5094
- return {
5095
- success: false,
5096
- message: "Failed to verify OTP"
5097
- };
4578
+ return { success: false, message: "Failed to verify OTP" };
5098
4579
  }
5099
4580
  },
5100
- [setStoredEmail, config?.routes?.defaultCallback, accounts, router]
4581
+ [setStoredEmail, redirectManager, router]
5101
4582
  );
5102
- const refreshToken = useCallback12(async () => {
5103
- try {
5104
- const refreshTokenValue = CfgAccountsApi.getRefreshToken();
5105
- if (!refreshTokenValue) {
5106
- clearAuthState("refreshToken:noToken");
5107
- Analytics.event("auth_session_expired" /* AUTH_SESSION_EXPIRED */, {
5108
- category: "auth" /* AUTH */
5109
- });
5110
- return {
5111
- success: false,
5112
- message: "No refresh token available"
5113
- };
5114
- }
5115
- await accounts.refreshToken(refreshTokenValue);
4583
+ const refreshToken = useCallback11(async () => {
4584
+ const access = await auth.refreshNow();
4585
+ if (access) {
5116
4586
  Analytics.event("auth_token_refresh" /* AUTH_TOKEN_REFRESH */, {
5117
4587
  category: "auth" /* AUTH */
5118
4588
  });
5119
- return {
5120
- success: true,
5121
- message: "Token refreshed"
5122
- };
5123
- } catch (error) {
5124
- authLogger.error("Refresh token error:", error);
5125
- clearAuthState("refreshToken:error");
5126
- Analytics.event("auth_token_refresh_fail" /* AUTH_TOKEN_REFRESH_FAIL */, {
5127
- category: "auth" /* AUTH */
5128
- });
5129
- return {
5130
- success: false,
5131
- message: "Error refreshing token"
5132
- };
4589
+ return { success: true, message: "Token refreshed" };
5133
4590
  }
5134
- }, [clearAuthState, accounts]);
5135
- const logout = useCallback12(() => {
4591
+ Analytics.event("auth_token_refresh_fail" /* AUTH_TOKEN_REFRESH_FAIL */, {
4592
+ category: "auth" /* AUTH */
4593
+ });
4594
+ return { success: false, message: "Error refreshing token" };
4595
+ }, []);
4596
+ const logout = useCallback11(() => {
5136
4597
  Analytics.event("auth_logout" /* AUTH_LOGOUT */, {
5137
4598
  category: "auth" /* AUTH */
5138
4599
  });
5139
- accounts.logout();
5140
- setInitialized(true);
5141
- setIsLoading(false);
5142
- const authCallbackUrl = config?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
5143
- router.hardReplace(authCallbackUrl);
5144
- }, [accounts, config?.routes?.defaultAuthCallback, router]);
5145
- const isAdminUser = useMemo3(() => {
5146
- return Boolean(user?.is_staff || user?.is_superuser);
5147
- }, [user]);
5148
- useEffect9(() => {
4600
+ accountsRef.current.logout();
4601
+ router.hardReplace(routesRef.current.defaultAuthCallback);
4602
+ }, [router]);
4603
+ const user = accounts.profile ?? null;
4604
+ const isAdminUser = Boolean(user?.is_staff || user?.is_superuser);
4605
+ useEffect7(() => {
5149
4606
  applyRoleLogPolicy({ isAdmin: isAdminUser });
5150
4607
  }, [isAdminUser]);
5151
- const updateProfile = useCallback12(
4608
+ const updateProfile = useCallback11(
5152
4609
  async (data) => {
5153
- const result = await accounts.partialUpdateProfile(data);
5154
- return result;
5155
- },
5156
- [accounts]
5157
- );
5158
- const uploadAvatar = useCallback12(
5159
- async (avatar) => {
5160
- const result = await accounts.uploadAvatar(avatar);
4610
+ const result = await accountsRef.current.partialUpdateProfile(data);
5161
4611
  return result;
5162
4612
  },
5163
- [accounts]
4613
+ []
5164
4614
  );
4615
+ const uploadAvatar = useCallback11(async (avatar) => {
4616
+ const result = await accountsRef.current.uploadAvatar(avatar);
4617
+ return result;
4618
+ }, []);
5165
4619
  const value = useMemo3(
5166
4620
  () => ({
5167
4621
  user,
5168
4622
  isLoading,
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(),
4623
+ // Reactive: flips via the store snapshot (token writes, refreshes,
4624
+ // expiry timer, cross-tab/cross-store sync) — no ticks, no polling.
4625
+ isAuthenticated,
5176
4626
  isAdminUser,
4627
+ routes,
5177
4628
  loadCurrentProfile,
5178
4629
  checkAuthAndRedirect,
5179
- getToken: /* @__PURE__ */ __name(() => CfgAccountsApi.getToken(), "getToken"),
5180
- getRefreshToken: /* @__PURE__ */ __name(() => CfgAccountsApi.getRefreshToken(), "getRefreshToken"),
4630
+ getToken: /* @__PURE__ */ __name(() => auth.getToken(), "getToken"),
4631
+ getRefreshToken: /* @__PURE__ */ __name(() => auth.getRefreshToken(), "getRefreshToken"),
5181
4632
  getSavedEmail: /* @__PURE__ */ __name(() => storedEmail, "getSavedEmail"),
5182
4633
  saveEmail: setStoredEmail,
5183
4634
  clearSavedEmail: clearStoredEmail,
@@ -5185,21 +4636,19 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5185
4636
  verifyOTP,
5186
4637
  refreshToken,
5187
4638
  logout,
5188
- // Redirect URL methods
5189
4639
  saveRedirectUrl: redirectManager.setRedirect,
5190
4640
  getRedirectUrl: redirectManager.getFinalRedirectUrl,
5191
4641
  clearRedirectUrl: redirectManager.clearRedirect,
5192
4642
  hasRedirectUrl: redirectManager.hasRedirect,
5193
- // Profile management
5194
4643
  updateProfile,
5195
4644
  uploadAvatar
5196
4645
  }),
5197
4646
  [
5198
4647
  user,
5199
4648
  isLoading,
5200
- authTick,
5201
- // recompute isAuthenticated when tokens are cleared/set
4649
+ isAuthenticated,
5202
4650
  isAdminUser,
4651
+ routes,
5203
4652
  loadCurrentProfile,
5204
4653
  checkAuthAndRedirect,
5205
4654
  storedEmail,
@@ -5214,7 +4663,7 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
5214
4663
  uploadAvatar
5215
4664
  ]
5216
4665
  );
5217
- return /* @__PURE__ */ jsx2(AuthContext.Provider, { value, children: /* @__PURE__ */ jsx2(SWRConfig, { value: swrConfig, children }) });
4666
+ return /* @__PURE__ */ jsx2(AuthContext.Provider, { value, children });
5218
4667
  }, "AuthProviderInternal");
5219
4668
  function AuthProviderRaw({ children, config, enabled = true }) {
5220
4669
  if (!enabled) return /* @__PURE__ */ jsx2(Fragment, { children });
@@ -5227,6 +4676,7 @@ var defaultAuthState = {
5227
4676
  isLoading: false,
5228
4677
  isAuthenticated: false,
5229
4678
  isAdminUser: false,
4679
+ routes: resolveAuthRoutes(),
5230
4680
  loadCurrentProfile: /* @__PURE__ */ __name(async () => {
5231
4681
  authLogger.warn("useAuth: loadCurrentProfile called outside AuthProvider");
5232
4682
  }, "loadCurrentProfile"),
@@ -5253,12 +4703,12 @@ var defaultAuthState = {
5253
4703
  authLogger.warn("useAuth: refreshToken called outside AuthProvider");
5254
4704
  return { success: false, message: "AuthProvider not available" };
5255
4705
  }, "refreshToken"),
5256
- logout: /* @__PURE__ */ __name(async (_options) => {
4706
+ logout: /* @__PURE__ */ __name(() => {
5257
4707
  authLogger.warn("useAuth: logout called outside AuthProvider");
5258
4708
  }, "logout"),
5259
4709
  saveRedirectUrl: /* @__PURE__ */ __name(() => {
5260
4710
  }, "saveRedirectUrl"),
5261
- getRedirectUrl: /* @__PURE__ */ __name(() => null, "getRedirectUrl"),
4711
+ getRedirectUrl: /* @__PURE__ */ __name(() => "", "getRedirectUrl"),
5262
4712
  clearRedirectUrl: /* @__PURE__ */ __name(() => {
5263
4713
  }, "clearRedirectUrl"),
5264
4714
  hasRedirectUrl: /* @__PURE__ */ __name(() => false, "hasRedirectUrl"),
@@ -5274,7 +4724,7 @@ var defaultAuthState = {
5274
4724
  var useAuth = /* @__PURE__ */ __name(() => {
5275
4725
  const context = useContext2(AuthContext);
5276
4726
  if (context === void 0) {
5277
- if (isBrowser && isDev) {
4727
+ if (isBrowser2 && isDev) {
5278
4728
  authLogger.debug("useAuth called outside AuthProvider, returning default state");
5279
4729
  }
5280
4730
  return defaultAuthState;
@@ -5305,6 +4755,22 @@ var formatAuthError = /* @__PURE__ */ __name((error) => {
5305
4755
  }
5306
4756
  return "An unexpected error occurred";
5307
4757
  }, "formatAuthError");
4758
+
4759
+ // src/auth/utils/guard.ts
4760
+ function resolveGuardIsLoading(s) {
4761
+ if (!s.mounted) return true;
4762
+ if (!s.requireAuth) return false;
4763
+ return s.authLoading || s.isRedirecting || !s.isAuthenticated;
4764
+ }
4765
+ __name(resolveGuardIsLoading, "resolveGuardIsLoading");
4766
+ function shouldRedirectToAuth(s) {
4767
+ return s.mounted && s.requireAuth && !s.authLoading && !s.isAuthenticated && !s.isRedirecting;
4768
+ }
4769
+ __name(shouldRedirectToAuth, "shouldRedirectToAuth");
4770
+ function resolveGuardIsAuthenticated(s) {
4771
+ return !s.requireAuth || s.isAuthenticated;
4772
+ }
4773
+ __name(resolveGuardIsAuthenticated, "resolveGuardIsAuthenticated");
5308
4774
  export {
5309
4775
  AUTH_CONSTANTS,
5310
4776
  AccountsProvider,
@@ -5316,19 +4782,17 @@ export {
5316
4782
  PatchedCfgUserUpdateRequestSchema,
5317
4783
  authLogger,
5318
4784
  clearProfileCache,
4785
+ consumeSavedRedirect,
5319
4786
  decodeBase64,
5320
4787
  encodeBase64,
5321
4788
  formatAuthError,
5322
4789
  getCacheMetadata,
5323
4790
  getCachedProfile,
5324
- getTokenExpiry,
5325
4791
  hasValidCache,
5326
4792
  isAllowedAuthPath,
5327
- isTokenExpired,
5328
- isTokenExpiringSoon,
5329
4793
  logger,
5330
- nextAuthEvaluationDelay,
5331
4794
  normalizePath,
4795
+ peekSavedRedirect,
5332
4796
  resolveGuardIsAuthenticated,
5333
4797
  resolveGuardIsLoading,
5334
4798
  setCachedProfile,
@@ -5337,7 +4801,6 @@ export {
5337
4801
  useAuth,
5338
4802
  useAuthForm,
5339
4803
  useAuthFormState,
5340
- useAuthGuard,
5341
4804
  useAuthRedirectManager,
5342
4805
  useAuthValidation,
5343
4806
  useAutoAuth,
@@ -5347,8 +4810,8 @@ export {
5347
4810
  useGithubAuth,
5348
4811
  useLocalStorage,
5349
4812
  useQueryParams,
4813
+ useSession,
5350
4814
  useSessionStorage,
5351
- useTokenRefresh,
5352
4815
  useTwoFactor,
5353
4816
  useTwoFactorSetup,
5354
4817
  useTwoFactorStatus,