@classic-homes/auth 1.27.0 → 1.41.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
@@ -148,6 +166,27 @@ function handleSSOLogin(redirectUrl: string) {
148
166
  }
149
167
  ```
150
168
 
169
+ `initiateSSOLogin` also sends a random `client_state`, kept in this tab's
170
+ sessionStorage (single-use, 10-minute TTL). The API echoes it in the callback
171
+ hash (`client_state`, in both the token and the MFA-challenge variants); check
172
+ it **before** storing any tokens from the hash, so a crafted callback URL can't
173
+ sign the user into someone else's account (login CSRF):
174
+
175
+ ```typescript
176
+ import { verifySsoClientState } from '@classic-homes/auth/core';
177
+
178
+ const params = new URLSearchParams(window.location.hash.slice(1));
179
+ const { accepted, result } = verifySsoClientState(params); // 'ok' | 'mismatch' | 'missing'
180
+ if (!accepted) {
181
+ window.history.replaceState({}, '', window.location.pathname);
182
+ throw new Error('This sign-in link was not started from this browser tab. Please sign in again.');
183
+ }
184
+ ```
185
+
186
+ Set `sso.requireClientState: true` once the API echoing `client_state` is
187
+ deployed to that environment. Until then (the default) every result is
188
+ accepted, so log `result` to watch the rollout.
189
+
151
190
  ### 5. MFA Challenge Verification
152
191
 
153
192
  ```typescript
@@ -395,6 +434,8 @@ interface AuthConfig {
395
434
  enabled: boolean;
396
435
  provider: string;
397
436
  authorizeUrl?: string;
437
+ /** Reject SSO callbacks whose client_state is missing or wrong (default false) */
438
+ requireClientState?: boolean;
398
439
  };
399
440
 
400
441
  /** Callback when auth errors occur */
@@ -1,3 +1,3 @@
1
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-POOONCPP.js';
2
- import './chunk-FEKQXUPJ.js';
1
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-HMZRBRIU.js';
2
+ import './chunk-QLEEAHJJ.js';
3
3
  import './chunk-K6TV3ZVS.js';
@@ -1,5 +1,5 @@
1
- import { authService } from './chunk-C2UTC5B7.js';
2
- import { authStore, currentUser, isAuthenticated, authActions } from './chunk-POOONCPP.js';
1
+ import { authService } from './chunk-BMCGFHVM.js';
2
+ import { authStore, currentUser, isAuthenticated, authActions } from './chunk-HMZRBRIU.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-FEKQXUPJ.js';
1
+ import { authApi, InvalidAuthResponseError, RoleDeniedError } from './chunk-QLEEAHJJ.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-WROD55FW.js')).authStore;
22
+ store = (await import('./auth.svelte-DSE2NZMP.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-FEKQXUPJ.js';
1
+ import { InvalidAuthResponseError, authApi } from './chunk-QLEEAHJJ.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
@@ -1,4 +1,72 @@
1
- import { getUserRoles, getConfig, getStorage, isTokenExpired, getFetch, isValidRedirectUrl } from './chunk-K6TV3ZVS.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
+ }
2
70
 
3
71
  // src/core/errors.ts
4
72
  var RoleDeniedError = class _RoleDeniedError extends Error {
@@ -28,6 +96,17 @@ var InvalidAuthResponseError = class _InvalidAuthResponseError extends Error {
28
96
  }
29
97
  }
30
98
  };
99
+ var MFAChallengeEmailError = class _MFAChallengeEmailError extends Error {
100
+ constructor(message, status, retryAfterSeconds) {
101
+ super(message);
102
+ this.name = "MFAChallengeEmailError";
103
+ this.status = status;
104
+ this.retryAfterSeconds = retryAfterSeconds;
105
+ if (Error.captureStackTrace) {
106
+ Error.captureStackTrace(this, _MFAChallengeEmailError);
107
+ }
108
+ }
109
+ };
31
110
 
32
111
  // src/core/client.ts
33
112
  var DEFAULT_REFRESH_RETRY_DELAYS = [1e3, 3e3];
@@ -59,8 +138,8 @@ function getRefreshToken() {
59
138
  }
60
139
  function getSessionTokenKey() {
61
140
  const config = getConfig();
62
- const storageKey = config.storageKey ?? "classic_auth";
63
- return `${storageKey}_session`;
141
+ const storageKey2 = config.storageKey ?? "classic_auth";
142
+ return `${storageKey2}_session`;
64
143
  }
