@djangocfg/api 2.1.455 → 2.1.456

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 (87) hide show
  1. package/dist/auth.cjs +2654 -2920
  2. package/dist/auth.cjs.map +1 -1
  3. package/dist/auth.d.cts +18 -82
  4. package/dist/auth.d.ts +18 -82
  5. package/dist/auth.mjs +385 -2861
  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 +2210 -1847
  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 +14 -1845
  28. package/dist/clients.mjs.map +1 -1
  29. package/dist/hooks.cjs +1776 -962
  30. package/dist/hooks.cjs.map +1 -1
  31. package/dist/hooks.mjs +4 -1375
  32. package/dist/hooks.mjs.map +1 -1
  33. package/dist/index.cjs +2245 -1877
  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 +20 -1860
  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 +276 -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 +226 -609
  71. package/src/auth/hooks/index.ts +3 -4
  72. package/src/auth/hooks/useGithubAuth.ts +3 -3
  73. package/src/auth/hooks/useSession.ts +22 -0
  74. package/src/auth/hooks/useTwoFactor.ts +4 -4
  75. package/src/auth/middlewares/index.ts +5 -6
  76. package/src/auth/utils/guard.ts +0 -22
  77. package/src/auth/utils/index.ts +0 -2
  78. package/src/_api/generated/_cfg_accounts/events.ts +0 -198
  79. package/src/_api/generated/_cfg_centrifugo/events.ts +0 -198
  80. package/src/_api/generated/_cfg_totp/events.ts +0 -198
  81. package/src/auth/__tests__/jwt.test.ts +0 -119
  82. package/src/auth/__tests__/sessionBootstrap.test.ts +0 -111
  83. package/src/auth/__tests__/useTokenRefresh.dom.test.tsx +0 -167
  84. package/src/auth/hooks/useAuthGuard.ts +0 -35
  85. package/src/auth/hooks/useTokenRefresh.ts +0 -131
  86. package/src/auth/refreshHandler.ts +0 -79
  87. package/src/auth/utils/jwt.ts +0 -66
@@ -149,6 +149,123 @@ let _refreshInflight: Promise<string | null> | null = null;
149
149
  /** Marker header — set on retried requests so we never loop on 401. */
150
150
  const RETRY_MARKER = 'X-Auth-Retry';
151
151
 
