@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 +6 -0
- package/dist/auth.svelte-WPCYPHEE.js +3 -0
- package/dist/{chunk-U5E4T4NS.js → chunk-2QYFVJEM.js} +100 -28
- package/dist/{chunk-EM7PUNZB.js → chunk-6NTZVDLW.js} +31 -2
- package/dist/{chunk-TUFSFAAI.js → chunk-CPFM7JEI.js} +2 -2
- package/dist/{chunk-GOKAXJJS.js → chunk-MPPV5S7S.js} +3 -3
- package/dist/{chunk-RKHRCHOI.js → chunk-UG4GSPGL.js} +3 -3
- package/dist/core/index.d.ts +25 -5
- package/dist/core/index.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/svelte/index.d.ts +4 -4
- package/dist/svelte/index.js +5 -5
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/{types-CKcLRCI_.d.ts → types-Di6SR1q5.d.ts} +30 -1
- package/dist/{user-utils-Bi3-FHxY.d.ts → user-utils-C-JbXqbj.d.ts} +2 -2
- package/package.json +4 -3
- package/dist/auth.svelte-JXZ7XEE5.js +0 -3
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,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { getConfig, getStorage, getFetch, isValidRedirectUrl } from './chunk-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
163
|
-
|
|
197
|
+
let refreshed = false;
|
|
198
|
+
try {
|
|
199
|
+
refreshed = await refreshAccessToken();
|
|
200
|
+
} finally {
|
|
201
|
+
isRefreshing = false;
|
|
202
|
+
}
|
|
164
203
|
if (refreshed) {
|
|
165
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
2
|
-
import { decodeJWT, isInitialized, getConfig, getStorage, getDefaultStorage } from './chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
import { authStore, currentUser, isAuthenticated, authActions } from './chunk-
|
|
3
|
-
import { isInitialized, initAuth } from './chunk-
|
|
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 = {}) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
export { A as AuthService, L as LoginOptions, M as MFAVerifyOptions, R as RoleDeniedError, a as authService, f as formatUserRoles,
|
|
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
|
-
*
|
|
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
|
*
|
package/dist/core/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-
|
|
2
|
-
export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from '../chunk-
|
|
3
|
-
export { decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-
|
|
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 {
|
|
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,
|
|
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-
|
|
2
|
-
export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-
|
|
3
|
-
export { authActions, authStore, currentUser, isAuthenticated } from './chunk-
|
|
4
|
-
export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, updateStoredTokens } from './chunk-
|
|
5
|
-
export { decodeJWT, extractClaims, getConfig, getDefaultStorage, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-
|
|
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';
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
import { a as authService } from '../user-utils-
|
|
4
|
-
export { R as RoleDeniedError, f as formatUserRoles,
|
|
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
|
package/dist/svelte/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-
|
|
2
|
-
export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-
|
|
3
|
-
export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-
|
|
4
|
-
import '../chunk-
|
|
5
|
-
export { initAuth, isInitialized } from '../chunk-
|
|
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';
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as User,
|
|
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
|
package/dist/testing/index.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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,
|
|
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.
|
|
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
|
|
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",
|