@classic-homes/auth 1.23.0 → 1.28.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
@@ -148,6 +148,27 @@ function handleSSOLogin(redirectUrl: string) {
148
148
  }
149
149
  ```
150
150
 
151
+ `initiateSSOLogin` also sends a random `client_state`, kept in this tab's
152
+ sessionStorage (single-use, 10-minute TTL). The API echoes it in the callback
153
+ hash (`client_state`, in both the token and the MFA-challenge variants); check
154
+ it **before** storing any tokens from the hash, so a crafted callback URL can't
155
+ sign the user into someone else's account (login CSRF):
156
+
157
+ ```typescript
158
+ import { verifySsoClientState } from '@classic-homes/auth/core';
159
+
160
+ const params = new URLSearchParams(window.location.hash.slice(1));
161
+ const { accepted, result } = verifySsoClientState(params); // 'ok' | 'mismatch' | 'missing'
162
+ if (!accepted) {
163
+ window.history.replaceState({}, '', window.location.pathname);
164
+ throw new Error('This sign-in link was not started from this browser tab. Please sign in again.');
165
+ }
166
+ ```
167
+
168
+ Set `sso.requireClientState: true` once the API echoing `client_state` is
169
+ deployed to that environment. Until then (the default) every result is
170
+ accepted, so log `result` to watch the rollout.
171
+
151
172
  ### 5. MFA Challenge Verification
152
173
 
153
174
  ```typescript
@@ -395,6 +416,8 @@ interface AuthConfig {
395
416
  enabled: boolean;
396
417
  provider: string;
397
418
  authorizeUrl?: string;
419
+ /** Reject SSO callbacks whose client_state is missing or wrong (default false) */
420
+ requireClientState?: boolean;
398
421
  };
399
422
 
400
423
  /** Callback when auth errors occur */
