@classic-homes/auth 1.28.0 → 1.42.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
@@ -96,6 +96,24 @@ async function handleLogin(email: string, password: string) {
96
96
  }
97
97
  ```
98
98
 
99
+ Email-MFA accounts get their code by email: send it with the challenge token,
100
+ then verify with `method: 'email'`. A cooldown (one code a minute, five an
101
+ hour) throws `MFAChallengeEmailError` with `retryAfterSeconds`.
102
+
103
+ ```typescript
104
+ import { authService, MFAChallengeEmailError } from '@classic-homes/auth';
105
+
106
+ try {
107
+ const { email } = await authService.sendMFAChallengeEmail(mfaToken); // "jo***@example.com"
108
+ } catch (error) {
109
+ if (error instanceof MFAChallengeEmailError && error.retryAfterSeconds) {
110
+ // Show "Resend in Ns"
111
+ }
112
+ }
113
+
114
+ await authService.verifyMFAChallenge({ mfaToken, code, method: 'email' });
115
+ ```
116
+
99
117
  To disable auto-set auth (for manual control):
100
118
 
101
119
  ```typescript
@@ -1,3 +1,3 @@
1
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-S3DNV7VV.js';
2
- import './chunk-GBYHYVXX.js';
1
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-MBVOLE5U.js';
2
+ import './chunk-7AQYLLTA.js';
3
3
  import './chunk-K6TV3ZVS.js';
@@ -1,4 +1,4 @@
1
- import { isInitialized, getConfig, getUserRoles, getStorage, isTokenExpired, getFetch, isValidRedirectUrl } from './chunk-K6TV3ZVS.js';
1
+ import { isInitialized, getConfig, getUserRoles, getStorage, isTokenExpired, getFetch, isValidRedirectUrl, decodeJWT } from './chunk-K6TV3ZVS.js';
2
2
 
3
3
  // src/core/sso-state.ts
4
4
  var SSO_CLIENT_STATE_PARAM = "client_state";
@@ -96,6 +96,69 @@ var InvalidAuthResponseError = class _InvalidAuthResponseError extends Error {
96
96
  }
97
97
  }
98
98
  };
99
+ var SessionChangedError = class _SessionChangedError extends Error {
100
+ constructor(message = "The session changed while this request was in flight; it was not retried.") {
101
+ super(message);
102
+ this.name = "SessionChangedError";
103
+ if (Error.captureStackTrace) {
104
+ Error.captureStackTrace(this, _SessionChangedError);
105
+ }
106
+ }
107
+ };
108
+ function isSessionChangedError(error) {
109
+ return error instanceof SessionChangedError || error?.name === "SessionChangedError";
110
+ }
111
+ var MFAChallengeEmailError = class _MFAChallengeEmailError extends Error {
112
+ constructor(message, status, retryAfterSeconds) {
113
+ super(message);
114
+ this.name = "MFAChallengeEmailError";
115
+ this.status = status;
116
+ this.retryAfterSeconds = retryAfterSeconds;
117
+ if (Error.captureStackTrace) {
118
+ Error.captureStackTrace(this, _MFAChallengeEmailError);
119
+ }
120
+ }
121
+ };
122
+
123
+ // src/core/session.ts
124
+ var generation = 0;
125
+ function bumpSessionGeneration() {
126
+ generation += 1;
127
+ }
128
+ function readIdentity() {
129
+ let parsed = {};
130
+ try {
131
+ const raw = getStorage().getItem(getConfig().storageKey ?? "classic_auth");
132
+ if (raw) parsed = JSON.parse(raw) ?? {};
133
+ } catch {
134
+ }
135
+ const accessToken = typeof parsed.accessToken === "string" ? parsed.accessToken : "";
136
+ const refreshToken = typeof parsed.refreshToken === "string" ? parsed.refreshToken : "";
137
+ const sub = accessToken ? decodeJWT(accessToken)?.sub : void 0;
138
+ const userId = parsed.user?.id;
139
+ return {
140
+ signedIn: accessToken !== "" || refreshToken !== "",
141
+ sub: typeof sub === "string" || typeof sub === "number" ? String(sub) : void 0,
142
+ userId: typeof userId === "string" || typeof userId === "number" ? String(userId) : void 0
143
+ };
144
+ }
145
+ function captureSession() {
146
+ return { generation, identity: readIdentity() };
147
+ }
148
+ function getSessionGeneration() {
149
+ return generation;
150
+ }
151
+ function isCurrentSession(snapshot) {
152
+ if (snapshot.generation !== generation) return false;
153
+ const then = snapshot.identity;
154
+ const now = readIdentity();
155
+ if (then.signedIn !== now.signedIn) return false;
156
+ if (then.sub !== void 0 && now.sub !== void 0 && then.sub !== now.sub) return false;
157
+ if (then.userId !== void 0 && now.userId !== void 0 && then.userId !== now.userId) {
158
+ return false;
159
+ }
160
+ return true;
161
+ }
99
162
 
