@classic-homes/auth 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -270,6 +270,34 @@ Two server rules to design around:
270
270
  - The server refuses to remove a user's **last** role, or to grant a role at or
271
271
  above the acting user's own level. Both come back as errors.
272
272
 
273
+ ## Token refresh
274
+
275
+ Access tokens are short-lived and refresh tokens rotate (single-use), so a
276
+ refresh must never run twice for the same token. `refreshSession()` is the one
277
+ refresh path — the client's own 401 retry uses it, and so should any code that
278
+ refreshes proactively:
279
+
280
+ ```typescript
281
+ import { refreshSession } from '@classic-homes/auth/core';
282
+
283
+ // Before a route guard reads claims (roles/permissions live in the JWT):
284
+ const outcome = await refreshSession({ onlyIfExpired: true });
285
+ // 'fresh' — token still valid, nothing sent
286
+ // 'refreshed' — tokens stored and the Svelte store already updated
287
+ // 'rejected' — session cleared (handleSessionExpired/onSessionExpired/onLogout ran)
288
+ // 'unavailable' — server unreachable/5xx even after retries; session kept, try later
289
+ ```
290
+
291
+ Guarantees: single-flight per tab (concurrent callers share one call),
292
+ serialized across tabs with a Web Lock and reconciled if another tab rotated
293
+ first, and transient failures retried with `refreshRetryDelays` backoff
294
+ (default `[1000, 3000]`). Do not call `authApi.refreshToken` + `updateStoredTokens`
295
+ yourself for this — that bypasses all of the above and can orphan a rotated
296
+ token.
297
+
298
+ Call `initCrossTabSync()` once at app start so a refresh or sign-out in another
299
+ tab rehydrates this tab's Svelte store.
300
+
273
301
  ## API Reference
274
302
 
275
303
  ### Core Exports
@@ -1,3 +1,3 @@
1
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-EBTCVIUQ.js';
2
- import './chunk-HVOZBQIF.js';
1
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-OCIQI3ZZ.js';
2
+ import './chunk-S4Y4XXDN.js';
3
3
  import './chunk-X7KH4DDN.js';
@@ -1,5 +1,5 @@
1
- import { authService } from './chunk-QTX7L4QY.js';
2
- import { authStore, currentUser, isAuthenticated, authActions } from './chunk-EBTCVIUQ.js';
1
+ import { authService } from './chunk-ZWCIY6MG.js';
2
+ import { authStore, currentUser, isAuthenticated, authActions } from './chunk-OCIQI3ZZ.js';
3
3
  import { isInitialized, initAuth, getUserRoles } from './chunk-X7KH4DDN.js';
4
4
 
5
5
  // src/svelte/guards/auth-guard.ts
@@ -1,4 +1,4 @@
1
- import { authApi } from './chunk-HVOZBQIF.js';
1
+ import { authApi } from './chunk-S4Y4XXDN.js';
2
2
  import { isInitialized, getConfig, hasRole, hasAnyRole, hasAllRoles, decodeJWT, getStorage, getDefaultStorage } from './chunk-X7KH4DDN.js';
3
3
 
4
4
  // src/svelte/stores/auth.svelte.ts
@@ -1,36 +1,9 @@
1
- import { getConfig, getStorage, getFetch, isValidRedirectUrl } from './chunk-X7KH4DDN.js';
1
+ import { getConfig, getStorage, isTokenExpired, getFetch, isValidRedirectUrl } from './chunk-X7KH4DDN.js';
2
2
 
3
3
  // src/core/client.ts