152
+ // ── Reactive session snapshot ───────────────────────────────────────────────
153
+ //
154
+ // The store is the single source of truth for "am I logged in". React (or any
155
+ // listener) subscribes via `auth.subscribe` + `auth.getSnapshot` — designed to
156
+ // plug straight into `useSyncExternalStore`. The snapshot flips automatically
157
+ // on: token writes, refresh success/failure, cross-tab storage events, and an
158
+ // internal timer armed to the next JWT `exp` boundary.
159
+
160
+ export type SessionStatus = 'authenticated' | 'anonymous';
161
+
162
+ export type SessionSnapshot = {
163
+ /** 'authenticated' = a live access token OR a live refresh token exists. */
164
+ status: SessionStatus;
165
+ /** ms-epoch expiry of the current access token (null: no token / no exp). */
166
+ accessExpiresAt: number | null;
167
+ };
168
+
169
+ /** Decode a JWT `exp` claim to ms epoch. Null on any malformed input. */
170
+ function jwtExpMs(token: string): number | null {
171
+ try {
172
+ const payload = token.split('.')[1];
173
+ if (!payload) return null;
174
+ const json = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
175
+ return typeof json.exp === 'number' ? json.exp * 1000 : null;
176
+ } catch { return null; }
177
+ }
178
+
179
+ function computeSnapshot(): SessionSnapshot {
180
+ const access = _storage.get(ACCESS_KEY);
181
+ const refresh = _storage.get(REFRESH_KEY);
182
+ const now = Date.now();
183
+ const accessExp = access ? jwtExpMs(access) : null;
184
+ const accessAlive = access !== null && (accessExp === null || accessExp > now);
185
+ const refreshExp = refresh ? jwtExpMs(refresh) : null;
186
+ const refreshAlive = refresh !== null && (refreshExp === null || refreshExp > now);
187
+ return {
188
+ status: accessAlive || refreshAlive ? 'authenticated' : 'anonymous',
189
+ accessExpiresAt: accessExp,
190
+ };
191
+ }
192
+
193
+ /** Stable object for SSR — `useSyncExternalStore` needs referential equality. */
194
+ const SERVER_SNAPSHOT: SessionSnapshot = { status: 'anonymous', accessExpiresAt: null };
195
+
196
+ /** Same-tab, cross-store sync channel. An app can host several copies of this
197
+ * generated store (separate JS modules) sharing the same storage keys; the
198
+ * `storage` event only fires in OTHER tabs, so a window event bridges them
199
+ * synchronously within the tab (login in one store flips all of them). */
200
+ const SESSION_SYNC_EVENT = `cfg-auth:changed:${ACCESS_KEY}`;
201
+
202
+ let _snapshot: SessionSnapshot = computeSnapshot();
203
+ const _sessionListeners = new Set<() => void>();
204
+ let _expiryTimer: ReturnType<typeof setTimeout> | null = null;
205
+ let _storageListenerInstalled = false;
206
+
207
+ /** Re-arm the timer that flips the snapshot exactly when a token expires. */
208
+ function scheduleExpiryFlip(): void {
209
+ if (!isBrowser) return;
210
+ if (_expiryTimer !== null) { clearTimeout(_expiryTimer); _expiryTimer = null; }
211
+ if (_sessionListeners.size === 0) return;
212
+ const now = Date.now();
213
+ const exps: number[] = [];
214
+ const access = _storage.get(ACCESS_KEY);
215
+ const refresh = _storage.get(REFRESH_KEY);
216
+ const accessExp = access ? jwtExpMs(access) : null;
217
+ const refreshExp = refresh ? jwtExpMs(refresh) : null;
218
+ if (accessExp !== null && accessExp > now) exps.push(accessExp);
219
+ if (refreshExp !== null && refreshExp > now) exps.push(refreshExp);
220
+ if (!exps.length) return;
221
+ // +250ms grace so the token is REALLY expired when we flip; clamp to the
222
+ // max signed-32-bit setTimeout delay (~24.8 days).
223
+ const delay = Math.min(Math.min(...exps) - now + 250, 0x7fffffff);
224
+ _expiryTimer = setTimeout(notifySessionChanged, delay);
225
+ }
226
+
227
+ /** Recompute the snapshot; notify subscribers only when it actually changed. */
228
+ function notifySessionChanged(): void {
229
+ const next = computeSnapshot();
230
+ const changed =
231
+ next.status !== _snapshot.status ||
232
+ next.accessExpiresAt !== _snapshot.accessExpiresAt;
233
+ if (changed) _snapshot = next;
234
+ scheduleExpiryFlip();
235
+ if (!changed) return;
236
+ for (const listener of Array.from(_sessionListeners)) {
237
+ try { listener(); } catch {}
238
+ }
239
+ // Wake sibling stores in this tab. Re-entrant but convergent: a store that
240
+ // recomputes to the SAME snapshot doesn't re-dispatch, so the cascade stops
241
+ // after every store has caught up.
242
+ if (isBrowser) {
243
+ try { window.dispatchEvent(new Event(SESSION_SYNC_EVENT)); } catch {}
244
+ }
245
+ }
246
+
247
+ /** Cross-tab + cross-store sync — `storage` events only fire in OTHER tabs;
248
+ * the window event covers sibling stores in THIS tab. Either way a login or
249
+ * logout anywhere flips every snapshot reading the same keys. */
250
+ function ensureStorageSync(): void {
251
+ if (!isBrowser || _storageListenerInstalled) return;
252
+ _storageListenerInstalled = true;
253
+ window.addEventListener('storage', (e) => {
254
+ if (e.key === null || e.key === ACCESS_KEY || e.key === REFRESH_KEY) {
255
+ notifySessionChanged();
256
+ }
257
+ });
258
+ window.addEventListener(SESSION_SYNC_EVENT, () => notifySessionChanged());
259
+ }
260
+
261
+ /**
262
+ * Terminal-401 subscribers. Unlike the single `onUnauthorized` slot these
263
+ * compose: each registration returns its own unsubscribe, so StrictMode
264
+ * double-mounts and multiple packages can't clobber each other.
265
+ */
266
+ type SessionExpiredHandler = (response: Response) => void;
267
+ const _sessionExpiredHandlers = new Set<SessionExpiredHandler>();
268
+
152
269
  /**
153
270
  * Captured reference to the shared Hey API client. Set exactly once by
154
271
  * `installAuthOnClient(client)` (called from client.gen.ts). All `auth.set*`
@@ -194,15 +311,79 @@ export const auth = {
194
311
  setStorageMode(mode: StorageMode): void {
195
312
  _storageMode = mode;
196
313
  _storage = mode === 'cookie' ? cookieBackend : localStorageBackend;
314
+ notifySessionChanged();
197
315
  },
198
316
 
199
317
  // ── Bearer token ──────────────────────────────────────────────────
200
318
  getToken(): string | null { return _storage.get(ACCESS_KEY); },
201
- setToken(token: string | null): void { _storage.set(ACCESS_KEY, token); },
319
+ setToken(token: string | null): void {
320
+ _storage.set(ACCESS_KEY, token);
321
+ notifySessionChanged();
322
+ },
202
323
  getRefreshToken(): string | null { return _storage.get(REFRESH_KEY); },
203
- setRefreshToken(token: string | null): void { _storage.set(REFRESH_KEY, token); },
204
- clearTokens(): void { _storage.set(ACCESS_KEY, null); _storage.set(REFRESH_KEY, null); },
205
- isAuthenticated(): boolean { return _storage.get(ACCESS_KEY) !== null; },
324
+ setRefreshToken(token: string | null): void {
325
+ _storage.set(REFRESH_KEY, token);
326
+ notifySessionChanged();
327
+ },
328
+ clearTokens(): void {
329
+ _storage.set(ACCESS_KEY, null);
330
+ _storage.set(REFRESH_KEY, null);
331
+ notifySessionChanged();
332
+ },
333
+ /** Session-aware: token PRESENT and not past its `exp` (or a live refresh
334
+ * token exists that can mint one). Prefer `getSnapshot().status` in React. */
335
+ isAuthenticated(): boolean { return computeSnapshot().status === 'authenticated'; },
336
+
337
+ // ── Session (the ONE write path for login/logout flows) ──────────
338
+ /**
339
+ * Persist a token pair atomically. Every login flow (OTP verify, 2FA,
340
+ * OAuth callback) and the refresh handler should end here — do NOT
341
+ * scatter `setToken`/`setRefreshToken` pairs through app code.
342
+ * `refresh: undefined` keeps the current refresh token (access-only
343
+ * rotation); `refresh: null` explicitly drops it.
344
+ */
345
+ setSession(tokens: { access: string; refresh?: string | null }): void {
346
+ _storage.set(ACCESS_KEY, tokens.access);
347
+ if (tokens.refresh !== undefined) _storage.set(REFRESH_KEY, tokens.refresh);
348
+ notifySessionChanged();
349
+ },
350
+ clearSession(): void { auth.clearTokens(); },
351
+
352
+ // ── Reactive snapshot (for useSyncExternalStore) ──────────────────
353
+ /**
354
+ * @example React:
355
+ * const session = useSyncExternalStore(
356
+ * auth.subscribe, auth.getSnapshot, auth.getServerSnapshot,
357
+ * );
358
+ * const isAuthenticated = session.status === 'authenticated';
359
+ */
360
+ getSnapshot(): SessionSnapshot { return _snapshot; },
361
+ getServerSnapshot(): SessionSnapshot { return SERVER_SNAPSHOT; },
362
+ subscribe(listener: () => void): () => void {
363
+ ensureStorageSync();
364
+ _sessionListeners.add(listener);
365
+ // Sync state & arm the expiry timer now that someone is listening.
366
+ notifySessionChanged();
367
+ return () => {
368
+ _sessionListeners.delete(listener);
369
+ if (_sessionListeners.size === 0 && _expiryTimer !== null) {
370
+ clearTimeout(_expiryTimer);
371
+ _expiryTimer = null;
372
+ }
373
+ };
374
+ },
375
+
376
+ /**
377
+ * Fired on TERMINAL 401 — after the refresh+retry path is exhausted.
378
+ * The store has already cleared the session before calling back, so
379
+ * handlers only need to route to login (no clear-then-redirect
380
+ * ordering to get wrong). Returns an unsubscribe function; multiple
381
+ * handlers compose (unlike the legacy single-slot `onUnauthorized`).
382
+ */
383
+ onSessionExpired(cb: SessionExpiredHandler): () => void {
384
+ _sessionExpiredHandlers.add(cb);
385
+ return () => { _sessionExpiredHandlers.delete(cb); };
386
+ },
206
387
 