100
163
  // src/core/client.ts
101
164
  var DEFAULT_REFRESH_RETRY_DELAYS = [1e3, 3e3];
@@ -155,7 +218,7 @@ function writeStoredTokens(accessToken, refreshToken) {
155
218
  }
156
219
  async function syncSvelteStore(apply) {
157
220
  try {
158
- const { authStore } = await import('./auth.svelte-JLSRQJAC.js');
221
+ const { authStore } = await import('./auth.svelte-XY4FQFZQ.js');
159
222
  apply(authStore);
160
223
  } catch {
161
224
  }
@@ -165,6 +228,7 @@ function clearStoredAuth() {
165
228
  const storage = getStorage();
166
229
  storage.removeItem(config.storageKey ?? "classic_auth");
167
230
  storage.removeItem(getSessionTokenKey());
231
+ bumpSessionGeneration();
168
232
  }
169
233
  async function expireSession() {
170
234
  clearStoredAuth();
@@ -234,6 +298,8 @@ function refreshSession(options = {}) {
234
298
  return refreshInflight;
235
299
  }
236
300
  async function runRefresh(options) {
301
+ const generation2 = getSessionGeneration();
302
+ const sameGeneration = () => generation2 === getSessionGeneration();
237
303
  const presented = getRefreshToken();
238
304
  if (!presented) {
239
305
  await expireSession();
@@ -243,6 +309,7 @@ async function runRefresh(options) {
243
309
  const current = getRefreshToken();
244
310
  if (current !== presented) {
245
311
  if (!current) {
312
+ if (!sameGeneration()) return "rejected";
246
313
  await expireSession();
247
314
  return "rejected";
248
315
  }
@@ -267,6 +334,7 @@ async function runRefresh(options) {
267
334
  await syncSvelteStore((authStore) => authStore.rehydrate());
268
335
  return "refreshed";
269
336
  }
337
+ if (!sameGeneration()) return "rejected";
270
338
  await writeStoredTokens(attempt.accessToken, attempt.refreshToken);
271
339
  return "refreshed";
272
340
  }
@@ -276,6 +344,7 @@ async function runRefresh(options) {
276
344
  await syncSvelteStore((authStore) => authStore.rehydrate());
277
345
  return "refreshed";
278
346
  }
347
+ if (!sameGeneration()) return "rejected";
279
348
  await expireSession();
280
349
  return "rejected";
281
350
  }
@@ -305,7 +374,7 @@ function isApiOrigin(url, baseUrl) {
305
374
  function apiRequest(endpoint, options = {}) {
306
375
  return apiRequestInternal(endpoint, options, false);
307
376
  }
308
- async function apiRequestInternal(endpoint, options, isRetry) {
377
+ async function apiRequestInternal(endpoint, options, isRetry, startedIn) {
309
378
  const {
310
379
  authenticated: wantsAuth = false,
311
380
  customFetch,
@@ -320,6 +389,14 @@ async function apiRequestInternal(endpoint, options, isRetry) {
320
389
  const isAbsolute = /^https?:\/\//i.test(endpoint);
321
390
  const url = isAbsolute ? endpoint : `${config.baseUrl}${endpoint}`;
322
391
  const authenticated = wantsAuth && (!isAbsolute || isApiOrigin(url, config.baseUrl));
392
+ const session = startedIn ?? captureSession();
393
+ const assertSameSession = async () => {
394
+ if (isCurrentSession(session)) return;
395
+ if (session.generation === getSessionGeneration()) {
396
+ await syncSvelteStore((authStore) => authStore.rehydrate());
397
+ }
398
+ throw new SessionChangedError();
399
+ };
323
400
  if (authenticated && noAuthRetry && !isRetry && typeof window !== "undefined") {
324
401
  const accessToken = getAccessToken();
325
402
  if (accessToken && isTokenExpired(accessToken)) {
@@ -327,6 +404,7 @@ async function apiRequestInternal(endpoint, options, isRetry) {
327
404
  if (outcome === "rejected") {
328
405
  throw new Error("Authentication failed. Please sign in again.");
329
406
  }
407
+ if (authenticated) await assertSameSession();
330
408
  }
331
409
  }
332
410
  const timeoutMs = timeout ?? config.requestTimeout ?? 3e4;
@@ -363,13 +441,15 @@ async function apiRequestInternal(endpoint, options, isRetry) {
363
441
  });
364
442
  clearTimeout(timeoutId);
365
443
  if (response.status === 401 && authenticated && !noAuthRetry && typeof window !== "undefined") {
444
+ await assertSameSession();
366
445
  if (isRetry) {
367
446
  await expireSession();
368
447
  throw new Error("Authentication failed. Please sign in again.");
369
448
  }
370
449
  const outcome = await refreshSession();
371
450
  if (outcome === "refreshed") {
372
- return apiRequestInternal(endpoint, options, true);
451
+ await assertSameSession();
452
+ return apiRequestInternal(endpoint, options, true, session);
373
453
  }
374
454
  if (outcome === "rejected") {
375
455
  throw new Error("Authentication failed. Please sign in again.");
@@ -729,7 +809,9 @@ var authApi = {
729
809
  const config = getConfig();
730
810
  const requestBody = {
731
811
  code: data.code,
732
- type: data.method === "backup" ? "backup" : "totp",
812
+ // CHAPI answers the challenge per method: an authenticator code, an
813
+ // emailed sign-in code or a backup code (no method → authenticator).
814
+ type: data.method === "backup" || data.method === "email" ? data.method : "totp",
733
815
  trustDevice: data.trustDevice
734
816
  };
735
817
  const response = await fetchWithTimeout(`${config.baseUrl}/auth/mfa/challenge`, {
@@ -747,6 +829,27 @@ var authApi = {
747
829
  const result = await response.json();
748
830
  return extractData(result);
749
831
  },
832
+ /**
833
+ * Email a sign-in code for an MFA challenge (accounts with email MFA).
834
+ * Authenticated by the challenge token from login / the SSO callback, not
835
+ * a session. Answer it with verifyMFAChallenge({ method: 'email' }).
836
+ * Throws MFAChallengeEmailError (with `retryAfterSeconds` on a cooldown).
837
+ */
838
+ async sendMFAChallengeEmail(mfaToken) {
839
+ const config = getConfig();
840
+ const response = await fetchWithTimeout(`${config.baseUrl}/auth/mfa/challenge/email/send`, {
841
+ method: "POST",
842
+ headers: { Authorization: `Bearer ${mfaToken}` }
843
+ });
844
+ const body = await response.json().catch(() => ({}));
845
+ if (!response.ok) {
846
+ const details = body?.error?.details;
847
+ const retryAfter = typeof details?.retryAfterSeconds === "number" ? details.retryAfterSeconds : typeof body?.retryAfterSeconds === "number" ? body.retryAfterSeconds : void 0;
848
+ const message = (typeof body?.error === "string" ? body.error : body?.error?.message) || body?.message || "Could not send the verification code";
849
+ throw new MFAChallengeEmailError(message, response.status, retryAfter);
850
+ }
851
+ return extractData(body);
852
+ },
750
853
  // ============================================================================
751
854
  // Device Management
752
855
  // ============================================================================
@@ -978,4 +1081,4 @@ var authApi = {
978
1081
  }
979
1082
  };
980
1083
 
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 };
1084
+ export { InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, bumpSessionGeneration, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState };
@@ -1,5 +1,5 @@
1
- import { authService } from './chunk-TUWTOMZZ.js';
2
- import { authStore, currentUser, isAuthenticated, authActions } from './chunk-S3DNV7VV.js';
1
+ import { authService } from './chunk-LPQKZRPC.js';
2
+ import { authStore, currentUser, isAuthenticated, authActions } from './chunk-MBVOLE5U.js';
3
3
  import { isInitialized, initAuth, getUserRoles } from './chunk-K6TV3ZVS.js';
4
4
 
5
5
  // src/svelte/guards/auth-guard.ts
@@ -1,4 +1,4 @@
1
- import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-GBYHYVXX.js';
1
+ import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-7AQYLLTA.js';
2
2
  import { hasAnyRole, getUserRoles } from './chunk-K6TV3ZVS.js';
3
3
 
4
4
  // src/core/guards.ts
@@ -19,7 +19,7 @@ function getAvailableMethods(response) {
19
19
  async function setStoreAuth(response) {
20
20
  let store;
21
21
  try {
22
- store = (await import('./auth.svelte-JLSRQJAC.js')).authStore;
22
+ store = (await import('./auth.svelte-XY4FQFZQ.js')).authStore;
23
23
  } catch {
24
24
  return;
25
25
  }
@@ -237,6 +237,15 @@ var AuthService = class {
237
237
  }
238
238
  return response;
239
239
  }
240
+ /**
241
+ * Email a sign-in code for an MFA challenge (accounts with email MFA).
242
+ * Pass the `mfaToken` from the login / SSO challenge; answer with
243
+ * `verifyMFAChallenge({ mfaToken, code, method: 'email' })`.
244
+ * Throws MFAChallengeEmailError — `retryAfterSeconds` is set on a cooldown.
245
+ */
246
+ async sendMFAChallengeEmail(mfaToken) {
247
+ return await authApi.sendMFAChallengeEmail(mfaToken);
248
+ }
240
249
  // ============================================================================
241
250
  // Device Management
242
251
  // ============================================================================
@@ -1,4 +1,4 @@
1
- import { InvalidAuthResponseError, authApi } from './chunk-GBYHYVXX.js';
1
+ import { InvalidAuthResponseError, bumpSessionGeneration, authApi } from './chunk-7AQYLLTA.js';
2
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
@@ -111,6 +111,7 @@ var AuthStore = class {
111
111
  throw new InvalidAuthResponseError("setAuth requires an access token and a user");
112
112
  }
113
113
  const userWithPermissions = normalizeUser(user, accessToken);
114
+ bumpSessionGeneration();
114
115
  this.state = {
115
116
  accessToken,
116
117
  refreshToken,
@@ -174,6 +175,7 @@ var AuthStore = class {
174
175
  * Clear auth state (logout).
175
176
  */
176
177
  logout() {
178
+ bumpSessionGeneration();
177
179
  this.state = {
178
180
  accessToken: null,
179
181
  refreshToken: null,
@@ -222,6 +224,7 @@ var AuthStore = class {
222
224
  */
223
225
  handleSessionExpired() {
224
226
  const currentPath = typeof window !== "undefined" ? window.location.pathname : "/";
227
+ bumpSessionGeneration();
225
228
  this.state = {
226
229
  accessToken: null,
227
230
  refreshToken: null,
@@ -303,6 +306,7 @@ var AuthStore = class {
303
306
  * ```
304
307
  */
305
308
  reset() {
309
+ bumpSessionGeneration();
306
310
  this.state = {
307
311
  accessToken: null,
308
312
  refreshToken: null,
@@ -1,6 +1,6 @@
1
- import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, k as RegisterResponse, p as User, j as ProfileUpdateData, C as ChangePasswordData, n as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, i as MFAStatus, h as MFASetupResponse, M as MFAChallengeData, D as Device, q as UserPreferences, L as LinkedAccount, m as SecurityEvent, P as Pagination, r as UserRolesResponse } from '../types-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';
1
+ import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, l as RegisterResponse, q as User, k as ProfileUpdateData, C as ChangePasswordData, o as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, j as MFAStatus, i as MFASetupResponse, M as MFAChallengeData, h as MFAChallengeEmailResponse, D as Device, r as UserPreferences, L as LinkedAccount, n as SecurityEvent, P as Pagination, s as UserRolesResponse } from '../types-DF5Ny2IA.js';
2
+ export { a as AuthConfig, b as AuthState, m as ResetPasswordData, S as SSOConfig, p as StorageAdapter, t as createMemoryStorage, u as createSessionStorage, v as getConfig, w as getDefaultStorage, x as getFetch, y as getStorage, z as initAuth, B as isInitialized, E as resetConfig } from '../types-DF5Ny2IA.js';
3
+ export { A as AuthService, I as InvalidAuthResponseError, L as LoginOptions, M as MFAChallengeEmailError, a as MFAVerifyOptions, R as RoleDeniedError, S as SessionChangedError, b as authService, f as formatUserRoles, g as getAvatarFallback, c as getDisplayName, d as getGreeting, e as getUserEmail, h as getUserInitials, i as isRoleDeniedError, j as isSessionChangedError } from '../user-utils-Bo3lMsuT.js';
4
4
 
5
5
  /**
6
6
  * SSO client state
@@ -347,6 +347,13 @@ declare const authApi: {
347
347
  * Verify MFA challenge during login.
348
348
  */
349
349
  verifyMFAChallenge(data: MFAChallengeData): Promise<LoginResponse>;
350
+ /**
351
+ * Email a sign-in code for an MFA challenge (accounts with email MFA).
352
+ * Authenticated by the challenge token from login / the SSO callback, not
353
+ * a session. Answer it with verifyMFAChallenge({ method: 'email' }).
354
+ * Throws MFAChallengeEmailError (with `retryAfterSeconds` on a cooldown).
355
+ */
356
+ sendMFAChallengeEmail(mfaToken: string): Promise<MFAChallengeEmailResponse>;
350
357
  /**
351
358
  * Get all devices.
352
359
  */
@@ -696,4 +703,4 @@ declare function hasAnyRole(user: Pick<User, 'roles'> | null | undefined, roles:
696
703
  */
697
704
  declare function hasAllRoles(user: Pick<User, 'roles'> | null | undefined, roles: readonly string[]): boolean;
698
705
 
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 };
706
+ export { ApiKey, type ApiRequestOptions, type ApiResponse, ChangePasswordData, CreateApiKeyRequest, CreateApiKeyResponse, Device, type JWTPayload, LinkedAccount, LoginCredentials, LoginResponse, LogoutResponse, MFAChallengeData, MFAChallengeEmailResponse, 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, 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';
1
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from '../chunk-LPQKZRPC.js';
2
+ export { InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState } from '../chunk-7AQYLLTA.js';
3
3
  export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from '../chunk-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-B70Kgcc_.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 MFAChallengeEmailResponse, i as MFASetupResponse, j as MFAStatus, P as Pagination, k as ProfileUpdateData, R as RegisterData, l as RegisterResponse, m as ResetPasswordData, S as SSOConfig, n as SecurityEvent, o as Session, p as StorageAdapter, U as UpdateApiKeyRequest, q as User, r as UserPreferences, s as UserRolesResponse, t as createMemoryStorage, u as createSessionStorage, v as getConfig, w as getDefaultStorage, x as getFetch, y as getStorage, z as initAuth, B as isInitialized, E as resetConfig } from './types-DF5Ny2IA.js';
2
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';
3
+ export { A as AuthService, I as InvalidAuthResponseError, L as LoginOptions, M as MFAChallengeEmailError, a as MFAVerifyOptions, R as RoleDeniedError, S as SessionChangedError, b as authService, f as formatUserRoles, g as getAvatarFallback, c as getDisplayName, d as getGreeting, e as getUserEmail, h as getUserInitials, i as isRoleDeniedError, j as isSessionChangedError } from './user-utils-Bo3lMsuT.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-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';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-7JLCFRUU.js';
2
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-LPQKZRPC.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-MBVOLE5U.js';
4
+ export { InvalidAuthResponseError, MFAChallengeEmailError, RoleDeniedError, SSO_CLIENT_STATE_PARAM, SSO_CLIENT_STATE_TTL_MS, SessionChangedError, api, apiRequest, authApi, clearStoredAuth, consumeSsoClientState, createSsoClientState, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, isSessionChangedError, refreshSession, updateStoredTokens, verifySsoClientState } from './chunk-7AQYLLTA.js';
5
5
  export { ROLE_HIERARCHY, ROLE_NAMES, computePrimaryRole, createMemoryStorage, createSessionStorage, decodeJWT, extractClaims, getConfig, getDefaultStorage, getEffectiveLevel, getFetch, getStorage, getTokenExpiration, getTokenRemainingTime, getUserRoles, hasAllRoles, hasAnyRole, hasRole, initAuth, isInitialized, isTokenExpired, resetConfig } from './chunk-K6TV3ZVS.js';
@@ -1,7 +1,7 @@
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';
1
+ import { b as AuthState, q as User, a as AuthConfig } from '../types-DF5Ny2IA.js';
2
+ export { z as initAuth, B as isInitialized } from '../types-DF5Ny2IA.js';
3
+ import { b as authService } from '../user-utils-Bo3lMsuT.js';
4
+ export { I as InvalidAuthResponseError, R as RoleDeniedError, S as SessionChangedError, f as formatUserRoles, g as getAvatarFallback, c as getDisplayName, d as getGreeting, e as getUserEmail, h as getUserInitials, i as isRoleDeniedError, j as isSessionChangedError } from '../user-utils-Bo3lMsuT.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-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';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-7JLCFRUU.js';
2
+ export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-LPQKZRPC.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-MBVOLE5U.js';
4
+ export { InvalidAuthResponseError, RoleDeniedError, SessionChangedError, isRoleDeniedError, isSessionChangedError } from '../chunk-7AQYLLTA.js';
5
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-B70Kgcc_.js';
1
+ import { q as User, D as Device, f as LoginResponse, n as SecurityEvent, o as Session, A as ApiKey, L as LinkedAccount, g as LogoutResponse, i as MFASetupResponse, j as MFAStatus, l as RegisterResponse, r as UserPreferences, p as StorageAdapter, b as AuthState, a as AuthConfig } from '../types-DF5Ny2IA.js';
2
2
 
3
3
  /**
4
4
  * User Fixtures
@@ -1065,6 +1065,11 @@ var MockFetchInstance = class {
1065
1065
  path: "/auth/mfa/challenge",
1066
1066
  response: { data: mockLoginSuccess }
1067
1067
  });
1068
+ this.addRoute({
1069
+ method: "POST",
1070
+ path: "/auth/mfa/challenge/email/send",
1071
+ response: { success: true, email: "te***@example.com" }
1072
+ });
1068
1073
  this.addRoute({
1069
1074
  method: "DELETE",
1070
1075
  path: "/auth/mfa",
@@ -326,6 +326,12 @@ interface MFAChallengeData {
326
326
  method?: 'totp' | 'email' | 'backup';
327
327
  trustDevice?: boolean;
328
328
  }
329
+ /** CHAPI's answer to sending an MFA challenge email code. */
330
+ interface MFAChallengeEmailResponse {
331
+ success: boolean;
332
+ /** The address the code went to, masked (`jo***@example.com`). */
333
+ email: string;
334
+ }
329
335
  interface Device {
330
336
  id: string;
331
337
  deviceFingerprint: string;
@@ -407,4 +413,4 @@ interface ResetPasswordData {
407
413
  newPassword: string;
408
414
  }
409
415
 
410
- export { type ApiKey as A, resetConfig as B, 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 UpdateApiKeyRequest 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 User as p, type UserPreferences as q, type UserRolesResponse as r, createMemoryStorage as s, createSessionStorage as t, getConfig as u, getDefaultStorage as v, getFetch as w, getStorage as x, initAuth as y, isInitialized as z };
416
+ export { type ApiKey as A, isInitialized as B, type ChangePasswordData as C, type Device as D, resetConfig as E, type LinkedAccount as L, type MFAChallengeData as M, type Pagination as P, type RegisterData as R, type SSOConfig as S, type UpdateApiKeyRequest 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 MFAChallengeEmailResponse as h, type MFASetupResponse as i, type MFAStatus as j, type ProfileUpdateData as k, type RegisterResponse as l, type ResetPasswordData as m, type SecurityEvent as n, type Session as o, type StorageAdapter as p, type User as q, type UserPreferences as r, type UserRolesResponse as s, createMemoryStorage as t, createSessionStorage as u, getConfig as v, getDefaultStorage as w, getFetch as x, getStorage as y, initAuth as z };
@@ -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-B70Kgcc_.js';
1
+ import { e as LoginCredentials, f as LoginResponse, g as LogoutResponse, R as RegisterData, l as RegisterResponse, q as User, k as ProfileUpdateData, o as Session, A as ApiKey, c as CreateApiKeyRequest, d as CreateApiKeyResponse, U as UpdateApiKeyRequest, j as MFAStatus, i as MFASetupResponse, M as MFAChallengeData, h as MFAChallengeEmailResponse, D as Device, r as UserPreferences, L as LinkedAccount, n as SecurityEvent, P as Pagination } from './types-DF5Ny2IA.js';
2
2
 
3
3
  /**
4
4
  * Auth Service
@@ -192,6 +192,13 @@ declare class AuthService {
192
192
  * @param options - Optional settings for verification behavior
193
193
  */
194
194
  verifyMFAChallenge(data: MFAChallengeData, options?: MFAVerifyOptions): Promise<LoginResponse>;
195
+ /**
196
+ * Email a sign-in code for an MFA challenge (accounts with email MFA).
197
+ * Pass the `mfaToken` from the login / SSO challenge; answer with
198
+ * `verifyMFAChallenge({ mfaToken, code, method: 'email' })`.
199
+ * Throws MFAChallengeEmailError — `retryAfterSeconds` is set on a cooldown.
200
+ */
201
+ sendMFAChallengeEmail(mfaToken: string): Promise<MFAChallengeEmailResponse>;
195
202
  /**
196
203
  * Get all devices.
197
204
  */
@@ -319,6 +326,36 @@ declare class InvalidAuthResponseError extends Error {
319
326
  readonly name = "InvalidAuthResponseError";
320
327
  constructor(message?: string);
321
328
  }
329
+ /**
330
+ * Error thrown when the session changed (logout, login as another account,
331
+ * expiry) while a request was in flight, and the request needed the session
332
+ * to continue (a 401 that would have been refreshed and retried). The request
333
+ * is not refreshed, retried or allowed to expire the new session: replaying it
334
+ * would send the previous account's request with the next account's
335
+ * credentials.
336
+ */
337
+ declare class SessionChangedError extends Error {
338
+ readonly name = "SessionChangedError";
339
+ constructor(message?: string);
340
+ }
341
+ /**
342
+ * Type guard to check if an error is a SessionChangedError.
343
+ */
344
+ declare function isSessionChangedError(error: unknown): error is SessionChangedError;
345
+ /**
346
+ * Error thrown when CHAPI won't send an MFA challenge email code: the
347
+ * challenge token is invalid or expired (401), the account has no email MFA
348
+ * (400), or a code was requested too soon / too often (429). On a 429,
349
+ * `retryAfterSeconds` says how long to wait when the server gave one.
350
+ */
351
+ declare class MFAChallengeEmailError extends Error {
352
+ readonly name = "MFAChallengeEmailError";
353
+ /** HTTP status CHAPI answered with. */
354
+ readonly status: number;
355
+ /** Seconds until another code may be requested (cooldown 429s only). */
356
+ readonly retryAfterSeconds?: number;
357
+ constructor(message: string, status: number, retryAfterSeconds?: number);
358
+ }
322
359
 
323
360
  /**
324
361
  * User Display Utilities
@@ -423,4 +460,4 @@ declare function formatUserRoles(user: User | null | undefined, options?: {
423
460
  capitalize?: boolean;
424
461
  }): string;
425
462
 
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 };
463
+ export { AuthService as A, InvalidAuthResponseError as I, type LoginOptions as L, MFAChallengeEmailError as M, RoleDeniedError as R, SessionChangedError as S, type MFAVerifyOptions as a, authService as b, getDisplayName as c, getGreeting as d, getUserEmail as e, formatUserRoles as f, getAvatarFallback as g, getUserInitials as h, isRoleDeniedError as i, isSessionChangedError as j };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/auth",
3
- "version": "1.28.0",
3
+ "version": "1.42.0",
4
4
  "description": "Authentication services and Svelte bindings for Classic Theme apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",