4
- var isRefreshing = false;
5
- var refreshSubscribers = [];
6
- var MAX_REFRESH_SUBSCRIBERS = 100;
7
- function subscribeTokenRefresh(cb) {
8
- if (refreshSubscribers.length >= MAX_REFRESH_SUBSCRIBERS) {
9
- throw new Error("Too many concurrent requests waiting for token refresh. Please try again.");
10
- }
11
- refreshSubscribers.push(cb);
12
- return () => {
13
- const idx = refreshSubscribers.indexOf(cb);
14
- if (idx > -1) refreshSubscribers.splice(idx, 1);
15
- };
16
- }
17
- function flushRefreshSubscribers(token, terminal = false) {
18
- const subs = refreshSubscribers;
19
- refreshSubscribers = [];
20
- subs.forEach((cb) => {
21
- try {
22
- cb(token, terminal);
23
- } catch (e) {
24
- console.error("Token refresh subscriber error:", e);
25
- }
26
- });
27
- }
28
- function onTokenRefreshed(token) {
29
- flushRefreshSubscribers(token);
30
- }
31
- function onTokenRefreshFailed(terminal) {
32
- flushRefreshSubscribers(null, terminal);
33
- }
4
+ var DEFAULT_REFRESH_RETRY_DELAYS = [1e3, 3e3];
5
+ var refreshInflight = null;
6
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
34
7
  function getAccessToken() {
35
8
  const config = getConfig();
36
9
  const storage = getStorage();
@@ -65,6 +38,9 @@ function getSessionToken() {
65
38
  return storage.getItem(getSessionTokenKey());
66
39
  }
67
40
  function updateStoredTokens(accessToken, refreshToken) {
41
+ void writeStoredTokens(accessToken, refreshToken);
42
+ }
43
+ function writeStoredTokens(accessToken, refreshToken) {
68
44
  const config = getConfig();
69
45
  const storage = getStorage();
70
46
  const storageKey = config.storageKey ?? "classic_auth";
@@ -77,11 +53,15 @@ function updateStoredTokens(accessToken, refreshToken) {
77
53
  parsed.accessToken = accessToken;
78
54
  parsed.refreshToken = refreshToken;
79
55
  storage.setItem(storageKey, JSON.stringify(parsed));
80
- import('./auth.svelte-FIHKI5BD.js').then(({ authStore }) => {
81
- authStore.updateTokens(accessToken, refreshToken);
82
- }).catch(() => {
83
- });
84
56
  config.onTokenRefresh?.({ accessToken, refreshToken });
57
+ return syncSvelteStore((authStore) => authStore.updateTokens(accessToken, refreshToken));
58
+ }
59
+ async function syncSvelteStore(apply) {
60
+ try {
61
+ const { authStore } = await import('./auth.svelte-P5ICPWBD.js');
62
+ apply(authStore);
63
+ } catch {
64
+ }
85
65
  }
86
66
  function clearStoredAuth() {
87
67
  const config = getConfig();
@@ -91,20 +71,12 @@ function clearStoredAuth() {
91
71
  }
92
72
  async function expireSession() {
93
73
  clearStoredAuth();
94
- try {
95
- const { authStore } = await import('./auth.svelte-FIHKI5BD.js');
96
- authStore.handleSessionExpired();
97
- } catch {
98
- }
74
+ await syncSvelteStore((authStore) => authStore.handleSessionExpired());
99
75
  getConfig().onLogout?.();
100
76
  }
101
- async function refreshAccessToken() {
102
- const refreshToken = getRefreshToken();
77
+ async function refreshOnce(refreshToken) {
103
78
  const config = getConfig();
104
79
  const fetchFn = getFetch();
105
- if (!refreshToken) {
106
- return "rejected";
107
- }
108
80
  const timeoutMs = config.requestTimeout ?? 3e4;
109
81
  const controller = new AbortController();
110
82
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -119,41 +91,94 @@ async function refreshAccessToken() {
119
91
  });
120
92
  if (response.ok) {
121
93
  let apiResponse;
122
- const contentType = response.headers.get("content-type");
123
- if (contentType?.includes("application/json")) {
124
- try {
125
- apiResponse = await response.json();
126
- } catch {
127
- return "unavailable";
128
- }
129
- } else {
130
- try {
131
- apiResponse = await response.json();
132
- } catch {
133
- return "unavailable";
134
- }
94
+ try {
95
+ apiResponse = await response.json();
96
+ } catch {
97
+ return { outcome: "unavailable" };
135
98
  }
136
99
  const data = apiResponse?.data;
137
100
  const tokens = data && typeof data === "object" && "data" in data ? data.data : data;
138
- if (tokens?.accessToken) {
139
- const newRefreshToken = tokens.refreshToken ?? refreshToken;
140
- updateStoredTokens(tokens.accessToken, newRefreshToken);
141
- onTokenRefreshed(tokens.accessToken);
142
- return "refreshed";
101
+ if (typeof tokens?.accessToken === "string" && tokens.accessToken) {
102
+ return {
103
+ outcome: "refreshed",
104
+ accessToken: tokens.accessToken,
105
+ refreshToken: typeof tokens.refreshToken === "string" && tokens.refreshToken ? tokens.refreshToken : refreshToken
106
+ };
143
107
  }
144
- return "unavailable";
108
+ return { outcome: "unavailable" };
145
109
  }
146
- if (response.status >= 500) {
147
- return "unavailable";
110
+ if (response.status >= 500 || response.status === 429 || response.status === 408) {
111
+ return { outcome: "unavailable" };
148
112
  }
149
- return "rejected";
113
+ return { outcome: "rejected" };
150
114
  } catch (error) {
151
115
  config.onAuthError?.(error instanceof Error ? error : new Error("Token refresh failed"));
152
- return "unavailable";
116
+ return { outcome: "unavailable" };
153
117
  } finally {
154
118
  clearTimeout(timeoutId);
155
119
  }
156
120
  }
121
+ function withRefreshLock(fn) {
122
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
123
+ if (!locks) return fn();
124
+ const storageKey = getConfig().storageKey ?? "classic_auth";
125
+ return locks.request(`${storageKey}:refresh`, fn);
126
+ }
127
+ function refreshSession(options = {}) {
128
+ if (options.onlyIfExpired) {
129
+ const accessToken = getAccessToken();
130
+ if (accessToken && !isTokenExpired(accessToken)) return Promise.resolve("fresh");
131
+ }
132
+ if (!refreshInflight) {
133
+ refreshInflight = runRefresh(options).finally(() => {
134
+ refreshInflight = null;
135
+ });
136
+ }
137
+ return refreshInflight;
138
+ }
139
+ async function runRefresh(options) {
140
+ const presented = getRefreshToken();
141
+ if (!presented) {
142
+ await expireSession();
143
+ return "rejected";
144
+ }
145
+ return withRefreshLock(async () => {
146
+ const current = getRefreshToken();
147
+ if (current !== presented) {
148
+ if (!current) {
149
+ await expireSession();
150
+ return "rejected";
151
+ }
152
+ await syncSvelteStore((authStore) => authStore.rehydrate());
153
+ return "refreshed";
154
+ }
155
+ if (options.onlyIfExpired) {
156
+ const accessToken = getAccessToken();
157
+ if (accessToken && !isTokenExpired(accessToken)) return "fresh";
158
+ }
159
+ const delays = getConfig().refreshRetryDelays ?? DEFAULT_REFRESH_RETRY_DELAYS;
160
+ let attempt = await refreshOnce(current);
161
+ for (const delay of delays) {
162
+ if (attempt.outcome !== "unavailable") break;
163
+ await sleep(delay);
164
+ attempt = await refreshOnce(current);
165
+ }
166
+ if (attempt.outcome === "refreshed") {
167
+ await writeStoredTokens(attempt.accessToken, attempt.refreshToken);
168
+ return "refreshed";
169
+ }
170
+ if (attempt.outcome === "rejected") {
171
+ const stored = getRefreshToken();
172
+ if (stored && stored !== current) {
173
+ await syncSvelteStore((authStore) => authStore.rehydrate());
174
+ return "refreshed";
175
+ }
176
+ await expireSession();
177
+ return "rejected";
178
+ }
179
+ return "unavailable";
180
+ });
181
+ }
157
182
  function extractData(response) {
158
183
  if (response && typeof response === "object" && "data" in response) {
159
184
  const dataField = response.data;
@@ -211,61 +236,14 @@ async function apiRequestInternal(endpoint, options, isRetry) {
211
236
  await expireSession();
212
237
  throw new Error("Authentication failed. Please sign in again.");
213
238
  }
214
- if (!isRefreshing) {
215
- isRefreshing = true;
216
- let refreshResult = "unavailable";
217
- try {
218
- refreshResult = await refreshAccessToken();
219
- } finally {
220
- isRefreshing = false;
221
- }
222
- if (refreshResult === "refreshed") {
223
- return apiRequestInternal(endpoint, options, true);
224
- }
225
- if (refreshResult === "rejected") {
226
- onTokenRefreshFailed(true);
227
- await expireSession();
228
- throw new Error("Authentication failed. Please sign in again.");
229
- }
230
- onTokenRefreshFailed(false);
231
- throw new Error("Authentication service unavailable. Please try again.");
232
- } else {
233
- const REFRESH_TIMEOUT_MS = 3e4;
234
- return new Promise((resolve, reject) => {
235
- let settled = false;
236
- const refreshTimeoutId = setTimeout(() => {
237
- if (!settled) {
238
- settled = true;
239
- unsubscribe?.();
240
- reject(new Error("Token refresh timed out. Please sign in again."));
241
- }
242
- }, REFRESH_TIMEOUT_MS);
243
- let unsubscribe = null;
244
- try {
245
- unsubscribe = subscribeTokenRefresh((token, terminal) => {
246
- if (!settled) {
247
- settled = true;
248
- clearTimeout(refreshTimeoutId);
249
- if (token) {
250
- apiRequestInternal(endpoint, options, true).then(resolve).catch(reject);
251
- } else {
252
- reject(
253
- new Error(
254
- terminal ? "Authentication failed. Please sign in again." : "Authentication service unavailable. Please try again."
255
- )
256
- );
257
- }
258
- }
259
- });
260
- } catch (subscribeError) {
261
- clearTimeout(refreshTimeoutId);
262
- if (!settled) {
263
- settled = true;
264
- reject(subscribeError);
265
- }
266
- }
267
- });
239
+ const outcome = await refreshSession();
240
+ if (outcome === "refreshed") {
241
+ return apiRequestInternal(endpoint, options, true);
242
+ }
243
+ if (outcome === "rejected") {
244
+ throw new Error("Authentication failed. Please sign in again.");
268
245
  }
246
+ throw new Error("Authentication service unavailable. Please try again.");
269
247
  }
270
248
  let data;
271
249
  const contentType = response.headers.get("content-type");
@@ -320,6 +298,18 @@ async function apiRequestInternal(endpoint, options, isRetry) {
320
298
  throw new Error("API request failed");
321
299
  }
322
300
  }
301
+ function initCrossTabSync() {
302
+ if (typeof window === "undefined") return () => {
303
+ };
304
+ const config = getConfig();
305
+ const storageKey = config.storageKey ?? "classic_auth";
306
+ function handleStorage(event) {
307
+ if (event.key !== storageKey) return;
308
+ void syncSvelteStore((authStore) => authStore.rehydrate());
309
+ }
310
+ window.addEventListener("storage", handleStorage);
311
+ return () => window.removeEventListener("storage", handleStorage);
312
+ }
323
313
  var api = {
324
314
  get: (endpoint, authenticated = false, customFetch) => apiRequest(endpoint, { method: "GET", authenticated, customFetch }),
325
315
  post: (endpoint, body, authenticated = false, customFetch) => apiRequest(endpoint, {
@@ -830,4 +820,4 @@ var authApi = {
830
820
  }
831
821
  };
832
822
 
833
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens };
823
+ export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, refreshSession, updateStoredTokens };
@@ -1,4 +1,4 @@
1
- import { authApi } from './chunk-HVOZBQIF.js';
1
+ import { authApi } from './chunk-S4Y4XXDN.js';
2
2
  import { getUserRoles, hasAnyRole } from './chunk-X7KH4DDN.js';
3
3
 
4
4
  // src/core/guards.ts
@@ -59,7 +59,7 @@ var AuthService = class {
59
59
  }
60
60
  if (options?.autoSetAuth !== false && !isMfaChallengeResponse(response)) {
61
61
  try {
62
- const { authStore } = await import('./auth.svelte-FIHKI5BD.js');
62
+ const { authStore } = await import('./auth.svelte-P5ICPWBD.js');
63
63
  authStore.setAuth(
64
64
  response.accessToken,
65
65
  response.refreshToken,
@@ -245,7 +245,7 @@ var AuthService = class {
245
245
  }
246
246
  if (options?.autoSetAuth !== false) {
247
247
  try {
248
- const { authStore } = await import('./auth.svelte-FIHKI5BD.js');
248
+ const { authStore } = await import('./auth.svelte-P5ICPWBD.js');
249
249
  authStore.setAuth(
250
250
  response.accessToken,
251
251
  response.refreshToken,
@@ -1,6 +1,6 @@
1
- import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, p as User, j as ProfileUpdateData, C as ChangePasswordData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, q as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination, r as UserRolesResponse } from '../types-C6MqT3DB.js';
2
- export { a as AuthConfig, b as AuthState, l as ResetPasswordData, S as SSOConfig, o as StorageAdapter, s as createMemoryStorage, t as createSessionStorage, u as getConfig, v as getDefaultStorage, w as getFetch, x as getStorage, y as initAuth, z as isInitialized, B as resetConfig } from '../types-C6MqT3DB.js';
3
- export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-Dla77tvM.js';
1
+ import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, p as User, j as ProfileUpdateData, C as ChangePasswordData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, q as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination, r as UserRolesResponse } from '../types-Bqh2gkvu.js';
2
+ export { a as AuthConfig, b as AuthState, l as ResetPasswordData, S as SSOConfig, o as StorageAdapter, s as createMemoryStorage, t as createSessionStorage, u as getConfig, v as getDefaultStorage, w as getFetch, x as getStorage, y as initAuth, z as isInitialized, B as resetConfig } from '../types-Bqh2gkvu.js';
3
+ export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-92PeSjcw.js';
4
4
 
5
5
  /**
6
6
  * HTTP Client
@@ -39,6 +39,26 @@ interface ApiResponse<T = unknown> {
39
39
  code?: string;
40
40
  };
41
41
  }
42
+ /**
43
+ * Outcome of {@link refreshSession}:
44
+ * - `refreshed`: the stored tokens are new — either this call refreshed them
45
+ * or another tab/caller did while this call waited its turn.
46
+ * - `fresh`: nothing to do — `onlyIfExpired` was set and the stored access
47
+ * token is still valid.
48
+ * - `rejected`: the server definitively rejected the refresh token (4xx) —
49
+ * the session is unrecoverable and has been cleared.
50
+ * - `unavailable`: the server could not be reached or errored (network
51
+ * failure, timeout, 5xx, 429) even after retries — the refresh token may
52
+ * still be perfectly good, so the session is kept and the caller can retry.
53
+ */
54
+ type RefreshOutcome = 'refreshed' | 'fresh' | 'rejected' | 'unavailable';
55
+ interface RefreshSessionOptions {
56
+ /**
57
+ * Skip the network call (→ `fresh`) when the stored access token has not
58
+ * expired yet. Default `false`: refresh unconditionally.
59
+ */
60
+ onlyIfExpired?: boolean;
61
+ }
42
62
  /**
43
63
  * Get the current access token from storage.
44
64
  */
@@ -53,18 +73,39 @@ declare function getRefreshToken(): string | null;
53
73
  declare function getSessionToken(): string | null;
54
74
  /**
55
75
  * Update stored tokens.
56
- * Also updates the Svelte auth store if available.
76
+ * Also updates the Svelte auth store if available (asynchronously).
57
77
  *
58
- * Note: The read-modify-write on localStorage is not atomic across tabs —
59
- * concurrent refreshes in two tabs are last-write-wins. The `storage` event
60
- * listener (set up via `initCrossTabSync`) propagates whichever write landed
61
- * last, so tabs converge on the same tokens.
78
+ * Prefer {@link refreshSession} over calling this with tokens obtained from a
79
+ * hand-rolled `/auth/refresh` call: that path bypasses the single-flight and
80
+ * cross-tab serialization, so it can race the client's own refresh and
81
+ * orphan a rotated token.
62
82
  */
63
83
  declare function updateStoredTokens(accessToken: string, refreshToken: string): void;
64
84
  /**
65
85
  * Clear stored auth data.
66
86
  */
67
87
  declare function clearStoredAuth(): void;
88
+ /**
89
+ * Refresh the session's tokens. The single refresh path for the whole client:
90
+ *
91
+ * - **Single-flight per tab.** Concurrent callers (several 401s, a proactive
92
+ * refresh before navigation, an upload client) share one in-flight call.
93
+ * - **Serialized across tabs** via a Web Lock, and reconciled: if another tab
94
+ * rotated the token while this call waited, the stored tokens are adopted
95
+ * (`refreshed`) instead of re-presenting a token the server has retired.
96
+ * - **Transient failures retried** with `AuthConfig.refreshRetryDelays`
97
+ * backoff, re-presenting the same token (a failed attempt never reached the
98
+ * rotation write).
99
+ * - **Consequences applied here.** `refreshed` → tokens stored and the Svelte
100
+ * store updated before this resolves; `rejected` → the session is cleared
101
+ * (`handleSessionExpired`, `onSessionExpired`, `onLogout`); `unavailable` →
102
+ * the session is left intact for a later retry.
103
+ *
104
+ * Apps that refresh proactively (e.g. before a route guard reads claims)
105
+ * should call this rather than `authApi.refreshToken`, which bypasses all of
106
+ * the above.
107
+ */
108
+ declare function refreshSession(options?: RefreshSessionOptions): Promise<RefreshOutcome>;
68
109
  /**
69
110
  * Helper to extract data from CHAPI response wrapper.
70
111
  *
@@ -81,6 +122,17 @@ declare function extractData<T>(response: {
81
122
  * Make an API request.
82
123
  */
83
124
  declare function apiRequest<T = unknown>(endpoint: string, options?: ApiRequestOptions): Promise<T>;
125
+ /**
126
+ * Set up cross-tab session synchronization via the `storage` event.
127
+ * When another tab refreshes tokens or signs out, this tab's Svelte store
128
+ * (when present) is rehydrated from storage so its claims and
129
+ * `isAuthenticated` follow. Requests already read tokens from storage on
130
+ * every call, so they pick up the change without this; the store is what
131
+ * would otherwise go stale.
132
+ *
133
+ * Call once during app initialization.
134
+ */
135
+ declare function initCrossTabSync(): () => void;
84
136
  /**
85
137
  * Convenience methods for common HTTP methods.
86
138
  */
@@ -565,4 +617,4 @@ declare function hasAnyRole(user: Pick<User, 'roles'> | null | undefined, roles:
565
617
  */
566
618
  declare function hasAllRoles(user: Pick<User, 'roles'> | null | undefined, roles: readonly string[]): boolean;
567
619
 
568
- export { ApiKey, type ApiRequestOptions, type ApiResponse, ChangePasswordData, CreateApiKeyRequest, CreateApiKeyResponse, Device, type JWTPayload, LinkedAccount, LoginCredentials, LoginResponse, LogoutResponse, MFAChallengeData, MFASetupResponse, MFAStatus, Pagination, ProfileUpdateData, ROLE_HIERARCHY, ROLE_NAMES, RegisterData, RegisterResponse, type RoleName, SecurityEvent, Session, UpdateApiKeyRequest, User, UserPreferences, UserRolesResponse, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, updateStoredTokens };
620
+ export { ApiKey, type ApiRequestOptions, type ApiResponse, ChangePasswordData, CreateApiKeyRequest, CreateApiKeyResponse, Device, type JWTPayload, LinkedAccount, LoginCredentials, LoginResponse, LogoutResponse, MFAChallengeData, MFASetupResponse, MFAStatus, Pagination, ProfileUpdateData, ROLE_HIERARCHY, ROLE_NAMES, type RefreshOutcome, type RefreshSessionOptions, RegisterData, RegisterResponse, type RoleName, SecurityEvent, Session, UpdateApiKeyRequest, User, UserPreferences, UserRolesResponse, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, refreshSession, updateStoredTokens };
@@ -1,3 +1,3 @@
1
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-QTX7L4QY.js';
2
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from '../chunk-HVOZBQIF.js';
1
+ export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-ZWCIY6MG.js';
2
+ export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, refreshSession, updateStoredTokens } from '../chunk-S4Y4XXDN.js';
3
3
  export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-X7KH4DDN.js';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ApiKey, a as AuthConfig, b as AuthState, C as ChangePasswordData, c as CreateApiKeyRequest, d as CreateApiKeyResponse, D as Device, L as LinkedAccount, e as LoginCredentials, f as LoginResponse, g as LogoutResponse, M as MFAChallengeData, h as MFASetupResponse, i as MFAStatus, P as Pagination, j as ProfileUpdateData, R as RegisterData, k as RegisterResponse, l as ResetPasswordData, S as SSOConfig, m as SecurityEvent, n as Session, o as StorageAdapter, U as UpdateApiKeyRequest, p as User, q as UserPreferences, r as UserRolesResponse, s as createMemoryStorage, t as createSessionStorage, u as getConfig, v as getDefaultStorage, w as getFetch, x as getStorage, y as initAuth, z as isInitialized, B as resetConfig } from './types-C6MqT3DB.js';
2
- export { ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RoleName, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, updateStoredTokens } from './core/index.js';
3
- export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from './user-utils-Dla77tvM.js';
1
+ export { A as ApiKey, a as AuthConfig, b as AuthState, C as ChangePasswordData, c as CreateApiKeyRequest, d as CreateApiKeyResponse, D as Device, L as LinkedAccount, e as LoginCredentials, f as LoginResponse, g as LogoutResponse, M as MFAChallengeData, h as MFASetupResponse, i as MFAStatus, P as Pagination, j as ProfileUpdateData, R as RegisterData, k as RegisterResponse, l as ResetPasswordData, S as SSOConfig, m as SecurityEvent, n as Session, o as StorageAdapter, U as UpdateApiKeyRequest, p as User, q as UserPreferences, r as UserRolesResponse, s as createMemoryStorage, t as createSessionStorage, u as getConfig, v as getDefaultStorage, w as getFetch, x as getStorage, y as initAuth, z as isInitialized, B as resetConfig } from './types-Bqh2gkvu.js';
2
+ export { ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RefreshOutcome, RefreshSessionOptions, RoleName, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, refreshSession, updateStoredTokens } from './core/index.js';
3
+ export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from './user-utils-92PeSjcw.js';
4
4
  export { AuthClient, AuthGuardOptions, AuthGuardResult, AuthHookOptions, CreateAuthClientOptions, NavFilterOptions, RoleRestrictedItem, authActions, authStore, canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, currentUser, filterByAccess, filterNavSections, isAuthenticated, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './svelte/index.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-UGD4PNYI.js';
2
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-QTX7L4QY.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-EBTCVIUQ.js';
4
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from './chunk-HVOZBQIF.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-AVSOQVSW.js';
2
+ export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-ZWCIY6MG.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-OCIQI3ZZ.js';
4
+ export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, refreshSession, updateStoredTokens } from './chunk-S4Y4XXDN.js';
5
5
  export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-X7KH4DDN.js';
@@ -1,7 +1,7 @@
1
- import { b as AuthState, p as User, a as AuthConfig } from '../types-C6MqT3DB.js';
2
- export { y as initAuth, z as isInitialized } from '../types-C6MqT3DB.js';
3
- import { a as authService } from '../user-utils-Dla77tvM.js';
4
- export { R as RoleDeniedError, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-Dla77tvM.js';
1
+ import { b as AuthState, p as User, a as AuthConfig } from '../types-Bqh2gkvu.js';
2
+ export { y as initAuth, z as isInitialized } from '../types-Bqh2gkvu.js';
3
+ import { a as authService } from '../user-utils-92PeSjcw.js';
4
+ export { R as RoleDeniedError, f as formatUserRoles, g as getAvatarFallback, b as getDisplayName, c as getGreeting, d as getUserEmail, e as getUserInitials, i as isRoleDeniedError } from '../user-utils-92PeSjcw.js';
5
5
 
6
6
  /**
7
7
  * Auth Store
@@ -1,5 +1,5 @@
1
- export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-UGD4PNYI.js';
2
- export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-QTX7L4QY.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-EBTCVIUQ.js';
4
- import '../chunk-HVOZBQIF.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-AVSOQVSW.js';
2
+ export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-ZWCIY6MG.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-OCIQI3ZZ.js';
4
+ import '../chunk-S4Y4XXDN.js';
5
5
  export { initAuth, isInitialized } from '../chunk-X7KH4DDN.js';
@@ -1,4 +1,4 @@
1
- import { p as User, D as Device, f as LoginResponse, m as SecurityEvent, n as Session, A as ApiKey, L as LinkedAccount, g as LogoutResponse, h as MFASetupResponse, i as MFAStatus, k as RegisterResponse, q as UserPreferences, o as StorageAdapter, b as AuthState, a as AuthConfig } from '../types-C6MqT3DB.js';
1
+ import { p as User, D as Device, f as LoginResponse, m as SecurityEvent, n as Session, A as ApiKey, L as LinkedAccount, g as LogoutResponse, h as MFASetupResponse, i as MFAStatus, k as RegisterResponse, q as UserPreferences, o as StorageAdapter, b as AuthState, a as AuthConfig } from '../types-Bqh2gkvu.js';
2
2
 
3
3
  /**
4
4
  * User Fixtures
@@ -52,6 +52,12 @@ interface AuthConfig {
52
52
  onSessionExpired?: (currentPath: string) => void;
53
53
  /** Request timeout in milliseconds (default: 30000) */
54
54
  requestTimeout?: number;
55
+ /**
56
+ * Backoff (ms) between retries of a token refresh that failed transiently
57
+ * (network error, timeout, 5xx, 429). One retry per entry; `[]` disables
58
+ * retries. Default: `[1000, 3000]` — three attempts in all.
59
+ */
60
+ refreshRetryDelays?: number[];
55
61
  /** SSO configuration */
56
62
  sso?: SSOConfig;
57
63
  /**
@@ -1,4 +1,4 @@
1
- import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, p as User, j as ProfileUpdateData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, q as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from './types-C6MqT3DB.js';
1
+ import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, p as User, j as ProfileUpdateData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, q as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from './types-Bqh2gkvu.js';
2
2
 
3
3
  /**
4
4
  * Auth Service
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/auth",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Authentication services and Svelte bindings for Classic Theme apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",