@classic-homes/auth 0.1.53 → 0.1.56

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
@@ -37,6 +37,12 @@ initAuth({
37
37
  setItem: (key, value) => localStorage.setItem(key, value),
38
38
  removeItem: (key) => localStorage.removeItem(key),
39
39
  },
40
+ // ⚠️ SECURITY: localStorage (also the default when `storage` is omitted)
41
+ // persists the long-lived refresh token where any XSS can read it.
42
+ // Safer options exported from '@classic-homes/auth/core':
43
+ // storage: createSessionStorage() // per-tab, cleared on close
44
+ // storage: createMemoryStorage() // never persisted, lost on reload
45
+ // or keep tokens out of JS entirely with an httpOnly-cookie flow.
40
46
  // SSO configuration (optional)
41
47
  sso: {
42
48
  enabled: true,
@@ -0,0 +1,3 @@
1
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-CPFM7JEI.js';
2
+ import './chunk-2QYFVJEM.js';
3
+ import './chunk-6NTZVDLW.js';
@@ -1,9 +1,13 @@
1
- import { getConfig, getStorage, getFetch, isValidRedirectUrl } from './chunk-EM7PUNZB.js';
1
+ import { getConfig, getStorage, getFetch, isValidRedirectUrl } from './chunk-6NTZVDLW.js';
2
2
 
3
3
  // src/core/client.ts
4
4
  var isRefreshing = false;
5
5
  var refreshSubscribers = [];
6
+ var MAX_REFRESH_SUBSCRIBERS = 100;
6
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
+ }
7
11
  refreshSubscribers.push(cb);
8
12
  return () => {
9
13
  const idx = refreshSubscribers.indexOf(cb);
@@ -11,8 +15,15 @@ function subscribeTokenRefresh(cb) {
11
15
  };
12
16
  }
13
17
  function onTokenRefreshed(token) {
14
- refreshSubscribers.forEach((cb) => cb(token));
18
+ const subs = refreshSubscribers;
15
19
  refreshSubscribers = [];
20
+ subs.forEach((cb) => {
21
+ try {
22
+ cb(token);
23
+ } catch (e) {
24
+ console.error("Token refresh subscriber error:", e);
25
+ }
26
+ });
16
27
  }
17
28
  function getAccessToken() {
18
29
  const config = getConfig();
@@ -51,18 +62,16 @@ function updateStoredTokens(accessToken, refreshToken) {
51
62
  const config = getConfig();
52
63
  const storage = getStorage();
53
64
  const storageKey = config.storageKey ?? "classic_auth";
54
- const authData = storage.getItem(storageKey);
55
65
  let parsed = {};
56
- if (authData) {
57
- try {
58
- parsed = JSON.parse(authData);
59
- } catch {
60
- }
66
+ try {
67
+ const authData = storage.getItem(storageKey);
68
+ if (authData) parsed = JSON.parse(authData);
69
+ } catch {
61
70
  }
62
71
  parsed.accessToken = accessToken;
63
72
  parsed.refreshToken = refreshToken;
64
73
  storage.setItem(storageKey, JSON.stringify(parsed));
65
- import('./auth.svelte-JXZ7XEE5.js').then(({ authStore }) => {
74
+ import('./auth.svelte-WPCYPHEE.js').then(({ authStore }) => {
66
75
  authStore.updateTokens(accessToken, refreshToken);
67
76
  }).catch(() => {
68
77
  });
@@ -81,13 +90,17 @@ async function refreshAccessToken() {
81
90
  if (!refreshToken) {
82
91
  return false;
83
92
  }
93
+ const timeoutMs = config.requestTimeout ?? 3e4;
94
+ const controller = new AbortController();
95
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
84
96
  try {
85
97
  const response = await fetchFn(`${config.baseUrl}/auth/refresh`, {
86
98
  method: "POST",
87
99
  headers: {
88
100
  "Content-Type": "application/json"
89
101
  },
90
- body: JSON.stringify({ refreshToken })
102
+ body: JSON.stringify({ refreshToken }),
103
+ signal: controller.signal
91
104
  });
92
105
  if (response.ok) {
93
106
  let apiResponse;
@@ -116,6 +129,8 @@ async function refreshAccessToken() {
116
129
  }
117
130
  } catch (error) {
118
131
  config.onAuthError?.(error instanceof Error ? error : new Error("Token refresh failed"));
132
+ } finally {
133
+ clearTimeout(timeoutId);
119
134
  }
120
135
  return false;
121
136
  }
@@ -131,11 +146,24 @@ function extractData(response) {
131
146
  }
132
147
  return response;
133
148
  }
134
- async function apiRequest(endpoint, options = {}) {
135
- const { authenticated = false, customFetch, body, ...fetchOptions } = options;
149
+ function apiRequest(endpoint, options = {}) {
150
+ return apiRequestInternal(endpoint, options, false);
151
+ }
152
+ async function apiRequestInternal(endpoint, options, isRetry) {
153
+ const { authenticated = false, customFetch, body, timeout, signal, ...fetchOptions } = options;
136
154
  const config = getConfig();
137
155
  const fetchFn = customFetch ?? getFetch();
138
156
  const url = endpoint.startsWith("http") ? endpoint : `${config.baseUrl}${endpoint}`;
157
+ const timeoutMs = timeout ?? config.requestTimeout ?? 3e4;
158
+ const controller = new AbortController();
159
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
160
+ if (signal) {
161
+ if (signal.aborted) {
162
+ controller.abort();
163
+ } else {
164
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
165
+ }
166
+ }
139
167
  const headers = {
140
168
  "Content-Type": "application/json",
141
169
  ...fetchOptions.headers
@@ -154,15 +182,26 @@ async function apiRequest(endpoint, options = {}) {
154
182
  const response = await fetchFn(url, {
155
183
  ...fetchOptions,
156
184
  headers,
157
- body: body ? JSON.stringify(body) : void 0
185
+ body: body ? JSON.stringify(body) : void 0,
186
+ signal: controller.signal
158
187
  });
188
+ clearTimeout(timeoutId);
159
189
  if (response.status === 401 && authenticated && typeof window !== "undefined") {
190
+ if (isRetry) {
191
+ clearStoredAuth();
192
+ config.onLogout?.();
193
+ throw new Error("Authentication failed. Please sign in again.");
194
+ }
160
195
  if (!isRefreshing) {
161
196
  isRefreshing = true;
162
- const refreshed = await refreshAccessToken();
163
- isRefreshing = false;
197
+ let refreshed = false;
198
+ try {
199
+ refreshed = await refreshAccessToken();
200
+ } finally {
201
+ isRefreshing = false;
202
+ }
164
203
  if (refreshed) {
165
- return apiRequest(endpoint, options);
204
+ return apiRequestInternal(endpoint, options, true);
166
205
  } else {
167
206
  clearStoredAuth();
168
207
  config.onLogout?.();
@@ -172,21 +211,29 @@ async function apiRequest(endpoint, options = {}) {
172
211
  const REFRESH_TIMEOUT_MS = 3e4;
173
212
  return new Promise((resolve, reject) => {
174
213
  let settled = false;
175
- let unsubscribe = null;
176
- const timeoutId = setTimeout(() => {
214
+ const refreshTimeoutId = setTimeout(() => {
177
215
  if (!settled) {
178
216
  settled = true;
179
217
  unsubscribe?.();
180
218
  reject(new Error("Token refresh timed out. Please sign in again."));
181
219
  }
182
220
  }, REFRESH_TIMEOUT_MS);
183
- unsubscribe = subscribeTokenRefresh(() => {
221
+ let unsubscribe = null;
222
+ try {
223
+ unsubscribe = subscribeTokenRefresh(() => {
224
+ if (!settled) {
225
+ settled = true;
226
+ clearTimeout(refreshTimeoutId);
227
+ apiRequestInternal(endpoint, options, true).then(resolve).catch(reject);
228
+ }
229
+ });
230
+ } catch (subscribeError) {
231
+ clearTimeout(refreshTimeoutId);
184
232
  if (!settled) {
185
233
  settled = true;
186
- clearTimeout(timeoutId);
187
- apiRequest(endpoint, options).then(resolve).catch(reject);
234
+ reject(subscribeError);
188
235
  }
189
- });
236
+ }
190
237
  });
191
238
  }
192
239
  }
@@ -233,6 +280,10 @@ async function apiRequest(endpoint, options = {}) {
233
280
  }
234
281
  return data;
235
282
  } catch (error) {
283
+ clearTimeout(timeoutId);
284
+ if (error instanceof DOMException && error.name === "AbortError") {
285
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
286
+ }
236
287
  if (error instanceof Error) {
237
288
  throw error;
238
289
  }
@@ -268,6 +319,23 @@ var api = {
268
319
  };
269
320
 
270
321
  // src/core/api.ts
322
+ async function fetchWithTimeout(url, init) {
323
+ const config = getConfig();
324
+ const fetchFn = config.fetch ?? fetch;
325
+ const timeoutMs = config.requestTimeout ?? 3e4;
326
+ const controller = new AbortController();
327
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
328
+ try {
329
+ return await fetchFn(url, { ...init, signal: controller.signal });
330
+ } catch (error) {
331
+ if (error instanceof DOMException && error.name === "AbortError") {
332
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
333
+ }
334
+ throw error;
335
+ } finally {
336
+ clearTimeout(timeoutId);
337
+ }
338
+ }
271
339
  var authApi = {
272
340
  // ============================================================================
273
341
  // Authentication
@@ -292,8 +360,9 @@ var authApi = {
292
360
  return extractData(response);
293
361
  } catch (error) {
294
362
  const config = getConfig();
295
- config.onAuthError?.(error instanceof Error ? error : new Error("Logout API call failed"));
296
- return { success: true };
363
+ const err = error instanceof Error ? error : new Error("Logout API call failed");
364
+ config.onAuthError?.(err);
365
+ return { success: true, serverError: err.message };
297
366
  }
298
367
  },
299
368
  /**
@@ -489,13 +558,12 @@ var authApi = {
489
558
  */
490
559
  async verifyMFAChallenge(data) {
491
560
  const config = getConfig();
492
- const fetchFn = config.fetch ?? fetch;
493
561
  const requestBody = {
494
562
  code: data.code,
495
563
  type: data.method === "backup" ? "backup" : "totp",
496
564
  trustDevice: data.trustDevice
497
565
  };
498
- const response = await fetchFn(`${config.baseUrl}/auth/mfa/challenge`, {
566
+ const response = await fetchWithTimeout(`${config.baseUrl}/auth/mfa/challenge`, {
499
567
  method: "POST",
500
568
  headers: {
501
569
  "Content-Type": "application/json",
@@ -624,8 +692,7 @@ var authApi = {
624
692
  throw new Error("Not authenticated");
625
693
  }
626
694
  const config = getConfig();
627
- const fetchFn = config.fetch ?? fetch;
628
- const response = await fetchFn(`${config.baseUrl}/auth/sso/link`, {
695
+ const response = await fetchWithTimeout(`${config.baseUrl}/auth/sso/link`, {
629
696
  method: "POST",
630
697
  headers: {
631
698
  "Content-Type": "application/json",
@@ -640,6 +707,11 @@ var authApi = {
640
707
  const result = await response.json();
641
708
  const authorizationUrl = extractData(result).authorizationUrl;
642
709
  if (authorizationUrl) {
710
+ if (!isValidRedirectUrl(authorizationUrl)) {
711
+ throw new Error(
712
+ "SSO link failed: authorization URL is not in the list of allowed redirect origins"
713
+ );
714
+ }
643
715
  window.location.href = authorizationUrl;
644
716
  }
645
717
  },
@@ -36,6 +36,34 @@ function getDefaultStorage() {
36
36
  }
37
37
  };
38
38
  }
39
+ function createSessionStorage() {
40
+ if (typeof window !== "undefined" && window.sessionStorage) {
41
+ return {
42
+ getItem: (key) => sessionStorage.getItem(key),
43
+ setItem: (key, value) => sessionStorage.setItem(key, value),
44
+ removeItem: (key) => sessionStorage.removeItem(key)
45
+ };
46
+ }
47
+ return {
48
+ getItem: () => null,
49
+ setItem: () => {
50
+ },
51
+ removeItem: () => {
52
+ }
53
+ };
54
+ }
55
+ function createMemoryStorage() {
56
+ const store = /* @__PURE__ */ new Map();
57
+ return {
58
+ getItem: (key) => store.get(key) ?? null,
59
+ setItem: (key, value) => {
60
+ store.set(key, value);
61
+ },
62
+ removeItem: (key) => {
63
+ store.delete(key);
64
+ }
65
+ };
66
+ }
39
67
  function getStorage() {
40
68
  const cfg = getConfig();
41
69
  return cfg.storage ?? getDefaultStorage();
@@ -91,7 +119,8 @@ function isTokenExpired(token) {
91
119
  if (!payload || !payload.exp) {
92
120
  return true;
93
121
  }
94
- return payload.exp * 1e3 < Date.now();
122
+ const CLOCK_SKEW_MS = 3e4;
123
+ return payload.exp * 1e3 < Date.now() - CLOCK_SKEW_MS;
95
124
  }
96
125
  function getTokenRemainingTime(token) {
97
126
  const payload = decodeJWT(token);
@@ -122,4 +151,4 @@ function extractClaims(token, claims) {
122
151
  return result;
123
152
  }
124
153
 
125
- export { decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, isValidRedirectUrl, resetConfig };
154
+ export { createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, isValidRedirectUrl, resetConfig };
@@ -1,5 +1,5 @@
1
- import { authApi } from './chunk-U5E4T4NS.js';
2
- import { decodeJWT, isInitialized, getConfig, getStorage, getDefaultStorage } from './chunk-EM7PUNZB.js';
1
+ import { authApi } from './chunk-2QYFVJEM.js';
2
+ import { decodeJWT, isInitialized, getConfig, getStorage, getDefaultStorage } from './chunk-6NTZVDLW.js';
3
3
 
4
4
  // src/svelte/stores/auth.svelte.ts
5
5
  function getStorageKey() {
@@ -1,4 +1,4 @@
1
- import { authApi } from './chunk-U5E4T4NS.js';
1
+ import { authApi } from './chunk-2QYFVJEM.js';
2
2
 
3
3
  // src/core/guards.ts
4
4
  function isMfaChallengeResponse(response) {
@@ -60,7 +60,7 @@ var AuthService = class {
60
60
  }
61
61
  if (options?.autoSetAuth !== false && !isMfaChallengeResponse(response)) {
62
62
  try {
63
- const { authStore } = await import('./auth.svelte-JXZ7XEE5.js');
63
+ const { authStore } = await import('./auth.svelte-WPCYPHEE.js');
64
64
  authStore.setAuth(
65
65
  response.accessToken,
66
66
  response.refreshToken,
@@ -246,7 +246,7 @@ var AuthService = class {
246
246
  }
247
247
  if (options?.autoSetAuth !== false) {
248
248
  try {
249
- const { authStore } = await import('./auth.svelte-JXZ7XEE5.js');
249
+ const { authStore } = await import('./auth.svelte-WPCYPHEE.js');
250
250
  authStore.setAuth(
251
251
  response.accessToken,
252
252
  response.refreshToken,
@@ -1,6 +1,6 @@
1
- import { authService } from './chunk-GOKAXJJS.js';
2
- import { authStore, currentUser, isAuthenticated, authActions } from './chunk-TUFSFAAI.js';
3
- import { isInitialized, initAuth } from './chunk-EM7PUNZB.js';
1
+ import { authService } from './chunk-MPPV5S7S.js';
2
+ import { authStore, currentUser, isAuthenticated, authActions } from './chunk-CPFM7JEI.js';
3
+ import { isInitialized, initAuth } from './chunk-6NTZVDLW.js';
4
4
 
5
5
  // src/svelte/guards/auth-guard.ts
6
6
  function checkAuth(options = {}) {
@@ -1,6 +1,6 @@
1
- import { L as LoginCredentials, h as LoginResponse, j as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, u as ProfileUpdateData, v as ChangePasswordData, l as Session, m as ApiKey, C as CreateApiKeyRequest, n as CreateApiKeyResponse, o as MFAStatus, M as MFASetupResponse, p as MFAChallengeData, D as Device, q as UserPreferences, s as LinkedAccount, t as SecurityEvent, P as Pagination } from '../types-CKcLRCI_.js';
2
- export { A as AuthConfig, f as AuthState, w as ResetPasswordData, S as SSOConfig, e as StorageAdapter, g as getConfig, b as getDefaultStorage, d as getFetch, c as getStorage, i as initAuth, a as isInitialized, r as resetConfig } from '../types-CKcLRCI_.js';
3
- export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles, c as getAvatarFallback, g as getDisplayName, e as getGreeting, d as getUserEmail, b as getUserInitials, i as isRoleDeniedError } from '../user-utils-Bi3-FHxY.js';
1
+ import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, j as ProfileUpdateData, C as ChangePasswordData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, p as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from '../types-Di6SR1q5.js';
2
+ export { a as AuthConfig, b as AuthState, l as ResetPasswordData, S as SSOConfig, o as StorageAdapter, q as createMemoryStorage, r as createSessionStorage, s as getConfig, t as getDefaultStorage, u as getFetch, v as getStorage, w as initAuth, x as isInitialized, y as resetConfig } from '../types-Di6SR1q5.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-C-JbXqbj.js';
4
4
 
5
5
  /**
6
6
  * HTTP Client
@@ -8,13 +8,17 @@ export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDe
8
8
  * A configurable HTTP client for making API requests.
9
9
  * Handles authentication headers, token refresh, and error handling.
10
10
  */
11
- interface ApiRequestOptions extends Omit<RequestInit, 'body'> {
11
+ interface ApiRequestOptions extends Omit<RequestInit, 'body' | 'signal'> {
12
12
  /** Whether to include auth headers */
13
13
  authenticated?: boolean;
14
14
  /** Custom fetch implementation for this request */
15
15
  customFetch?: typeof fetch;
16
16
  /** Request body (will be JSON stringified) */
17
17
  body?: unknown;
18
+ /** Request timeout in ms (overrides global config.requestTimeout) */
19
+ timeout?: number;
20
+ /** AbortSignal for manual cancellation */
21
+ signal?: AbortSignal;
18
22
  }
19
23
  interface ApiResponse<T = unknown> {
20
24
  data?: T;
@@ -42,6 +46,11 @@ declare function getSessionToken(): string | null;
42
46
  /**
43
47
  * Update stored tokens.
44
48
  * Also updates the Svelte auth store if available.
49
+ *
50
+ * Note: The read-modify-write on localStorage is not atomic across tabs —
51
+ * concurrent refreshes in two tabs are last-write-wins. The `storage` event
52
+ * listener (set up via `initCrossTabSync`) propagates whichever write landed
53
+ * last, so tabs converge on the same tokens.
45
54
  */
46
55
  declare function updateStoredTokens(accessToken: string, refreshToken: string): void;
47
56
  /**
@@ -50,6 +59,10 @@ declare function updateStoredTokens(accessToken: string, refreshToken: string):
50
59
  declare function clearStoredAuth(): void;
51
60
  /**
52
61
  * Helper to extract data from CHAPI response wrapper.
62
+ *
63
+ * WARNING: The final fallback returns `response as T` without runtime
64
+ * validation. Callers should validate the shape of the returned data
65
+ * at trust boundaries (e.g., after network requests).
53
66
  */
54
67
  declare function extractData<T>(response: {
55
68
  data?: {
@@ -304,7 +317,10 @@ declare function getAvailableMethods(response: LoginResponse): string[];
304
317
  * JWT Utilities
305
318
  *
306
319
  * Simple JWT decoder utilities for extracting payload data.
307
- * Note: These functions do NOT verify signatures - they only extract payloads.
320
+ *
321
+ * SECURITY: These functions do NOT verify signatures — they only extract payloads.
322
+ * Permissions and roles decoded from JWTs are for **UI display only**.
323
+ * All authorization decisions MUST be enforced server-side.
308
324
  */
309
325
  interface JWTPayload {
310
326
  /** Subject (usually user ID) */
@@ -323,6 +339,10 @@ interface JWTPayload {
323
339
  /**
324
340
  * Decode a JWT token and extract its payload.
325
341
  *
342
+ * **SECURITY:** No signature verification is performed. The returned claims
343
+ * (including `roles` and `permissions`) are for client-side UI hints only.
344
+ * Never use these claims for authorization without server-side verification.
345
+ *
326
346
  * @param token - The JWT token string
327
347
  * @returns The decoded payload, or null if decoding fails
328
348
  *
@@ -1,3 +1,3 @@
1
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-GOKAXJJS.js';
2
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from '../chunk-U5E4T4NS.js';
3
- export { decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-EM7PUNZB.js';
1
+ export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-MPPV5S7S.js';
2
+ export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from '../chunk-2QYFVJEM.js';
3
+ export { createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-6NTZVDLW.js';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { m as ApiKey, A as AuthConfig, f as AuthState, v as ChangePasswordData, C as CreateApiKeyRequest, n as CreateApiKeyResponse, D as Device, s as LinkedAccount, L as LoginCredentials, h as LoginResponse, j as LogoutResponse, p as MFAChallengeData, M as MFASetupResponse, o as MFAStatus, P as Pagination, u as ProfileUpdateData, R as RegisterData, k as RegisterResponse, w as ResetPasswordData, S as SSOConfig, t as SecurityEvent, l as Session, e as StorageAdapter, U as User, q as UserPreferences, g as getConfig, b as getDefaultStorage, d as getFetch, c as getStorage, i as initAuth, a as isInitialized, r as resetConfig } from './types-CKcLRCI_.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 User, p as UserPreferences, q as createMemoryStorage, r as createSessionStorage, s as getConfig, t as getDefaultStorage, u as getFetch, v as getStorage, w as initAuth, x as isInitialized, y as resetConfig } from './types-Di6SR1q5.js';
2
2
  export { ApiRequestOptions, ApiResponse, JWTPayload, api, apiRequest, authApi, clearStoredAuth, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, 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, c as getAvatarFallback, g as getDisplayName, e as getGreeting, d as getUserEmail, b as getUserInitials, i as isRoleDeniedError } from './user-utils-Bi3-FHxY.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-C-JbXqbj.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-RKHRCHOI.js';
2
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-GOKAXJJS.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-TUFSFAAI.js';
4
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from './chunk-U5E4T4NS.js';
5
- export { decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-EM7PUNZB.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-UG4GSPGL.js';
2
+ export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-MPPV5S7S.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-CPFM7JEI.js';
4
+ export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from './chunk-2QYFVJEM.js';
5
+ export { createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-6NTZVDLW.js';
@@ -1,7 +1,7 @@
1
- import { f as AuthState, U as User, A as AuthConfig } from '../types-CKcLRCI_.js';
2
- export { i as initAuth, a as isInitialized } from '../types-CKcLRCI_.js';
3
- import { a as authService } from '../user-utils-Bi3-FHxY.js';
4
- export { R as RoleDeniedError, f as formatUserRoles, c as getAvatarFallback, g as getDisplayName, e as getGreeting, d as getUserEmail, b as getUserInitials, i as isRoleDeniedError } from '../user-utils-Bi3-FHxY.js';
1
+ import { b as AuthState, U as User, a as AuthConfig } from '../types-Di6SR1q5.js';
2
+ export { w as initAuth, x as isInitialized } from '../types-Di6SR1q5.js';
3
+ import { a as authService } from '../user-utils-C-JbXqbj.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-C-JbXqbj.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-RKHRCHOI.js';
2
- export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-GOKAXJJS.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-TUFSFAAI.js';
4
- import '../chunk-U5E4T4NS.js';
5
- export { initAuth, isInitialized } from '../chunk-EM7PUNZB.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-UG4GSPGL.js';
2
+ export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-MPPV5S7S.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-CPFM7JEI.js';
4
+ import '../chunk-2QYFVJEM.js';
5
+ export { initAuth, isInitialized } from '../chunk-6NTZVDLW.js';
@@ -1,4 +1,4 @@
1
- import { U as User, h as LoginResponse, j as LogoutResponse, k as RegisterResponse, M as MFASetupResponse, o as MFAStatus, l as Session, D as Device, m as ApiKey, s as LinkedAccount, t as SecurityEvent, q as UserPreferences, e as StorageAdapter, f as AuthState, A as AuthConfig } from '../types-CKcLRCI_.js';
1
+ import { U 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, p as UserPreferences, o as StorageAdapter, b as AuthState, a as AuthConfig } from '../types-Di6SR1q5.js';
2
2
 
3
3
  /**
4
4
  * User Fixtures
@@ -1,4 +1,4 @@
1
- import { initAuth, resetConfig, isTokenExpired, decodeJWT } from '../chunk-EM7PUNZB.js';
1
+ import { initAuth, resetConfig, isTokenExpired, decodeJWT } from '../chunk-6NTZVDLW.js';
2
2
 
3
3
  // src/testing/fixtures/users.ts
4
4
  var mockUser = {
@@ -50,6 +50,8 @@ interface AuthConfig {
50
50
  * ```
51
51
  */
52
52
  onSessionExpired?: (currentPath: string) => void;
53
+ /** Request timeout in milliseconds (default: 30000) */
54
+ requestTimeout?: number;
53
55
  /** SSO configuration */
54
56
  sso?: SSOConfig;
55
57
  /**
@@ -103,8 +105,29 @@ declare function resetConfig(): void;
103
105
  /**
104
106
  * Get the default storage adapter.
105
107
  * Returns localStorage in browser, or a no-op adapter in SSR.
108
+ *
109
+ * SECURITY NOTE: localStorage persists tokens (including the long-lived
110
+ * refresh token) where any XSS on the page can read them. It is the default
111
+ * for cross-tab session continuity, but consider the safer alternatives:
112
+ * `createSessionStorage()` (per-tab, cleared on close),
113
+ * `createMemoryStorage()` (never persisted, lost on reload), or an
114
+ * httpOnly-cookie flow on your backend so tokens never reach JS at all.
115
+ * Pass the adapter via `initAuth({ storage: ... })`.
106
116
  */
107
117
  declare function getDefaultStorage(): StorageAdapter;
118
+ /**
119
+ * Storage adapter backed by sessionStorage: tokens live per-tab and are
120
+ * cleared when the tab closes. Safer than localStorage against persistent
121
+ * token theft, at the cost of cross-tab session continuity.
122
+ * Falls back to no-op in SSR.
123
+ */
124
+ declare function createSessionStorage(): StorageAdapter;
125
+ /**
126
+ * In-memory storage adapter: tokens are never persisted and are lost on
127
+ * page reload (the user re-authenticates or a silent SSO flow restores the
128
+ * session). The safest JS-side option against token exfiltration.
129
+ */
130
+ declare function createMemoryStorage(): StorageAdapter;
108
131
  /**
109
132
  * Get the storage adapter from config or use default.
110
133
  */
@@ -172,6 +195,12 @@ interface LogoutResponse {
172
195
  message?: string;
173
196
  ssoLogout?: boolean;
174
197
  logoutUrl?: string;
198
+ /**
199
+ * Set when the server-side logout call failed. `success` stays true so
200
+ * callers clear local state, but the server session may still be alive —
201
+ * check this to warn the user or retry revocation.
202
+ */
203
+ serverError?: string;
175
204
  }
176
205
  interface RegisterResponse {
177
206
  message: string;
@@ -318,4 +347,4 @@ interface ResetPasswordData {
318
347
  newPassword: string;
319
348
  }
320
349
 
321
- export { type AuthConfig as A, type CreateApiKeyRequest as C, type Device as D, type LoginCredentials as L, type MFASetupResponse as M, type Pagination as P, type RegisterData as R, type SSOConfig as S, type User as U, isInitialized as a, getDefaultStorage as b, getStorage as c, getFetch as d, type StorageAdapter as e, type AuthState as f, getConfig as g, type LoginResponse as h, initAuth as i, type LogoutResponse as j, type RegisterResponse as k, type Session as l, type ApiKey as m, type CreateApiKeyResponse as n, type MFAStatus as o, type MFAChallengeData as p, type UserPreferences as q, resetConfig as r, type LinkedAccount as s, type SecurityEvent as t, type ProfileUpdateData as u, type ChangePasswordData as v, type ResetPasswordData as w };
350
+ export { type ApiKey as A, type ChangePasswordData as C, type Device as D, type LinkedAccount as L, type MFAChallengeData as M, type Pagination as P, type RegisterData as R, type SSOConfig as S, type User as U, type AuthConfig as a, type AuthState as b, type CreateApiKeyRequest as c, type CreateApiKeyResponse as d, type LoginCredentials as e, type LoginResponse as f, type LogoutResponse as g, type MFASetupResponse as h, type MFAStatus as i, type ProfileUpdateData as j, type RegisterResponse as k, type ResetPasswordData as l, type SecurityEvent as m, type Session as n, type StorageAdapter as o, type UserPreferences as p, createMemoryStorage as q, createSessionStorage as r, getConfig as s, getDefaultStorage as t, getFetch as u, getStorage as v, initAuth as w, isInitialized as x, resetConfig as y };
@@ -1,4 +1,4 @@
1
- import { L as LoginCredentials, h as LoginResponse, j as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, u as ProfileUpdateData, l as Session, m as ApiKey, C as CreateApiKeyRequest, n as CreateApiKeyResponse, o as MFAStatus, M as MFASetupResponse, p as MFAChallengeData, D as Device, q as UserPreferences, s as LinkedAccount, t as SecurityEvent, P as Pagination } from './types-CKcLRCI_.js';
1
+ import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, U as User, j as ProfileUpdateData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, p as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination } from './types-Di6SR1q5.js';
2
2
 
3
3
  /**
4
4
  * Auth Service
@@ -411,4 +411,4 @@ declare function formatUserRoles(user: User | null | undefined, options?: {
411
411
  capitalize?: boolean;
412
412
  }): string;
413
413
 
414
- export { AuthService as A, type LoginOptions as L, type MFAVerifyOptions as M, RoleDeniedError as R, authService as a, getUserInitials as b, getAvatarFallback as c, getUserEmail as d, getGreeting as e, formatUserRoles as f, getDisplayName as g, isRoleDeniedError as i };
414
+ export { AuthService as A, type LoginOptions as L, type MFAVerifyOptions as M, RoleDeniedError as R, authService as a, getDisplayName as b, getGreeting as c, getUserEmail as d, getUserInitials as e, formatUserRoles as f, getAvatarFallback as g, isRoleDeniedError as i };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/auth",
3
- "version": "0.1.53",
3
+ "version": "0.1.56",
4
4
  "description": "Authentication services and Svelte bindings for Classic Theme apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,8 +31,9 @@
31
31
  "scripts": {
32
32
  "build": "tsup",
33
33
  "dev": "tsup --watch",
34
- "clean": "rm -rf dist",
35
- "typecheck": "tsc --noEmit"
34
+ "clean": "node ../../scripts/rm.mjs dist",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "vitest run"
36
37
  },
37
38
  "keywords": [
38
39
  "authentication",
@@ -1,3 +0,0 @@
1
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-TUFSFAAI.js';
2
- import './chunk-U5E4T4NS.js';
3
- import './chunk-EM7PUNZB.js';