65
144
  function getSessionToken() {
66
145
  const storage = getStorage();
@@ -72,22 +151,22 @@ function updateStoredTokens(accessToken, refreshToken) {
72
151
  function writeStoredTokens(accessToken, refreshToken) {
73
152
  const config = getConfig();
74
153
  const storage = getStorage();
75
- const storageKey = config.storageKey ?? "classic_auth";
154
+ const storageKey2 = config.storageKey ?? "classic_auth";
76
155
  let parsed = {};
77
156
  try {
78
- const authData = storage.getItem(storageKey);
157
+ const authData = storage.getItem(storageKey2);
79
158
  if (authData) parsed = JSON.parse(authData);
80
159
  } catch {
81
160
  }
82
161
  parsed.accessToken = accessToken;
83
162
  parsed.refreshToken = refreshToken;
84
- storage.setItem(storageKey, JSON.stringify(parsed));
163
+ storage.setItem(storageKey2, JSON.stringify(parsed));
85
164
  config.onTokenRefresh?.({ accessToken, refreshToken });
86
165
  return syncSvelteStore((authStore) => authStore.updateTokens(accessToken, refreshToken));
87
166
  }
88
167
  async function syncSvelteStore(apply) {
89
168
  try {
90
- const { authStore } = await import('./auth.svelte-WROD55FW.js');
169
+ const { authStore } = await import('./auth.svelte-DSE2NZMP.js');
91
170
  apply(authStore);
92
171
  } catch {
93
172
  }
@@ -150,8 +229,8 @@ async function refreshOnce(refreshToken) {
150
229
  function withRefreshLock(fn) {
151
230
  const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
152
231
  if (!locks) return fn();
153
- const storageKey = getConfig().storageKey ?? "classic_auth";
154
- return locks.request(`${storageKey}:refresh`, fn);
232
+ const storageKey2 = getConfig().storageKey ?? "classic_auth";
233
+ return locks.request(`${storageKey2}:refresh`, fn);
155
234
  }
156
235
  function refreshSession(options = {}) {
157
236
  if (options.onlyIfExpired) {
@@ -368,9 +447,9 @@ function initCrossTabSync() {
368
447
  if (typeof window === "undefined") return () => {
369
448
  };
370
449
  const config = getConfig();
371
- const storageKey = config.storageKey ?? "classic_auth";
450
+ const storageKey2 = config.storageKey ?? "classic_auth";
372
451
  function handleStorage(event) {
373
- if (event.key !== storageKey) return;
452
+ if (event.key !== storageKey2) return;
374
453
  void syncSvelteStore((authStore) => authStore.rehydrate());
375
454
  }
376
455
  window.addEventListener("storage", handleStorage);
@@ -481,6 +560,8 @@ var authApi = {
481
560
  * Initiate SSO login by redirecting to the SSO provider.
482
561
  * @param options.callbackUrl - The URL where the SSO provider should redirect after auth
483
562
  * @param options.redirectUrl - The final URL to redirect to after processing the callback
563
+ * Also sends a fresh `client_state` (kept in this tab's sessionStorage) that
564
+ * the callback page must check with `verifySsoClientState()`.
484
565
  * @throws Error if redirectUrl or callbackUrl fail validation
485
566
  */
486
567
  initiateSSOLogin(options) {
@@ -500,6 +581,10 @@ var authApi = {
500
581
  if (options?.redirectUrl) {
501
582
  params.set("redirect", options.redirectUrl);
502
583
  }
584
+ const clientState = createSsoClientState();
585
+ if (clientState) {
586
+ params.set(SSO_CLIENT_STATE_PARAM, clientState);
587
+ }
503
588
  const url = params.toString() ? `${authorizeUrl}?${params}` : authorizeUrl;
504
589
  window.location.href = url;
505
590
  },
@@ -655,7 +740,9 @@ var authApi = {
655
740
  const config = getConfig();
656
741
  const requestBody = {
657
742
  code: data.code,
658
- type: data.method === "backup" ? "backup" : "totp",
743
+ // CHAPI answers the challenge per method: an authenticator code, an
744
+ // emailed sign-in code or a backup code (no method → authenticator).
745
+ type: data.method === "backup" || data.method === "email" ? data.method : "totp",
659
746
  trustDevice: data.trustDevice
660
747
  };
661
748
  const response = await fetchWithTimeout(`${config.baseUrl}/auth/mfa/challenge`, {
@@ -673,6 +760,27 @@ var authApi = {
673
760
  const result = await response.json();
674
761
  return extractData(result);
675
762
  },
763
+ /**
764
+ * Email a sign-in code for an MFA challenge (accounts with email MFA).
765
+ * Authenticated by the challenge token from login / the SSO callback, not
766
+ * a session. Answer it with verifyMFAChallenge({ method: 'email' }).
767
+ * Throws MFAChallengeEmailError (with `retryAfterSeconds` on a cooldown).
768
+ */
769
+ async sendMFAChallengeEmail(mfaToken) {
770
+ const config = getConfig();
771
+ const response = await fetchWithTimeout(`${config.baseUrl}/auth/mfa/challenge/email/send`, {
772
+ method: "POST",
773
+ headers: { Authorization: `Bearer ${mfaToken}` }
774
+ });
775
+ const body = await response.json().catch(() => ({}));
776
+ if (!response.ok) {
777
+ const details = body?.error?.details;
778
+ const retryAfter = typeof details?.retryAfterSeconds === "number" ? details.retryAfterSeconds : typeof body?.retryAfterSeconds === "number" ? body.retryAfterSeconds : void 0;
779
+ const message = (typeof body?.error === "string" ? body.error : body?.error?.message) || body?.message || "Could not send the verification code";
780
+ throw new MFAChallengeEmailError(message, response.status, retryAfter);
781
+ }
782
+ return extractData(body);
783
+ },
676
784
  // ============================================================================
677
785
  // Device Management
678
786
  // ============================================================================
@@ -904,4 +1012,4 @@ var authApi = {
904
1012
  }
905
1013
  };
906
1014
 
907
- export { InvalidAuthResponseError, RoleDeniedError, api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, refreshSession, updateStoredTokens };
1015
+ export { InvalidAuthResponseError, MFAChallengeEmailError, 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 };
@@ -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-D_5ze47z.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-D_5ze47z.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-Tc6yv4-Q.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, b as authService, f as formatUserRoles, g as getAvatarFallback, c as getDisplayName, d as getGreeting, e as getUserEmail, h as getUserInitials, i as isRoleDeniedError } from '../user-utils-B6BNJzF2.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
@@ -194,6 +257,8 @@ declare const authApi: {
194
257
  * Initiate SSO login by redirecting to the SSO provider.
195
258
  * @param options.callbackUrl - The URL where the SSO provider should redirect after auth
196
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()`.
197
262
  * @throws Error if redirectUrl or callbackUrl fail validation
198
263
  */
199
264
  initiateSSOLogin(options?: {
@@ -282,6 +347,13 @@ declare const authApi: {
282
347
  * Verify MFA challenge during login.
283
348
  */
284
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>;
285
357
  /**
286
358
  * Get all devices.
287
359
  */
@@ -631,4 +703,4 @@ declare function hasAnyRole(user: Pick<User, 'roles'> | null | undefined, roles:
631
703
  */
632
704
  declare function hasAllRoles(user: Pick<User, 'roles'> | null | undefined, roles: readonly string[]): boolean;
633
705
 
634
- 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 };
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-C2UTC5B7.js';
2
- export { InvalidAuthResponseError, RoleDeniedError, api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, refreshSession, updateStoredTokens } from '../chunk-FEKQXUPJ.js';
1
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from '../chunk-BMCGFHVM.js';
2
+ export { InvalidAuthResponseError, MFAChallengeEmailError, 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-QLEEAHJJ.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-D_5ze47z.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, 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-Tc6yv4-Q.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
+ 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 MFAChallengeEmailError, a as MFAVerifyOptions, R as RoleDeniedError, b as authService, f as formatUserRoles, g as getAvatarFallback, c as getDisplayName, d as getGreeting, e as getUserEmail, h as getUserInitials, i as isRoleDeniedError } from './user-utils-B6BNJzF2.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-JFWZUQGP.js';
2
- export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-C2UTC5B7.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from './chunk-POOONCPP.js';
4
- export { InvalidAuthResponseError, RoleDeniedError, api, apiRequest, authApi, clearStoredAuth, extractData, getAccessToken, getRefreshToken, getSessionToken, initCrossTabSync, isRoleDeniedError, refreshSession, updateStoredTokens } from './chunk-FEKQXUPJ.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from './chunk-5SZT7GKG.js';
2
+ export { AuthService, authService, formatUserRoles, getAvailableMethods, getAvatarFallback, getDisplayName, getGreeting, getMfaToken, getUserEmail, getUserInitials, isLoginSuccessResponse, isMfaChallengeResponse } from './chunk-BMCGFHVM.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from './chunk-HMZRBRIU.js';
4
+ export { InvalidAuthResponseError, MFAChallengeEmailError, 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-QLEEAHJJ.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-D_5ze47z.js';
2
- export { y as initAuth, z as isInitialized } from '../types-D_5ze47z.js';
3
- import { a as authService } from '../user-utils-Tc6yv4-Q.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-Tc6yv4-Q.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-B6BNJzF2.js';
4
+ export { I as InvalidAuthResponseError, R as RoleDeniedError, f as formatUserRoles, g as getAvatarFallback, c as getDisplayName, d as getGreeting, e as getUserEmail, h as getUserInitials, i as isRoleDeniedError } from '../user-utils-B6BNJzF2.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-JFWZUQGP.js';
2
- export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-C2UTC5B7.js';
3
- export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-POOONCPP.js';
4
- export { InvalidAuthResponseError, RoleDeniedError, isRoleDeniedError } from '../chunk-FEKQXUPJ.js';
1
+ export { canAccess, checkAuth, createAuthClient, createAuthGuard, createAuthHook, createNavFilter, filterByAccess, filterNavSections, matchesRoute, protectedLoad, requireAuth, requirePermission, requireRole, routePatterns } from '../chunk-5SZT7GKG.js';
2
+ export { formatUserRoles, getAvatarFallback, getDisplayName, getGreeting, getUserEmail, getUserInitials } from '../chunk-BMCGFHVM.js';
3
+ export { authActions, authStore, currentUser, isAuthenticated } from '../chunk-HMZRBRIU.js';
4
+ export { InvalidAuthResponseError, RoleDeniedError, isRoleDeniedError } from '../chunk-QLEEAHJJ.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-D_5ze47z.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",
@@ -18,6 +18,14 @@ interface SSOConfig {
18
18
  * a logout URL anywhere else is dropped.
19
19
  */
20
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;
21
29
  }
22
30
  interface StorageAdapter {
23
31
  getItem(key: string): string | null;
@@ -318,6 +326,12 @@ interface MFAChallengeData {
318
326
  method?: 'totp' | 'email' | 'backup';
319
327
  trustDevice?: boolean;
320
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
+ }
321
335
  interface Device {
322
336
  id: string;
323
337
  deviceFingerprint: string;
@@ -399,4 +413,4 @@ interface ResetPasswordData {
399
413
  newPassword: string;
400
414
  }
401
415
 
402
- 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-D_5ze47z.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,20 @@ declare class InvalidAuthResponseError extends Error {
319
326
  readonly name = "InvalidAuthResponseError";
320
327
  constructor(message?: string);
321
328
  }
329
+ /**
330
+ * Error thrown when CHAPI won't send an MFA challenge email code: the
331
+ * challenge token is invalid or expired (401), the account has no email MFA
332
+ * (400), or a code was requested too soon / too often (429). On a 429,
333
+ * `retryAfterSeconds` says how long to wait when the server gave one.
334
+ */
335
+ declare class MFAChallengeEmailError extends Error {
336
+ readonly name = "MFAChallengeEmailError";
337
+ /** HTTP status CHAPI answered with. */
338
+ readonly status: number;
339
+ /** Seconds until another code may be requested (cooldown 429s only). */
340
+ readonly retryAfterSeconds?: number;
341
+ constructor(message: string, status: number, retryAfterSeconds?: number);
342
+ }
322
343
 
323
344
  /**
324
345
  * User Display Utilities
@@ -423,4 +444,4 @@ declare function formatUserRoles(user: User | null | undefined, options?: {
423
444
  capitalize?: boolean;
424
445
  }): string;
425
446
 
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 };
447
+ export { AuthService as A, InvalidAuthResponseError as I, type LoginOptions as L, MFAChallengeEmailError as M, RoleDeniedError as R, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/auth",
3
- "version": "1.27.0",
3
+ "version": "1.41.0",
4
4
  "description": "Authentication services and Svelte bindings for Classic Theme apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",