207
388
  // ── API key ───────────────────────────────────────────────────────
208
389
  getApiKey(): string | null {
@@ -278,23 +459,53 @@ export const auth = {
278
459
  };
279
460
 
280
461
  /**
281
- * Run the user-supplied refresh handler under single-flight, persist
282
- * the new tokens, and return the fresh access token (or null on any
283
- * failure path). All concurrent 401s share the same in-flight promise.
462
+ * Run the user-supplied refresh handler, persist the rotated tokens, and
463
+ * return the fresh access token (or null on any failure path).
464
+ *
465
+ * TWO layers of coordination, because a single app can host MULTIPLE copies of
466
+ * this generated store (e.g. `@djangocfg/api` + a locally-generated client +
467
+ * `@djangocfg/monitor`), all sharing the SAME refresh token in storage:
468
+ *
469
+ * 1. Per-store single-flight (`_refreshInflight`) — concurrent 401s WITHIN
470
+ * this module coalesce onto one promise. Fast path, no lock.
471
+ * 2. Cross-store mutex (`navigator.locks`) keyed on the refresh-token storage
472
+ * key — serializes refreshes across EVERY store in the app. The refresh
473
+ * token is re-read INSIDE the lock, so if another store already rotated it
474
+ * (SimpleJWT `ROTATE_REFRESH_TOKENS` blacklists the old one), the loser
475
+ * picks up the fresh access token instead of re-POSTing a blacklisted
476
+ * refresh token and getting a spurious 401 → logout.
477
+ *
478
+ * Web Locks is browser-only; on SSR / older runtimes we degrade gracefully to
479
+ * layer 1 alone (correct whenever at most one store refreshes).
284
480
  */
285
481
  async function tryRefresh(): Promise<string | null> {
286
482
  if (_refreshInflight) return _refreshInflight;
287
483
  if (!_refreshHandler) return null;
288
- const refresh = auth.getRefreshToken();
289
- if (!refresh) return null;
484
+
485
+ // The critical section — re-reads the refresh token every time it runs, so a
486
+ // rotation performed by another store (or by us on a prior attempt) is always
487
+ // observed. Returns the fresh access token, or null on any failure.
488
+ const runRefresh = async (): Promise<string | null> => {
489
+ const refresh = auth.getRefreshToken();
490
+ if (!refresh) return null;
491
+ const result = await _refreshHandler!(refresh);
492
+ if (!result?.access) return null;
493
+ auth.setToken(result.access);
494
+ if (result.refresh) auth.setRefreshToken(result.refresh);
495
+ return result.access;
496
+ };
290
497
 
291
498
  _refreshInflight = (async () => {
292
499
  try {
293
- const result = await _refreshHandler!(refresh);
294
- if (!result?.access) return null;
295
- auth.setToken(result.access);
296
- if (result.refresh) auth.setRefreshToken(result.refresh);
297
- return result.access;
500
+ // Serialize across all stores in this app when Web Locks is available.
501
+ const locks =
502
+ typeof navigator !== 'undefined'
503
+ ? (navigator as Navigator & { locks?: LockManager }).locks
504
+ : undefined;
505
+ if (locks?.request) {
506
+ return await locks.request(`${REFRESH_KEY}:refresh`, runRefresh);
507
+ }
508
+ return await runRefresh();
298
509
  } catch {
299
510
  return null;
300
511
  } finally {
@@ -305,6 +516,23 @@ async function tryRefresh(): Promise<string | null> {
305
516
  return _refreshInflight;
306
517
  }
307
518
 
519
+ /**
520
+ * Terminal 401 — no refresh path can recover this session. The store clears
521
+ * its own tokens FIRST (subscribers flip to 'anonymous' via the snapshot),
522
+ * then notifies `onSessionExpired` subscribers and the legacy
523
+ * `onUnauthorized` slot. Apps only route to login; they never own the
524
+ * clear-before-redirect ordering.
525
+ */
526
+ function expireSession(response: Response): void {
527
+ auth.clearTokens();
528
+ for (const cb of Array.from(_sessionExpiredHandlers)) {
529
+ try { cb(response); } catch {}
530
+ }
531
+ if (_onUnauthorized) {
532
+ try { _onUnauthorized(response); } catch {}
533
+ }
534
+ }
535
+
308
536
  /**
309
537
  * Wire the shared client to the global auth store. Called exactly
310
538
  * once from `client.gen.ts` (post-processed) right after
@@ -476,7 +704,7 @@ export function installAuthOnClient(client: HeyClient): void {
476
704
  if (locale) request.headers.set('Accept-Language', locale);
477
705
 
478
706
  // Only set X-API-Key from global store if NOT already present on the
479
- // request — per-request headers (option 1 above) take precedence.
707
+ // request — per-request headers take precedence.
480
708
  const apiKey = auth.getApiKey();
481
709
  if (apiKey && !request.headers.has('X-API-Key')) request.headers.set('X-API-Key', apiKey);
482
710
 
@@ -496,9 +724,6 @@ export function installAuthOnClient(client: HeyClient): void {
496
724
  return request;
497
725
  });
498
726
 
499
- // Wrap raw JSON error objects (thrown by hey-api on non-2xx responses) into
500
- // APIError so callers can do `error instanceof APIError` and read
501
- // `error.statusCode` / `error.response` consistently.
502
727
  client.interceptors.error.use((err, res, req) => {
503
728
  if (err instanceof APIError) return err;
504
729
  const url = (req as Request | undefined)?.url ?? '';
@@ -507,22 +732,19 @@ export function installAuthOnClient(client: HeyClient): void {
507
732
  return new APIError(status, statusText, err, url);
508
733
  });
509
734
 
735
+
510
736
  client.interceptors.response.use(async (response, request) => {
511
737
  if (response.status !== 401) return response;
512
738
 
513
739
  // Already retried once — give up to avoid loops.
514
740
  if (request.headers.get(RETRY_MARKER)) {
515
- if (_onUnauthorized) {
516
- try { _onUnauthorized(response); } catch {}
517
- }
741
+ expireSession(response);
518
742
  return response;
519
743
  }
520
744
 
521
745
  const newToken = await tryRefresh();
522
746
  if (!newToken) {
523
- if (_onUnauthorized) {
524
- try { _onUnauthorized(response); } catch {}
525
- }
747
+ expireSession(response);
526
748
  return response;
527
749
  }
528
750
 
@@ -540,18 +762,41 @@ export function installAuthOnClient(client: HeyClient): void {
540
762
  }
541
763
  try {
542
764
  const retried = await fetch(retry);
543
- if (retried.status === 401 && _onUnauthorized) {
544
- try { _onUnauthorized(retried); } catch {}
545
- }
765
+ if (retried.status === 401) expireSession(retried);
546
766
  return retried;
547
767
  } catch {
548
- // Network error on retry surface the original 401.
549
- if (_onUnauthorized) {
550
- try { _onUnauthorized(response); } catch {}
551
- }
768
+ // Network error on the retry. The refresh SUCCEEDED (we hold a fresh
769
+ // token), so the session is alive — a transient network blip must NOT
770
+ // log the user out. Surface the original 401 to the caller only.
552
771
  return response;
553
772
  }
554
773
  });
555
774
  }
556
775
 
776
+ // ── Default refresh handler (auto-registered) ──────────────────────────────
777
+ //
778
+ // Wires the SimpleJWT refresh endpoint so a 401 transparently refreshes and
779
+ // retries. Registered at import time; an app can override it afterwards with
780
+ // `auth.setRefreshHandler(...)` (last write wins). Returns the ROTATED refresh
781
+ // token so the store persists it — `ROTATE_REFRESH_TOKENS` blacklists the old
782
+ // one, so reusing it would 401 on the next refresh.
783
+ //
784
+ // The SDK is imported LAZILY inside the handler: a static import would close
785
+ // the cycle auth.ts → sdk.gen → client.gen → auth.ts and TDZ-crash whichever
786
+ // module evaluates first. The dynamic import resolves once, then caches.
787
+ auth.setRefreshHandler(async (refresh) => {
788
+ try {
789
+ const { CfgAccountsAuth } = await import('../sdk.gen');
790
+ const { data } = await CfgAccountsAuth.cfgAccountsTokenRefreshCreate({
791
+ body: { refresh },
792
+ throwOnError: true,
793
+ });
794
+ return data?.access
795
+ ? { access: data.access, refresh: data.refresh ?? refresh }
796
+ : null;
797
+ } catch {
798
+ return null;
799
+ }
800
+ });
801
+
557
802
  export type Auth = typeof auth;
@@ -241,6 +241,9 @@
241
241
  },
242
242
  {
243
243
  "jwtAuthWithLastLogin": []
244
+ },
245
+ {
246
+ "tokenAuth": []
244
247
  }
245
248
  ],
246
249
  "responses": {
@@ -282,6 +285,9 @@
282
285
  },
283
286
  {
284
287
  "jwtAuthWithLastLogin": []
288
+ },
289
+ {
290
+ "tokenAuth": []
285
291
  }
286
292
  ],
287
293
  "responses": {
@@ -460,6 +466,9 @@
460
466
  {
461
467
  "jwtAuthWithLastLogin": []
462
468
  },
469
+ {
470
+ "tokenAuth": []
471
+ },
463
472
  {}
464
473
  ],
465
474
  "responses": {
@@ -531,6 +540,9 @@
531
540
  {
532
541
  "jwtAuthWithLastLogin": []
533
542
  },
543
+ {
544
+ "tokenAuth": []
545
+ },
534
546
  {}
535
547
  ],
536
548
  "responses": {
@@ -642,6 +654,9 @@
642
654
  },
643
655
  {
644
656
  "jwtAuthWithLastLogin": []
657
+ },
658
+ {
659
+ "tokenAuth": []
645
660
  }
646
661
  ],
647
662
  "responses": {
@@ -756,6 +771,16 @@
756
771
  "summary": "Profile Update with Avatar"
757
772
  }
758
773
  }
774
+ },
775
+ "multipart/form-data": {
776
+ "schema": {
777
+ "$ref": "#/components/schemas/CfgUserUpdateRequest"
778
+ }
779
+ },
780
+ "application/x-www-form-urlencoded": {
781
+ "schema": {
782
+ "$ref": "#/components/schemas/CfgUserUpdateRequest"
783
+ }
759
784
  }
760
785
  }
761
786
  },
@@ -826,6 +851,16 @@
826
851
  "summary": "Profile Update with Avatar"
827
852
  }
828
853
  }
854
+ },
855
+ "multipart/form-data": {
856
+ "schema": {
857
+ "$ref": "#/components/schemas/PatchedCfgUserUpdateRequest"
858
+ }
859
+ },
860
+ "application/x-www-form-urlencoded": {
861
+ "schema": {
862
+ "$ref": "#/components/schemas/PatchedCfgUserUpdateRequest"
863
+ }
829
864
  }
830
865
  }
831
866
  },
@@ -898,6 +933,16 @@
898
933
  "summary": "Valid Profile Update"
899
934
  }
900
935
  }
936
+ },
937
+ "multipart/form-data": {
938
+ "schema": {
939
+ "$ref": "#/components/schemas/CfgUserUpdateRequest"
940
+ }
941
+ },
942
+ "application/x-www-form-urlencoded": {
943
+ "schema": {
944
+ "$ref": "#/components/schemas/CfgUserUpdateRequest"
945
+ }
901
946
  }
902
947
  }
903
948
  },
@@ -968,6 +1013,16 @@
968
1013
  "summary": "Valid Profile Update"
969
1014
  }
970
1015
  }
1016
+ },
1017
+ "multipart/form-data": {
1018
+ "schema": {
1019
+ "$ref": "#/components/schemas/PatchedCfgUserUpdateRequest"
1020
+ }
1021
+ },
1022
+ "application/x-www-form-urlencoded": {
1023
+ "schema": {
1024
+ "$ref": "#/components/schemas/PatchedCfgUserUpdateRequest"
1025
+ }
971
1026
  }
972
1027
  }
973
1028
  },
@@ -1014,6 +1069,31 @@
1014
1069
  "x-async-capable": false
1015
1070
  }
1016
1071
  },
1072
+ "/cfg/accounts/token/blacklist/": {
1073
+ "post": {
1074
+ "operationId": "cfg_accounts_token_blacklist_create",
1075
+ "description": "Revoke a refresh token (logout).\n\nBlacklists the posted refresh token so it can never mint another access\ntoken. Called best-effort by the client's logout — without it, \"logout\"\nis purely client-side and a stolen refresh token survives until expiry.",
1076
+ "tags": [
1077
+ "cfg_accounts_auth"
1078
+ ],
1079
+ "requestBody": {
1080
+ "content": {
1081
+ "application/json": {
1082
+ "schema": {
1083
+ "$ref": "#/components/schemas/TokenBlacklistRequest"
1084
+ }
1085
+ }
1086
+ },
1087
+ "required": true
1088
+ },
1089
+ "responses": {
1090
+ "200": {
1091
+ "description": "No response body"
1092
+ }
1093
+ },
1094
+ "x-async-capable": false
1095
+ }
1096
+ },
1017
1097
  "/cfg/accounts/token/refresh/": {
1018
1098
  "post": {
1019
1099
  "operationId": "cfg_accounts_token_refresh_create",
@@ -1060,6 +1140,9 @@
1060
1140
  },
1061
1141
  {
1062
1142
  "jwtAuthWithLastLogin": []
1143
+ },
1144
+ {
1145
+ "tokenAuth": []
1063
1146
  }
1064
1147
  ],
1065
1148
  "responses": {
@@ -1110,6 +1193,9 @@
1110
1193
  },
1111
1194
  {
1112
1195
  "jwtAuthWithLastLogin": []
1196
+ },
1197
+ {
1198
+ "tokenAuth": []
1113
1199
  }
1114
1200
  ],
1115
1201
  "responses": {
@@ -1150,6 +1236,9 @@
1150
1236
  },
1151
1237
  {
1152
1238
  "jwtAuthWithLastLogin": []
1239
+ },
1240
+ {
1241
+ "tokenAuth": []
1153
1242
  }
1154
1243
  ],
1155
1244
  "responses": {
@@ -1190,6 +1279,9 @@
1190
1279
  },
1191
1280
  {
1192
1281
  "jwtAuthWithLastLogin": []
1282
+ },
1283
+ {
1284
+ "tokenAuth": []
1193
1285
  }
1194
1286
  ],
1195
1287
  "responses": {
@@ -1231,6 +1323,9 @@
1231
1323
  },
1232
1324
  {
1233
1325
  "jwtAuthWithLastLogin": []
1326
+ },
1327
+ {
1328
+ "tokenAuth": []
1234
1329
  }
1235
1330
  ],
1236
1331
  "responses": {
@@ -1264,6 +1359,9 @@
1264
1359
  },
1265
1360
  {
1266
1361
  "jwtAuthWithLastLogin": []
1362
+ },
1363
+ {
1364
+ "tokenAuth": []
1267
1365
  }
1268
1366
  ],
1269
1367
  "responses": {
@@ -1313,6 +1411,9 @@
1313
1411
  },
1314
1412
  {
1315
1413
  "jwtAuthWithLastLogin": []
1414
+ },
1415
+ {
1416
+ "tokenAuth": []
1316
1417
  }
1317
1418
  ],
1318
1419
  "responses": {
@@ -1363,6 +1464,9 @@
1363
1464
  },
1364
1465
  {
1365
1466
  "jwtAuthWithLastLogin": []
1467
+ },
1468
+ {
1469
+ "tokenAuth": []
1366
1470
  }
1367
1471
  ],
1368
1472
  "responses": {
@@ -1414,6 +1518,9 @@
1414
1518
  {
1415
1519
  "jwtAuthWithLastLogin": []
1416
1520
  },
1521
+ {
1522
+ "tokenAuth": []
1523
+ },
1417
1524
  {}
1418
1525
  ],
1419
1526
  "responses": {
@@ -1475,6 +1582,9 @@
1475
1582
  {
1476
1583
  "jwtAuthWithLastLogin": []
1477
1584
  },
1585
+ {
1586
+ "tokenAuth": []
1587
+ },
1478
1588
  {}
1479
1589
  ],
1480
1590
  "responses": {
@@ -2439,6 +2549,19 @@
2439
2549
  "secret"
2440
2550
  ]
2441
2551
  },
2552
+ "TokenBlacklistRequest": {
2553
+ "type": "object",
2554
+ "properties": {
2555
+ "refresh": {
2556
+ "type": "string",
2557
+ "writeOnly": true,
2558
+ "minLength": 1
2559
+ }
2560
+ },
2561
+ "required": [
2562
+ "refresh"
2563
+ ]
2564
+ },
2442
2565
  "TokenRefresh": {
2443
2566
  "type": "object",
2444
2567
  "properties": {
@@ -2943,6 +3066,12 @@
2943
3066
  "type": "http",
2944
3067
  "scheme": "bearer",
2945
3068
  "bearerFormat": "JWT"
3069
+ },
3070
+ "tokenAuth": {
3071
+ "type": "apiKey",
3072
+ "in": "header",
3073
+ "name": "Authorization",
3074
+ "description": "Token-based authentication with required prefix \"Token\""
2946
3075
  }
2947
3076
  }
2948
3077
  },