@@ -0,0 +1,3 @@
1
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-S3DNV7VV.js';
2
+ import './chunk-GBYHYVXX.js';
3
+ import './chunk-K6TV3ZVS.js';
@@ -1,4 +1,101 @@
1
- import { getConfig, getStorage, isTokenExpired, getFetch, isValidRedirectUrl } from './chunk-2CYGQUUY.js';
1
+ import { isInitialized, getConfig, getUserRoles, getStorage, isTokenExpired, getFetch, isValidRedirectUrl } from './chunk-K6TV3ZVS.js';
2
+
3
+ // src/core/sso-state.ts
4
+ var SSO_CLIENT_STATE_PARAM = "client_state";
5
+ var SSO_CLIENT_STATE_TTL_MS = 10 * 60 * 1e3;
6
+ function storageKey() {
7
+ const prefix = isInitialized() ? getConfig().storageKey ?? "classic_auth" : "classic_auth";
8
+ return `${prefix}_sso_client_state`;
9
+ }
10
+ function sessionStore() {
11
+ try {
12
+ return typeof globalThis.sessionStorage === "undefined" ? null : globalThis.sessionStorage;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function randomUrlSafe(byteLength) {
18
+ const bytes = new Uint8Array(byteLength);
19
+ globalThis.crypto.getRandomValues(bytes);
20
+ let binary = "";
21
+ for (const byte of bytes) binary += String.fromCharCode(byte);
22
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
23
+ }
24
+ function createSsoClientState() {
25
+ const store = sessionStore();
26
+ if (!store) return null;
27
+ const value = randomUrlSafe(32);
28
+ const entry = { value, createdAt: Date.now() };
29
+ try {
30
+ store.setItem(storageKey(), JSON.stringify(entry));
31
+ } catch {
32
+ return null;
33
+ }
34
+ return value;
35
+ }
36
+ function takeStored() {
37
+ const store = sessionStore();
38
+ if (!store) return null;
39
+ const key = storageKey();
40
+ try {
41
+ const raw = store.getItem(key);
42
+ store.removeItem(key);
43
+ if (!raw) return null;
44
+ const parsed = JSON.parse(raw);
45
+ if (typeof parsed?.value !== "string" || typeof parsed.createdAt !== "number") return null;
46
+ return { value: parsed.value, createdAt: parsed.createdAt };
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function timingSafeEqual(a, b) {
52
+ if (a.length !== b.length) return false;
53
+ let diff = 0;
54
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
55
+ return diff === 0;
56
+ }
57
+ function consumeSsoClientState(received) {
58
+ const stored = takeStored();
59
+ if (!received) return "missing";
60
+ if (!stored) return "mismatch";
61
+ if (Date.now() - stored.createdAt > SSO_CLIENT_STATE_TTL_MS) return "mismatch";
62
+ return timingSafeEqual(stored.value, received) ? "ok" : "mismatch";
63
+ }
64
+ function verifySsoClientState(input) {
65
+ const received = input instanceof URLSearchParams ? input.get(SSO_CLIENT_STATE_PARAM) : input;
66
+ const result = consumeSsoClientState(received);
67
+ const required = isInitialized() && getConfig().sso?.requireClientState === true;
68
+ return { result, accepted: result === "ok" || !required };
69
+ }
70
+
71
+ // src/core/errors.ts
72
+ var RoleDeniedError = class _RoleDeniedError extends Error {
73
+ constructor(user, requiredRoles, redirectTo) {
74
+ const userRoles = getUserRoles(user).join(", ") || "none";
75
+ super(
76
+ `User "${user.username}" does not have required role(s). Has: [${userRoles}], Required: [${requiredRoles.join(", ")}]`
77
+ );
78
+ this.name = "RoleDeniedError";
79
+ this.user = user;
80
+ this.requiredRoles = requiredRoles;
81
+ this.redirectTo = redirectTo;
82
+ if (Error.captureStackTrace) {
83
+ Error.captureStackTrace(this, _RoleDeniedError);
84
+ }
85
+ }
86
+ };
87
+ function isRoleDeniedError(error) {
88
+ return error instanceof RoleDeniedError || error?.name === "RoleDeniedError";
89
+ }
90
+ var InvalidAuthResponseError = class _InvalidAuthResponseError extends Error {
91
+ constructor(message = "Unexpected authentication response: no access token or user") {
92
+ super(message);
93
+ this.name = "InvalidAuthResponseError";
94
+ if (Error.captureStackTrace) {
95
+ Error.captureStackTrace(this, _InvalidAuthResponseError);
96
+ }
97
+ }
98
+ };
2
99
 
3
100
  // src/core/client.ts
4
101
  var DEFAULT_REFRESH_RETRY_DELAYS = [1e3, 3e3];
@@ -30,8 +127,8 @@ function getRefreshToken() {
30
127
  }
31
128
  function getSessionTokenKey() {
32
129
  const config = getConfig();
33
- const storageKey = config.storageKey ?? "classic_auth";
34
- return `${storageKey}_session`;
130
+ const storageKey2 = config.storageKey ?? "classic_auth";
131
+ return `${storageKey2}_session`;
35
132
  }
36
133
  function getSessionToken() {
37
134
  const storage = getStorage();
@@ -43,22 +140,22 @@ function updateStoredTokens(accessToken, refreshToken) {
43
140
  function writeStoredTokens(accessToken, refreshToken) {
44
141
  const config = getConfig();
45
142
  const storage = getStorage();
46
- const storageKey = config.storageKey ?? "classic_auth";
143
+ const storageKey2 = config.storageKey ?? "classic_auth";
47
144
  let parsed = {};
48
145
  try {
49
- const authData = storage.getItem(storageKey);
146
+ const authData = storage.getItem(storageKey2);
50
147
  if (authData) parsed = JSON.parse(authData);
51
148
  } catch {
52
149
  }
53
150
  parsed.accessToken = accessToken;
54
151
  parsed.refreshToken = refreshToken;
55
- storage.setItem(storageKey, JSON.stringify(parsed));
152
+ storage.setItem(storageKey2, JSON.stringify(parsed));
56
153
  config.onTokenRefresh?.({ accessToken, refreshToken });
57
154
  return syncSvelteStore((authStore) => authStore.updateTokens(accessToken, refreshToken));
58
155
  }
59
156
  async function syncSvelteStore(apply) {
60
157
  try {
61
- const { authStore } = await import('./auth.svelte-T4LKTTDZ.js');
158
+ const { authStore } = await import('./auth.svelte-JLSRQJAC.js');
62
159
  apply(authStore);
63
160
  } catch {
64
161
  }
@@ -121,8 +218,8 @@ async function refreshOnce(refreshToken) {
121
218
  function withRefreshLock(fn) {
122
219
  const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
123
220
  if (!locks) return fn();
124
- const storageKey = getConfig().storageKey ?? "classic_auth";
125
- return locks.request(`${storageKey}:refresh`, fn);
221
+ const storageKey2 = getConfig().storageKey ?? "classic_auth";
222
+ return locks.request(`${storageKey2}:refresh`, fn);
126
223
  }
127
224
  function refreshSession(options = {}) {
128
225
  if (options.onlyIfExpired) {
@@ -197,14 +294,41 @@ function extractData(response) {
197
294
  }
198
295
  return response;
199
296
  }
297
+ function isApiOrigin(url, baseUrl) {
298
+ try {
299
+ const page = typeof location !== "undefined" ? location.href : void 0;
300
+ return new URL(url).origin === new URL(baseUrl, page).origin;
301
+ } catch {
302
+ return false;
303
+ }
304
+ }
200
305
  function apiRequest(endpoint, options = {}) {
201
306
  return apiRequestInternal(endpoint, options, false);
202
307
  }
203
308
  async function apiRequestInternal(endpoint, options, isRetry) {
204
- const { authenticated = false, customFetch, body, timeout, signal, ...fetchOptions } = options;
309
+ const {
310
+ authenticated: wantsAuth = false,
311
+ customFetch,
312
+ body,
313
+ timeout,
314
+ signal,
315
+ noAuthRetry = false,
316
+ ...fetchOptions
317
+ } = options;
205
318
  const config = getConfig();
206
319
  const fetchFn = customFetch ?? getFetch();
207
- const url = endpoint.startsWith("http") ? endpoint : `${config.baseUrl}${endpoint}`;
320
+ const isAbsolute = /^https?:\/\//i.test(endpoint);
321
+ const url = isAbsolute ? endpoint : `${config.baseUrl}${endpoint}`;
322
+ const authenticated = wantsAuth && (!isAbsolute || isApiOrigin(url, config.baseUrl));
323
+ if (authenticated && noAuthRetry && !isRetry && typeof window !== "undefined") {
324
+ const accessToken = getAccessToken();
325
+ if (accessToken && isTokenExpired(accessToken)) {
326
+ const outcome = await refreshSession({ onlyIfExpired: true });
327
+ if (outcome === "rejected") {
328
+ throw new Error("Authentication failed. Please sign in again.");
329
+ }
330
+ }
331
+ }
208
332
  const timeoutMs = timeout ?? config.requestTimeout ?? 3e4;
209
333
  const controller = new AbortController();
210
334
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -238,7 +362,7 @@ async function apiRequestInternal(endpoint, options, isRetry) {
238
362
  signal: controller.signal
239
363
  });
240
364
  clearTimeout(timeoutId);
241
- if (response.status === 401 && authenticated && typeof window !== "undefined") {
365
+ if (response.status === 401 && authenticated && !noAuthRetry && typeof window !== "undefined") {
242
366
  if (isRetry) {
243
367
  await expireSession();
244
368
  throw new Error("Authentication failed. Please sign in again.");
@@ -312,9 +436,9 @@ function initCrossTabSync() {
312
436
  if (typeof window === "undefined") return () => {
313
437
  };
314
438
  const config = getConfig();
315
- const storageKey = config.storageKey ?? "classic_auth";
439
+ const storageKey2 = config.storageKey ?? "classic_auth";
316
440
  function handleStorage(event) {
317
- if (event.key !== storageKey) return;
441
+ if (event.key !== storageKey2) return;
318
442
  void syncSvelteStore((authStore) => authStore.rehydrate());
319
443
  }
320
444
  window.addEventListener("storage", handleStorage);
@@ -425,6 +549,8 @@ var authApi = {
425
549
  * Initiate SSO login by redirecting to the SSO provider.
426
550
  * @param options.callbackUrl - The URL where the SSO provider should redirect after auth
427
551
  * @param options.redirectUrl - The final URL to redirect to after processing the callback
552
+ * Also sends a fresh `client_state` (kept in this tab's sessionStorage) that
553
+ * the callback page must check with `verifySsoClientState()`.
428
554
  * @throws Error if redirectUrl or callbackUrl fail validation
429
555
  */
430
556
  initiateSSOLogin(options) {
@@ -444,6 +570,10 @@ var authApi = {
444
570
  if (options?.redirectUrl) {
445
571
  params.set("redirect", options.redirectUrl);
446
572
  }
573
+ const clientState = createSsoClientState();
574
+ if (clientState) {
575
+ params.set(SSO_CLIENT_STATE_PARAM, clientState);
576
+ }
447
577
  const url = params.toString() ? `${authorizeUrl}?${params}` : authorizeUrl;
448
578
  window.location.href = url;
449
579
  },
@@ -468,7 +598,12 @@ var authApi = {
468
598
  * Change the current user's password.
469
599
  */
470
600
  async changePassword(data) {
471
- await api.put("/auth/change-password", data, true);
601
+ await apiRequest("/auth/change-password", {
602
+ method: "PUT",
603
+ body: data,
604
+ authenticated: true,
605
+ noAuthRetry: true
606
+ });
472
607
  },
473
608
  // ============================================================================
474
609
  // Email Verification
@@ -570,16 +705,20 @@ var authApi = {
570
705
  * Disable MFA.
571
706
  */
572
707
  async disableMFA(password) {
573
- await api.delete("/auth/mfa", true, void 0, { password });
708
+ await apiRequest("/auth/mfa", {
709
+ method: "DELETE",
710
+ body: { password },
711
+ authenticated: true,
712
+ noAuthRetry: true
713
+ });
574
714
  },
575
715
  /**
576
716
  * Regenerate MFA backup codes.
577
717
  */
578
718
  async regenerateBackupCodes(password) {
579
- const response = await api.post(
719
+ const response = await apiRequest(
580
720
  "/auth/mfa/regenerate-backup-codes",
581
- { password },
582
- true
721
+ { method: "POST", body: { password }, authenticated: true, noAuthRetry: true }
583
722
  );
584
723
  return extractData(response);
585
724
  },
@@ -707,7 +846,12 @@ var authApi = {
707
846
  * Unlink an SSO account.
708
847
  */
709
848
  async unlinkSSOAccount(provider, password) {
710
- await api.delete("/auth/sso/unlink", true, void 0, { provider, password });
849
+ await apiRequest("/auth/sso/unlink", {
850
+ method: "DELETE",
851
+ body: { provider, password },
852
+ authenticated: true,
853
+ noAuthRetry: true
854
+ });
711
855
  },
712
856
  /**
713
857
  * Link an SSO account.
@@ -834,4 +978,4 @@ var authApi = {
834
978
  }
835
979
  };
836
980
 
837
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, refreshSession, updateStoredTokens };
981
+ export { InvalidAuthResponseError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, refreshSession, updateStoredTokens, verifySsoClientState };
@@ -97,13 +97,48 @@ function isValidRedirectUrl(url) {
97
97
  }
98
98
  } catch {
99
99
  if (url.includes("://") || url.startsWith("//")) return false;
100
+ return isSameOriginRelative(url);
101
+ }
102
+ }
103
+ function isSameOriginRelative(url) {
104
+ const sentinel = "https://relative.invalid";
105
+ let current = url;
106
+ for (let round = 0; round < 4; round++) {
107
+ try {
108
+ if (new URL(current, sentinel).origin !== sentinel) return false;
109
+ } catch {
110
+ return false;
111
+ }
112
+ let decoded;
100
113
  try {
101
- const sentinel = "https://relative.invalid";
102
- return new URL(url, sentinel).origin === sentinel;
114
+ decoded = decodeURIComponent(current);
103
115
  } catch {
104
116
  return false;
105
117
  }
118
+ if (decoded === current) return true;
119
+ current = decoded;
106
120
  }
121
+ return false;
122
+ }
123
+ function isValidSSOLogoutUrl(url) {
124
+ if (isValidRedirectUrl(url)) return true;
125
+ const cfg = getConfig();
126
+ let parsed;
127
+ try {
128
+ parsed = new URL(url);
129
+ } catch {
130
+ return false;
131
+ }
132
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
133
+ const origins = [...cfg.sso?.logoutOrigins ?? []];
134
+ if (cfg.sso?.authorizeUrl) origins.push(cfg.sso.authorizeUrl);
135
+ return origins.some((origin) => {
136
+ try {
137
+ return new URL(origin).origin === parsed.origin;
138
+ } catch {
139
+ return false;
140
+ }
141
+ });
107
142
  }
108
143
 
109
144
  // src/core/jwt.ts
@@ -251,4 +286,4 @@ function hasAllRoles(user, roles) {
251
286
  return roles.every((role) => held.includes(role));
252
287
  }
253
288
 
254
- export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, isValidRedirectUrl, resetConfig };
289
+ export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, isValidRedirectUrl, isValidSSOLogoutUrl, resetConfig };
@@ -1,5 +1,5 @@
1
- import { authApi } from './chunk-ETKDIPD2.js';
2
- import { isInitialized, getConfig, hasRole, hasAnyRole, hasAllRoles, decodeJWT, getStorage, getDefaultStorage } from './chunk-2CYGQUUY.js';
1
+ import { InvalidAuthResponseError, authApi } from './chunk-GBYHYVXX.js';
2
+ import { isInitialized, getConfig, isValidSSOLogoutUrl, hasRole, hasAnyRole, hasAllRoles, decodeJWT, getStorage, getDefaultStorage } from './chunk-K6TV3ZVS.js';
3
3
 
4
4
  // src/svelte/stores/auth.svelte.ts
5
5
  var STORAGE_VERSION = 2;
@@ -107,6 +107,9 @@ var AuthStore = class {
107
107
  * Set auth data after successful login.
108
108
  */
109
109
  setAuth(accessToken, refreshToken, user, sessionToken) {
110
+ if (typeof accessToken !== "string" || accessToken === "" || !user) {
111
+ throw new InvalidAuthResponseError("setAuth requires an access token and a user");
112
+ }
110
113
  const userWithPermissions = normalizeUser(user, accessToken);
111
114
  this.state = {
112
115
  accessToken,
@@ -198,7 +201,12 @@ var AuthStore = class {
198
201
  const response = await authApi.logout();
199
202
  this.logout();
200
203
  if (response?.ssoLogout && response?.logoutUrl) {
201
- return { ssoLogoutUrl: response.logoutUrl };
204
+ if (isValidSSOLogoutUrl(response.logoutUrl)) {
205
+ return { ssoLogoutUrl: response.logoutUrl };
206
+ }
207
+ getConfig().onAuthError?.(
208
+ new Error("Ignored SSO logout URL: origin is not allowed (see sso.logoutOrigins)")
209
+ );
202
210
  }
203
211
  } catch {
204
212
  this.logout();
@@ -1,12 +1,12 @@
1
- import { authApi } from './chunk-ETKDIPD2.js';
2
- import { getUserRoles, hasAnyRole } from './chunk-2CYGQUUY.js';
1
+ import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-GBYHYVXX.js';
2
+ import { hasAnyRole, getUserRoles } from './chunk-K6TV3ZVS.js';
3
3
 
4
4
  // src/core/guards.ts
5
5
  function isMfaChallengeResponse(response) {
6
6
  return "requiresMFA" in response && response.requiresMFA === true || "mfaRequired" in response && response.mfaRequired === true;
7
7
  }
8
8
  function isLoginSuccessResponse(response) {
9
- return "accessToken" in response && "user" in response && !isMfaChallengeResponse(response);
9
+ return typeof response?.accessToken === "string" && response.accessToken !== "" && typeof response.user === "object" && response.user !== null && !isMfaChallengeResponse(response);
10
10
  }
11
11
  function getMfaToken(response) {
12
12
  return response.mfaToken ?? response.mfaChallengeToken;
@@ -15,27 +15,16 @@ function getAvailableMethods(response) {
15
15
  return response.availableMethods ?? ["totp"];
16
16
  }
17
17
 
18
- // src/core/errors.ts
19
- var RoleDeniedError = class _RoleDeniedError extends Error {
20
- constructor(user, requiredRoles, redirectTo) {
21
- const userRoles = getUserRoles(user).join(", ") || "none";
22
- super(
23
- `User "${user.username}" does not have required role(s). Has: [${userRoles}], Required: [${requiredRoles.join(", ")}]`
24
- );
25
- this.name = "RoleDeniedError";
26
- this.user = user;
27
- this.requiredRoles = requiredRoles;
28
- this.redirectTo = redirectTo;
29
- if (Error.captureStackTrace) {
30
- Error.captureStackTrace(this, _RoleDeniedError);
31
- }
32
- }
33
- };
34
- function isRoleDeniedError(error) {
35
- return error instanceof RoleDeniedError || error?.name === "RoleDeniedError";
36
- }
37
-
38
18
  // src/core/service.ts
19
+ async function setStoreAuth(response) {
20
+ let store;
21
+ try {
22
+ store = (await import('./auth.svelte-JLSRQJAC.js')).authStore;
23
+ } catch {
24
+ return;
25
+ }
26
+ store.setAuth(response.accessToken, response.refreshToken, response.user, response.sessionToken);
27
+ }
39
28
  var AuthService = class {
40
29
  // ============================================================================
41
30
  // Authentication
@@ -48,7 +37,13 @@ var AuthService = class {
48
37
  */
49
38
  async login(credentials, options) {
50
39
  const response = await authApi.login(credentials);
51
- if (options?.allowedRoles?.length && !isMfaChallengeResponse(response)) {
40
+ if (isMfaChallengeResponse(response)) {
41
+ return response;
42
+ }
43
+ if (!isLoginSuccessResponse(response)) {
44
+ throw new InvalidAuthResponseError();
45
+ }
46
+ if (options?.allowedRoles?.length) {
52
47
  if (!hasAnyRole(response.user, options.allowedRoles)) {
53
48
  throw new RoleDeniedError(
54
49
  response.user,
@@ -57,17 +52,8 @@ var AuthService = class {
57
52
  );
58
53
  }
59
54
  }
60
- if (options?.autoSetAuth !== false && !isMfaChallengeResponse(response)) {
61
- try {
62
- const { authStore } = await import('./auth.svelte-T4LKTTDZ.js');
63
- authStore.setAuth(
64
- response.accessToken,
65
- response.refreshToken,
66
- response.user,
67
- response.sessionToken
68
- );
69
- } catch {
70
- }
55
+ if (options?.autoSetAuth !== false) {
56
+ await setStoreAuth(response);
71
57
  }
72
58
  return response;
73
59
  }
@@ -234,6 +220,9 @@ var AuthService = class {
234
220
  */
235
221
  async verifyMFAChallenge(data, options) {
236
222
  const response = await authApi.verifyMFAChallenge(data);
223
+ if (!isLoginSuccessResponse(response)) {
224
+ throw new InvalidAuthResponseError();
225
+ }
237
226
  if (options?.allowedRoles?.length) {
238
227
  if (!hasAnyRole(response.user, options.allowedRoles)) {
239
228
  throw new RoleDeniedError(
@@ -244,16 +233,7 @@ var AuthService = class {
244
233
  }
245
234
  }
246
235
  if (options?.autoSetAuth !== false) {
247
- try {
248
- const { authStore } = await import('./auth.svelte-T4LKTTDZ.js');
249
- authStore.setAuth(
250
- response.accessToken,
251
- response.refreshToken,
252
- response.user,
253
- response.sessionToken
254
- );
255
- } catch {
256
- }
236
+ await setStoreAuth(response);
257
237
  }
258
238
  return response;
259
239
  }
@@ -407,4 +387,4 @@ function formatUserRoles(user, options = {}) {
407
387
  return formatted.join(separator);
408
388
  }
409
389
 
410
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError };
390
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse };
@@ -1,6 +1,6 @@
1
- import { authService } from './chunk-DFCBWKCU.js';
2
- import { authStore, currentUser, isAuthenticated, authActions } from './chunk-KSOYQYUZ.js';
3
- import { isInitialized, initAuth, getUserRoles } from './chunk-2CYGQUUY.js';
1
+ import { authService } from './chunk-TUWTOMZZ.js';
2
+ import { authStore, currentUser, isAuthenticated, authActions } from './chunk-S3DNV7VV.js';
3
+ import { isInitialized, initAuth, getUserRoles } from './chunk-K6TV3ZVS.js';
4
4
 
5
5
  // src/svelte/guards/auth-guard.ts
6
6
  function checkAuth(options = {}) {
@@ -175,7 +175,12 @@ function createAuthHook(options = {}) {
175
175
  event,
176
176
  resolve
177
177
  }) => {
178
- const { pathname } = event.url;
178
+ let pathname;
179
+ try {
180
+ pathname = decodePathname(event.url.pathname);
181
+ } catch {
182
+ return new Response("Malformed URI", { status: 400 });
183
+ }
179
184
  const isLoginPage = pathname.replace(/(.)\/+$/, "$1") === loginPathname;
180
185
  const isPublic = isLoginPage || publicRoutes.some((r) => r.test(pathname));
181
186
  if (isPublic) {
@@ -198,7 +203,7 @@ function createAuthHook(options = {}) {
198
203
  return onUnauthenticated(event);
199
204
  }
200
205
  const loginUrl = new URL(loginPath, event.url.origin);
201
- loginUrl.searchParams.set(redirectParam, pathname + event.url.search);
206
+ loginUrl.searchParams.set(redirectParam, event.url.pathname + event.url.search);
202
207
  return new Response(null, {
203
208
  status: 302,
204
209
  headers: { Location: loginUrl.pathname + loginUrl.search }
@@ -207,6 +212,9 @@ function createAuthHook(options = {}) {
207
212
  return resolve(event);
208
213
  };
209
214
  }
215
+ function decodePathname(pathname) {
216
+ return pathname.split("%25").map(decodeURI).join("%25");
217
+ }
210
218
  function matchesRoute(pathname, patterns) {
211
219
  return patterns.some((pattern) => pattern.test(pathname));
212
220
  }
@@ -1,6 +1,69 @@
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';
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-B70Kgcc_.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-B70Kgcc_.js';
3
+ export { A as AuthService, I as InvalidAuthResponseError, 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-DWEU-Ovm.js';
4
+
5
+ /**
6
+ * SSO client state
7
+ *
8
+ * Binds an SSO callback to the browser tab that started the login. Before
9
+ * redirecting to `/auth/sso/authorize`, the tab generates a random
10
+ * `client_state`, keeps it in sessionStorage (per-tab, single-use, 10-minute
11
+ * TTL) and sends it along; the server echoes it back in the callback hash
12
+ * fragment (token and MFA-challenge variants alike). The callback page then
13
+ * calls `verifySsoClientState()` before trusting anything in the hash.
14
+ *
15
+ * Without this, an attacker could complete SSO for their own account and send
16
+ * the victim the resulting callback URL, silently signing the victim in as the
17
+ * attacker (login CSRF / session fixation).
18
+ */
19
+ /** Hash-fragment (and authorize query) parameter carrying the client state. */
20
+ declare const SSO_CLIENT_STATE_PARAM = "client_state";
21
+ /** How long a stored client state stays valid. */
22
+ declare const SSO_CLIENT_STATE_TTL_MS: number;
23
+ /**
24
+ * Result of checking a callback's `client_state` against the one this tab stored:
25
+ * - `ok`: present and matches an unexpired stored value
26
+ * - `mismatch`: present but this tab stored none, it expired, or it differs
27
+ * - `missing`: the callback carried no `client_state`
28
+ */
29
+ type SsoClientStateResult = 'ok' | 'mismatch' | 'missing';
30
+ interface SsoClientStateVerification {
31
+ result: SsoClientStateResult;
32
+ /**
33
+ * Whether the callback may proceed: always for `ok`; for `missing` and
34
+ * `mismatch` only while `sso.requireClientState` is off (rollout mode).
35
+ */
36
+ accepted: boolean;
37
+ }
38
+ /**
39
+ * Generate a fresh client state and store it for this tab, replacing any
40
+ * earlier one. Returns `null` when sessionStorage is unavailable — sending a
41
+ * value the callback can't check would only produce a mismatch.
42
+ */
43
+ declare function createSsoClientState(): string | null;
44
+ /**
45
+ * Compare the `client_state` an SSO callback received with the one this tab
46
+ * stored, and consume the stored value (single-use) whatever the outcome.
47
+ * This is the raw check; `verifySsoClientState()` applies the
48
+ * `sso.requireClientState` policy on top.
49
+ */
50
+ declare function consumeSsoClientState(received: string | null | undefined): SsoClientStateResult;
51
+ /**
52
+ * Verify an SSO callback's `client_state` and decide whether to proceed.
53
+ * Accepts the received value, or the callback's parsed hash params.
54
+ *
55
+ * With `sso.requireClientState: true`, anything but `ok` is rejected. While it
56
+ * is off (the default, so apps can ship before the API echoes the value),
57
+ * every result is accepted — log `result` to watch the rollout.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const params = new URLSearchParams(window.location.hash.slice(1));
62
+ * const { accepted, result } = verifySsoClientState(params);
63
+ * if (!accepted) throw new Error(`SSO sign-in could not be verified (${result})`);
64
+ * ```
65
+ */
66
+ declare function verifySsoClientState(input: string | URLSearchParams | null | undefined): SsoClientStateVerification;
4
67
 
5
68
  /**
6
69
  * HTTP Client
@@ -19,6 +82,14 @@ interface ApiRequestOptions extends Omit<RequestInit, 'body' | 'signal'> {
19
82
  timeout?: number;
20
83
  /** AbortSignal for manual cancellation */
21
84
  signal?: AbortSignal;
85
+ /**
86
+ * Treat a 401 as an ordinary request error: no token refresh, no retry
87
+ * and no session expiry. For endpoints where a 401 means a wrong password
88
+ * rather than a dead token (change password, disable MFA, ...), so a typo
89
+ * does not sign the user out. An already-expired access token is still
90
+ * refreshed before the request is sent.
91
+ */
92
+ noAuthRetry?: boolean;
22
93
  }
23
94
  /**
24
95
  * The CHAPI envelope shape, for endpoints that wrap their payload in `data`.
@@ -186,6 +257,8 @@ declare const authApi: {
186
257
  * Initiate SSO login by redirecting to the SSO provider.
187
258
  * @param options.callbackUrl - The URL where the SSO provider should redirect after auth
188
259
  * @param options.redirectUrl - The final URL to redirect to after processing the callback
260
+ * Also sends a fresh `client_state` (kept in this tab's sessionStorage) that
261
+ * the callback page must check with `verifySsoClientState()`.
189
262
  * @throws Error if redirectUrl or callbackUrl fail validation
190
263
  */
191
264
  initiateSSOLogin(options?: {
@@ -389,7 +462,9 @@ declare const authApi: {
389
462
  declare function isMfaChallengeResponse(response: LoginResponse): boolean;
390
463
  /**
391
464
  * Check if a login response is a successful login (with tokens and user).
392
- * Returns true if the response contains valid authentication data.
465
+ * Returns true only for a non-empty access token plus a user object, so an
466
+ * MFA challenge in a shape isMfaChallengeResponse doesn't recognise is never
467
+ * mistaken for a login.
393
468
  */
394
469
  declare function isLoginSuccessResponse(response: LoginResponse): boolean;
395
470
  /**
@@ -621,4 +696,4 @@ declare function hasAnyRole(user: Pick<User, 'roles'> | null | undefined, roles:
621
696
  */
622
697
  declare function hasAllRoles(user: Pick<User, 'roles'> | null | undefined, roles: readonly string[]): boolean;
623
698
 
624
- 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 };
699
+ 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, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SecurityEvent, Session, type SsoClientStateResult, type SsoClientStateVerification, UpdateApiKeyRequest, User, UserPreferences, UserRolesResponse, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, consumeSsoClientState, createSsoClientState, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, refreshSession, updateStoredTokens, verifySsoClientState };
@@ -1,3 +1,3 @@
1
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from '../chunk-DFCBWKCU.js';
2
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, refreshSession, updateStoredTokens } from '../chunk-ETKDIPD2.js';
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-2CYGQUUY.js';
1
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from '../chunk-TUWTOMZZ.js';
2
+ export { InvalidAuthResponseError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, refreshSession, updateStoredTokens, verifySsoClientState } from '../chunk-GBYHYVXX.js';
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-K6TV3ZVS.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-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';
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-B70Kgcc_.js';
2
+ export { ApiRequestOptions, ApiResponse, JWTPayload, ROLE_HIERARCHY, ROLE_NAMES, RefreshOutcome, RefreshSessionOptions, RoleName, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SsoClientStateResult, SsoClientStateVerification, api, apiRequest, authApi, clearStoredAuth, computePrimaryRole, consumeSsoClientState, createSsoClientState, decodeJWT, extractClaims, extractData, getAccessToken, getAvailableMethods, getEffectiveLevel, getMfaToken, getRefreshToken, getSessionToken, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initCrossTabSync, isLoginSuccessResponse, isMfaChallengeResponse, isTokenExpired, refreshSession, updateStoredTokens, verifySsoClientState } from './core/index.js';
3
+ export { A as AuthService, I as InvalidAuthResponseError, 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-DWEU-Ovm.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-NLG6NSE4.js';
2
- export { AuthService, RoleDeniedError, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse, isRoleDeniedError } from './chunk-DFCBWKCU.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-KSOYQYUZ.js';
4
- export { api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, refreshSession, updateStoredTokens } from './chunk-ETKDIPD2.js';
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-2CYGQUUY.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-XUOSWSDO.js';
2
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-TUWTOMZZ.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-S3DNV7VV.js';
4
+ export { InvalidAuthResponseError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, refreshSession, updateStoredTokens, verifySsoClientState } from './chunk-GBYHYVXX.js';
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-K6TV3ZVS.js';
@@ -1,7 +1,7 @@
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';
1
+ import { b as AuthState, p as User, a as AuthConfig } from '../types-B70Kgcc_.js';
2
+ export { y as initAuth, z as isInitialized } from '../types-B70Kgcc_.js';
3
+ import { a as authService } from '../user-utils-DWEU-Ovm.js';
4
+ export { I as InvalidAuthResponseError, 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-DWEU-Ovm.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-NLG6NSE4.js';
2
- export { RoleDeniedError, formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials, isRoleDeniedError } from '../chunk-DFCBWKCU.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-KSOYQYUZ.js';
4
- import '../chunk-ETKDIPD2.js';
5
- export { initAuth, isInitialized } from '../chunk-2CYGQUUY.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-XUOSWSDO.js';
2
+ export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-TUWTOMZZ.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-S3DNV7VV.js';
4
+ export { InvalidAuthResponseError, RoleDeniedError, isRoleDeniedError } from '../chunk-GBYHYVXX.js';
5
+ export { initAuth, isInitialized } from '../chunk-K6TV3ZVS.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-Bqh2gkvu.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-B70Kgcc_.js';
2
2
 
3
3
  /**
4
4
  * User Fixtures
@@ -1,4 +1,4 @@
1
- import { hasRole, hasAnyRole, hasAllRoles, initAuth, resetConfig, getUserRoles, isTokenExpired, decodeJWT } from '../chunk-2CYGQUUY.js';
1
+ import { hasRole, hasAnyRole, hasAllRoles, initAuth, resetConfig, getUserRoles, isTokenExpired, decodeJWT } from '../chunk-K6TV3ZVS.js';
2
2
 
3
3
  // src/testing/fixtures/users.ts
4
4
  var mockUser = {
@@ -11,6 +11,21 @@ interface SSOConfig {
11
11
  provider: string;
12
12
  /** Override the default authorize URL (defaults to /auth/sso/authorize) */
13
13
  authorizeUrl?: string;
14
+ /**
15
+ * Origins the server's SSO logout URL may point to (the identity
16
+ * provider's, e.g. 'https://sso.example.com'). The authorizeUrl origin and
17
+ * the redirect origins accepted by isValidRedirectUrl are always allowed;
18
+ * a logout URL anywhere else is dropped.
19
+ */
20
+ logoutOrigins?: string[];
21
+ /**
22
+ * Reject SSO callbacks whose `client_state` is missing or doesn't match the
23
+ * one this tab sent to the authorize endpoint (see `verifySsoClientState`).
24
+ * Default `false` for one release so apps can roll out before the API
25
+ * echoes `client_state`; turn it on once the API is deployed to that
26
+ * environment. It becomes the default in the next major.
27
+ */
28
+ requireClientState?: boolean;
14
29
  }
15
30
  interface StorageAdapter {
16
31
  getItem(key: string): string | null;
@@ -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-Bqh2gkvu.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-B70Kgcc_.js';
2
2
 
3
3
  /**
4
4
  * Auth Service
@@ -309,6 +309,16 @@ declare class RoleDeniedError extends Error {
309
309
  * Type guard to check if an error is a RoleDeniedError.
310
310
  */
311
311
  declare function isRoleDeniedError(error: unknown): error is RoleDeniedError;
312
+ /**
313
+ * Error thrown when a login or MFA verification response is neither a
314
+ * recognised MFA challenge nor a success (an access token and a user).
315
+ * The auth state is left untouched, so an unexpected server shape fails
316
+ * closed instead of signing the user in without a token.
317
+ */
318
+ declare class InvalidAuthResponseError extends Error {
319
+ readonly name = "InvalidAuthResponseError";
320
+ constructor(message?: string);
321
+ }
312
322
 
313
323
  /**
314
324
  * User Display Utilities
@@ -413,4 +423,4 @@ declare function formatUserRoles(user: User | null | undefined, options?: {
413
423
  capitalize?: boolean;
414
424
  }): string;
415
425
 
416
- 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 };
426
+ export { AuthService as A, InvalidAuthResponseError as I, 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": "1.23.0",
3
+ "version": "1.28.0",
4
4
  "description": "Authentication services and Svelte bindings for Classic Theme apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,3 +0,0 @@
1
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-KSOYQYUZ.js';
2
- import './chunk-ETKDIPD2.js';
3
- import './chunk-2CYGQUUY.js';