@dloizides/auth-client 1.0.0 → 2.0.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/dist/index.d.mts CHANGED
@@ -1,111 +1,5 @@
1
- /**
2
- * Configuration for {@link AuthClient}.
3
- *
4
- * `realm` and `clientId` are **required** and never have sensible portfolio-wide
5
- * defaults. Each consumer (Questioner web, OnlineMenu web, future apps) supplies
6
- * its own values. This is the contract that enables the cross-realm hard wall
7
- * planned in Phase 2 of the product split.
8
- */
9
- interface AuthClientConfig {
10
- /** Keycloak base URL, e.g. `https://identity.dloizides.com`. Trailing slash optional. */
11
- baseUrl: string;
12
- /** Realm name, e.g. `OnlineMenu` or `Questioner`. NEVER hardcoded. */
13
- realm: string;
14
- /** OAuth client id registered in the realm. */
15
- clientId: string;
16
- /** Where Keycloak should redirect after authorization (PKCE / authorization-code flow). */
17
- redirectUri?: string;
18
- /** Space-separated OAuth scopes. Defaults to `openid profile email`. */
19
- scope?: string;
20
- }
21
-
22
- /**
23
- * Persisted token bundle.
24
- *
25
- * `expiresAt` is an absolute UNIX millisecond timestamp computed at
26
- * acquisition time (`Date.now() + expires_in * 1000`) so consumers can
27
- * trivially compare against `Date.now()` without re-deriving an expiry clock.
28
- */
29
- interface AuthTokens {
30
- accessToken: string;
31
- refreshToken?: string;
32
- idToken?: string;
33
- /** Absolute UNIX millis. `0` or missing means unknown — treat as expired. */
34
- expiresAt: number;
35
- }
36
-
37
- /**
38
- * Pluggable token persistence.
39
- *
40
- * Consumers (web, native, server-side) provide an implementation tailored to
41
- * their platform: `localStorage`, `sessionStorage`, `expo-secure-store`,
42
- * `AsyncStorage`, or an in-memory map for tests. Keeping the interface narrow
43
- * (read / write / clear) keeps the package transport-agnostic.
44
- */
45
- interface TokenStorage {
46
- read(): Promise<AuthTokens | null>;
47
- write(tokens: AuthTokens): Promise<void>;
48
- clear(): Promise<void>;
49
- }
50
-
51
- /**
52
- * Inputs to {@link AuthClient.fromIssuerUrl}.
53
- *
54
- * Used by consumers that store only an issuer URL and want to derive `realm`
55
- * + `baseUrl` rather than configure them separately.
56
- */
57
- interface AuthClientFromIssuerInput {
58
- issuerUrl: string;
59
- clientId: string;
60
- redirectUri?: string;
61
- scope?: string;
62
- }
63
- declare class AuthClient {
64
- private readonly config;
65
- private readonly tokenStorage;
66
- /**
67
- * @throws Error when `baseUrl`, `realm`, or `clientId` is missing or empty.
68
- */
69
- constructor(config: AuthClientConfig, storage: TokenStorage);
70
- /**
71
- * Build an {@link AuthClient} from a standalone issuer URL by parsing the
72
- * realm and base URL. Useful when migrating from the legacy
73
- * `KEYCLOAK_ISSUER` env var convention.
74
- *
75
- * @throws Error when the issuer URL doesn't match `{base}/realms/{realm}`.
76
- */
77
- static fromIssuerUrl(input: AuthClientFromIssuerInput, storage: TokenStorage): AuthClient;
78
- private static validateConfig;
79
- get realm(): string;
80
- get clientId(): string;
81
- get baseUrl(): string;
82
- get scope(): string;
83
- get redirectUri(): string | undefined;
84
- /** Issuer URL: `{baseUrl}/realms/{realm}`. */
85
- get issuerUrl(): string;
86
- get authorizationEndpoint(): string;
87
- get tokenEndpoint(): string;
88
- get userInfoEndpoint(): string;
89
- get logoutEndpoint(): string;
90
- /**
91
- * Build a fully-formed authorization URL the user agent can navigate to.
92
- *
93
- * @throws Error when `redirectUri` is not configured.
94
- */
95
- buildAuthorizationUrl(input?: {
96
- state?: string;
97
- codeChallenge?: string;
98
- codeChallengeMethod?: 'S256' | 'plain';
99
- }): string;
100
- getTokens(): Promise<AuthTokens | null>;
101
- setTokens(tokens: AuthTokens): Promise<void>;
102
- clearTokens(): Promise<void>;
103
- /**
104
- * Read the current access token if it exists and is not expired.
105
- * Returns `null` for "no usable token".
106
- */
107
- getAccessToken(now?: number): Promise<string | null>;
108
- }
1
+ import { T as TokenStorage, A as AuthTokens } from './AuthClient-Dim7HPRz.mjs';
2
+ export { a as AuthApiClient, b as AuthApiClientOptions, c as AuthClient, d as AuthClientCollaborators, e as AuthClientConfig, f as AuthClientFromIssuerInput, g as AuthEventEmitter, h as AuthEventListener, i as AuthEventName, j as AuthEventUnsubscribe, k as AuthSessionInfo, F as ForgotPasswordRequest, H as HttpClient, l as HttpRequest, m as HttpResponse, I as InactivityStore, n as InactivityTracker, o as InactivityTrackerOptions, L as LoginOptions, p as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, q as RefreshFn, r as RefreshInterceptor, s as RefreshInterceptorOptions, t as ResetPasswordRequest, u as createFetchHttpClient } from './AuthClient-Dim7HPRz.mjs';
109
3
 
110
4
  /**
111
5
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -242,6 +136,207 @@ declare class InMemoryTokenStorage implements TokenStorage {
242
136
  clear(): Promise<void>;
243
137
  }
244
138
 
139
+ /**
140
+ * Web token storage that pairs an in-memory access token with a backend-managed
141
+ * httpOnly + Secure + SameSite=Lax refresh-token cookie.
142
+ *
143
+ * The browser handles the refresh cookie (`__Host-refresh` by default — set by
144
+ * the IdentityService on login and rotated on every `/auth/refresh-cookie`
145
+ * call). JavaScript MUST NOT have access to it, so this adapter intentionally
146
+ * does NOT persist `refreshToken` into the cookie itself; that's the backend's
147
+ * job. The adapter just keeps the access token in memory and exposes the same
148
+ * `TokenStorage` interface as `BrowserStorageTokenStorage` so the rest of the
149
+ * library doesn't need to know which transport is in use.
150
+ *
151
+ * Page reloads drop the access token (memory clears), but the refresh cookie
152
+ * survives — `RefreshInterceptor` swaps it for a new access token via
153
+ * `/auth/refresh-cookie` with `credentials: 'include'`.
154
+ */
155
+ declare class CookieTokenStorage implements TokenStorage {
156
+ private accessToken;
157
+ private idToken;
158
+ private expiresAt;
159
+ read(): Promise<AuthTokens | null>;
160
+ write(tokens: AuthTokens): Promise<void>;
161
+ clear(): Promise<void>;
162
+ }
163
+
164
+ /**
165
+ * Subset of `expo-secure-store` we use, abstracted so the package itself never
166
+ * imports `expo-secure-store` (and so web bundles never pull it in).
167
+ *
168
+ * Mobile consumers wire this up at the edge:
169
+ *
170
+ * ```ts
171
+ * import * as SecureStore from 'expo-secure-store';
172
+ * const adapter: SecureStoreLike = {
173
+ * getItemAsync: SecureStore.getItemAsync,
174
+ * setItemAsync: SecureStore.setItemAsync,
175
+ * deleteItemAsync: SecureStore.deleteItemAsync,
176
+ * };
177
+ * ```
178
+ */
179
+ interface SecureStoreLike {
180
+ getItemAsync(key: string, options?: {
181
+ requireAuthentication?: boolean;
182
+ }): Promise<string | null>;
183
+ setItemAsync(key: string, value: string, options?: {
184
+ requireAuthentication?: boolean;
185
+ }): Promise<void>;
186
+ deleteItemAsync(key: string, options?: {
187
+ requireAuthentication?: boolean;
188
+ }): Promise<void>;
189
+ }
190
+ /**
191
+ * Optional biometric gate. When provided AND `requireBiometric` is `true`, the
192
+ * gate's `unlock()` is called before reading the refresh token. Used by mobile
193
+ * consumers that opt in to biometric-protected sessions.
194
+ */
195
+ interface BiometricGateLike {
196
+ unlock(): Promise<void>;
197
+ isEnabled(): boolean;
198
+ }
199
+ interface SecureStoreTokenStorageOptions {
200
+ secureStore: SecureStoreLike;
201
+ /** Defaults applied to every key — usually `'auth'`. */
202
+ keyPrefix?: string;
203
+ /**
204
+ * When true, secure-store reads use `requireAuthentication: true`, prompting
205
+ * the OS biometric / device-passcode dialog (iOS Keychain access control,
206
+ * Android Keystore strongbox).
207
+ */
208
+ requireAuthentication?: boolean;
209
+ /**
210
+ * Optional biometric gate run BEFORE the secure-store read. Belt-and-braces
211
+ * with `requireAuthentication`: the gate enforces our own retry/lockout
212
+ * semantics, while `requireAuthentication` enforces the OS keychain ACL.
213
+ */
214
+ biometricGate?: BiometricGateLike;
215
+ }
216
+ /**
217
+ * Persist tokens in iOS Keychain / Android Keystore via `expo-secure-store`.
218
+ *
219
+ * Keys are split (access / refresh / id / expiresAt) rather than stored as a
220
+ * single JSON blob so the OS-level ACL on the refresh token slot can be
221
+ * tightened independently. With `requireAuthentication: true`, reads of any
222
+ * key trigger the OS biometric prompt — that's why we keep it OFF for writes
223
+ * (login flows must not prompt) and ON for reads (boot-time session restore).
224
+ *
225
+ * Storage key shape: `{prefix}.{slot}` (e.g. `auth.refresh`).
226
+ */
227
+ declare class SecureStoreTokenStorage implements TokenStorage {
228
+ private readonly secureStore;
229
+ private readonly prefix;
230
+ private readonly requireAuthentication;
231
+ private readonly biometricGate;
232
+ constructor(options: SecureStoreTokenStorageOptions);
233
+ read(): Promise<AuthTokens | null>;
234
+ write(tokens: AuthTokens): Promise<void>;
235
+ clear(): Promise<void>;
236
+ private shouldRunBiometricGate;
237
+ private fullKey;
238
+ }
239
+
240
+ /**
241
+ * Subset of `expo-local-authentication` we use, abstracted so the package
242
+ * itself never imports `expo-local-authentication`.
243
+ *
244
+ * Mobile consumers wire this up at the edge:
245
+ *
246
+ * ```ts
247
+ * import * as LocalAuthentication from 'expo-local-authentication';
248
+ * const adapter: LocalAuthLike = {
249
+ * hasHardwareAsync: LocalAuthentication.hasHardwareAsync,
250
+ * isEnrolledAsync: LocalAuthentication.isEnrolledAsync,
251
+ * authenticateAsync: (opts) => LocalAuthentication.authenticateAsync(opts),
252
+ * };
253
+ * ```
254
+ */
255
+ interface LocalAuthLike {
256
+ hasHardwareAsync(): Promise<boolean>;
257
+ isEnrolledAsync(): Promise<boolean>;
258
+ authenticateAsync(options?: {
259
+ promptMessage?: string;
260
+ cancelLabel?: string;
261
+ disableDeviceFallback?: boolean;
262
+ }): Promise<{
263
+ success: boolean;
264
+ error?: string;
265
+ }>;
266
+ }
267
+ /**
268
+ * Optional persistence so the "enabled" flag survives app restart. Backed by
269
+ * any `TokenStorage`-shaped key/value store via the calling consumer (or the
270
+ * app's settings store). We keep it pluggable to avoid coupling
271
+ * `BiometricGate` to a specific storage adapter.
272
+ */
273
+ interface BiometricFlagStore {
274
+ read(): Promise<boolean>;
275
+ write(enabled: boolean): Promise<void>;
276
+ }
277
+ interface BiometricGateOptions {
278
+ localAuth: LocalAuthLike;
279
+ /** Optional persistence for the user's opt-in choice. */
280
+ flagStore?: BiometricFlagStore;
281
+ /** Default prompt message; consumers usually override. */
282
+ promptMessage?: string;
283
+ /** Max consecutive prompt failures before {@link unlock} throws. Default 3. */
284
+ maxFailures?: number;
285
+ }
286
+ /**
287
+ * Biometric gate wrapping `expo-local-authentication`.
288
+ *
289
+ * Lifecycle:
290
+ *
291
+ * 1. `isAvailable()` — checks hardware + enrolment. Pure read.
292
+ * 2. `setEnabled(true|false)` — consumer's settings UI flips this. Persisted
293
+ * via the optional flag store. Default = disabled (opt-in).
294
+ * 3. `unlock()` — called by `SecureStoreTokenStorage` (when wired) or by
295
+ * consumer code before sensitive operations. Counts consecutive failures;
296
+ * after `maxFailures` (default 3), throws `BiometricLockedOutError` and
297
+ * consumers MUST navigate to login.
298
+ * 4. `prompt()` — one-shot biometric prompt that doesn't change the failure
299
+ * counter. Useful for re-confirming an action mid-session.
300
+ *
301
+ * The failure counter resets on success.
302
+ */
303
+ declare class BiometricGate {
304
+ private readonly localAuth;
305
+ private readonly flagStore;
306
+ private readonly promptMessage;
307
+ private readonly maxFailures;
308
+ private enabled;
309
+ private failureCount;
310
+ private hydrated;
311
+ constructor(options: BiometricGateOptions);
312
+ /** Hardware present AND a fingerprint/face ID is enrolled. */
313
+ isAvailable(): Promise<boolean>;
314
+ /** Synchronous read of the current enabled flag (post-hydration). */
315
+ isEnabled(): boolean;
316
+ /** Read the persisted opt-in flag once at app boot. Idempotent. */
317
+ hydrate(): Promise<void>;
318
+ /**
319
+ * Toggle biometric requirement. Persists via {@link BiometricFlagStore} when
320
+ * configured. Resets the failure counter so a re-enable starts fresh.
321
+ */
322
+ setEnabled(enabled: boolean): Promise<void>;
323
+ /** Reset the failure counter. Tests + consumer recovery flows. */
324
+ resetFailures(): void;
325
+ /**
326
+ * One-shot biometric prompt. Returns `true` on success. Does NOT throw on
327
+ * failure or update the failure counter — useful for action confirmation.
328
+ */
329
+ prompt(): Promise<boolean>;
330
+ /**
331
+ * Required pre-condition for sensitive token reads. No-op when disabled.
332
+ *
333
+ * @throws Error after {@link maxFailures} consecutive failures.
334
+ * @throws Error on a single failure (lower in the count, but still throws so
335
+ * `SecureStoreTokenStorage.read()` short-circuits).
336
+ */
337
+ unlock(): Promise<void>;
338
+ }
339
+
245
340
  /**
246
341
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
247
342
  *
@@ -415,4 +510,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
415
510
  */
416
511
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
417
512
 
418
- export { AuthClient, type AuthClientConfig, type AuthClientFromIssuerInput, type AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type NormalizedUser, type RawTokenResponse, type RefreshTokenBodyInput, type StorageLike, type TokenResponse, type TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
513
+ export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, type RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, type TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
package/dist/index.d.ts CHANGED
@@ -1,111 +1,5 @@
1
- /**
2
- * Configuration for {@link AuthClient}.
3
- *
4
- * `realm` and `clientId` are **required** and never have sensible portfolio-wide
5
- * defaults. Each consumer (Questioner web, OnlineMenu web, future apps) supplies
6
- * its own values. This is the contract that enables the cross-realm hard wall
7
- * planned in Phase 2 of the product split.
8
- */
9
- interface AuthClientConfig {
10
- /** Keycloak base URL, e.g. `https://identity.dloizides.com`. Trailing slash optional. */
11
- baseUrl: string;
12
- /** Realm name, e.g. `OnlineMenu` or `Questioner`. NEVER hardcoded. */
13
- realm: string;
14
- /** OAuth client id registered in the realm. */
15
- clientId: string;
16
- /** Where Keycloak should redirect after authorization (PKCE / authorization-code flow). */
17
- redirectUri?: string;
18
- /** Space-separated OAuth scopes. Defaults to `openid profile email`. */
19
- scope?: string;
20
- }
21
-
22
- /**
23
- * Persisted token bundle.
24
- *
25
- * `expiresAt` is an absolute UNIX millisecond timestamp computed at
26
- * acquisition time (`Date.now() + expires_in * 1000`) so consumers can
27
- * trivially compare against `Date.now()` without re-deriving an expiry clock.
28
- */
29
- interface AuthTokens {
30
- accessToken: string;
31
- refreshToken?: string;
32
- idToken?: string;
33
- /** Absolute UNIX millis. `0` or missing means unknown — treat as expired. */
34
- expiresAt: number;
35
- }
36
-
37
- /**
38
- * Pluggable token persistence.
39
- *
40
- * Consumers (web, native, server-side) provide an implementation tailored to
41
- * their platform: `localStorage`, `sessionStorage`, `expo-secure-store`,
42
- * `AsyncStorage`, or an in-memory map for tests. Keeping the interface narrow
43
- * (read / write / clear) keeps the package transport-agnostic.
44
- */
45
- interface TokenStorage {
46
- read(): Promise<AuthTokens | null>;
47
- write(tokens: AuthTokens): Promise<void>;
48
- clear(): Promise<void>;
49
- }
50
-
51
- /**
52
- * Inputs to {@link AuthClient.fromIssuerUrl}.
53
- *
54
- * Used by consumers that store only an issuer URL and want to derive `realm`
55
- * + `baseUrl` rather than configure them separately.
56
- */
57
- interface AuthClientFromIssuerInput {
58
- issuerUrl: string;
59
- clientId: string;
60
- redirectUri?: string;
61
- scope?: string;
62
- }
63
- declare class AuthClient {
64
- private readonly config;
65
- private readonly tokenStorage;
66
- /**
67
- * @throws Error when `baseUrl`, `realm`, or `clientId` is missing or empty.
68
- */
69
- constructor(config: AuthClientConfig, storage: TokenStorage);
70
- /**
71
- * Build an {@link AuthClient} from a standalone issuer URL by parsing the
72
- * realm and base URL. Useful when migrating from the legacy
73
- * `KEYCLOAK_ISSUER` env var convention.
74
- *
75
- * @throws Error when the issuer URL doesn't match `{base}/realms/{realm}`.
76
- */
77
- static fromIssuerUrl(input: AuthClientFromIssuerInput, storage: TokenStorage): AuthClient;
78
- private static validateConfig;
79
- get realm(): string;
80
- get clientId(): string;
81
- get baseUrl(): string;
82
- get scope(): string;
83
- get redirectUri(): string | undefined;
84
- /** Issuer URL: `{baseUrl}/realms/{realm}`. */
85
- get issuerUrl(): string;
86
- get authorizationEndpoint(): string;
87
- get tokenEndpoint(): string;
88
- get userInfoEndpoint(): string;
89
- get logoutEndpoint(): string;
90
- /**
91
- * Build a fully-formed authorization URL the user agent can navigate to.
92
- *
93
- * @throws Error when `redirectUri` is not configured.
94
- */
95
- buildAuthorizationUrl(input?: {
96
- state?: string;
97
- codeChallenge?: string;
98
- codeChallengeMethod?: 'S256' | 'plain';
99
- }): string;
100
- getTokens(): Promise<AuthTokens | null>;
101
- setTokens(tokens: AuthTokens): Promise<void>;
102
- clearTokens(): Promise<void>;
103
- /**
104
- * Read the current access token if it exists and is not expired.
105
- * Returns `null` for "no usable token".
106
- */
107
- getAccessToken(now?: number): Promise<string | null>;
108
- }
1
+ import { T as TokenStorage, A as AuthTokens } from './AuthClient-Dim7HPRz.js';
2
+ export { a as AuthApiClient, b as AuthApiClientOptions, c as AuthClient, d as AuthClientCollaborators, e as AuthClientConfig, f as AuthClientFromIssuerInput, g as AuthEventEmitter, h as AuthEventListener, i as AuthEventName, j as AuthEventUnsubscribe, k as AuthSessionInfo, F as ForgotPasswordRequest, H as HttpClient, l as HttpRequest, m as HttpResponse, I as InactivityStore, n as InactivityTracker, o as InactivityTrackerOptions, L as LoginOptions, p as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, R as RawAuthLoginResponse, q as RefreshFn, r as RefreshInterceptor, s as RefreshInterceptorOptions, t as ResetPasswordRequest, u as createFetchHttpClient } from './AuthClient-Dim7HPRz.js';
109
3
 
110
4
  /**
111
5
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -242,6 +136,207 @@ declare class InMemoryTokenStorage implements TokenStorage {
242
136
  clear(): Promise<void>;
243
137
  }
244
138
 
139
+ /**
140
+ * Web token storage that pairs an in-memory access token with a backend-managed
141
+ * httpOnly + Secure + SameSite=Lax refresh-token cookie.
142
+ *
143
+ * The browser handles the refresh cookie (`__Host-refresh` by default — set by
144
+ * the IdentityService on login and rotated on every `/auth/refresh-cookie`
145
+ * call). JavaScript MUST NOT have access to it, so this adapter intentionally
146
+ * does NOT persist `refreshToken` into the cookie itself; that's the backend's
147
+ * job. The adapter just keeps the access token in memory and exposes the same
148
+ * `TokenStorage` interface as `BrowserStorageTokenStorage` so the rest of the
149
+ * library doesn't need to know which transport is in use.
150
+ *
151
+ * Page reloads drop the access token (memory clears), but the refresh cookie
152
+ * survives — `RefreshInterceptor` swaps it for a new access token via
153
+ * `/auth/refresh-cookie` with `credentials: 'include'`.
154
+ */
155
+ declare class CookieTokenStorage implements TokenStorage {
156
+ private accessToken;
157
+ private idToken;
158
+ private expiresAt;
159
+ read(): Promise<AuthTokens | null>;
160
+ write(tokens: AuthTokens): Promise<void>;
161
+ clear(): Promise<void>;
162
+ }
163
+
164
+ /**
165
+ * Subset of `expo-secure-store` we use, abstracted so the package itself never
166
+ * imports `expo-secure-store` (and so web bundles never pull it in).
167
+ *
168
+ * Mobile consumers wire this up at the edge:
169
+ *
170
+ * ```ts
171
+ * import * as SecureStore from 'expo-secure-store';
172
+ * const adapter: SecureStoreLike = {
173
+ * getItemAsync: SecureStore.getItemAsync,
174
+ * setItemAsync: SecureStore.setItemAsync,
175
+ * deleteItemAsync: SecureStore.deleteItemAsync,
176
+ * };
177
+ * ```
178
+ */
179
+ interface SecureStoreLike {
180
+ getItemAsync(key: string, options?: {
181
+ requireAuthentication?: boolean;
182
+ }): Promise<string | null>;
183
+ setItemAsync(key: string, value: string, options?: {
184
+ requireAuthentication?: boolean;
185
+ }): Promise<void>;
186
+ deleteItemAsync(key: string, options?: {
187
+ requireAuthentication?: boolean;
188
+ }): Promise<void>;
189
+ }
190
+ /**
191
+ * Optional biometric gate. When provided AND `requireBiometric` is `true`, the
192
+ * gate's `unlock()` is called before reading the refresh token. Used by mobile
193
+ * consumers that opt in to biometric-protected sessions.
194
+ */
195
+ interface BiometricGateLike {
196
+ unlock(): Promise<void>;
197
+ isEnabled(): boolean;
198
+ }
199
+ interface SecureStoreTokenStorageOptions {
200
+ secureStore: SecureStoreLike;
201
+ /** Defaults applied to every key — usually `'auth'`. */
202
+ keyPrefix?: string;
203
+ /**
204
+ * When true, secure-store reads use `requireAuthentication: true`, prompting
205
+ * the OS biometric / device-passcode dialog (iOS Keychain access control,
206
+ * Android Keystore strongbox).
207
+ */
208
+ requireAuthentication?: boolean;
209
+ /**
210
+ * Optional biometric gate run BEFORE the secure-store read. Belt-and-braces
211
+ * with `requireAuthentication`: the gate enforces our own retry/lockout
212
+ * semantics, while `requireAuthentication` enforces the OS keychain ACL.
213
+ */
214
+ biometricGate?: BiometricGateLike;
215
+ }
216
+ /**
217
+ * Persist tokens in iOS Keychain / Android Keystore via `expo-secure-store`.
218
+ *
219
+ * Keys are split (access / refresh / id / expiresAt) rather than stored as a
220
+ * single JSON blob so the OS-level ACL on the refresh token slot can be
221
+ * tightened independently. With `requireAuthentication: true`, reads of any
222
+ * key trigger the OS biometric prompt — that's why we keep it OFF for writes
223
+ * (login flows must not prompt) and ON for reads (boot-time session restore).
224
+ *
225
+ * Storage key shape: `{prefix}.{slot}` (e.g. `auth.refresh`).
226
+ */
227
+ declare class SecureStoreTokenStorage implements TokenStorage {
228
+ private readonly secureStore;
229
+ private readonly prefix;
230
+ private readonly requireAuthentication;
231
+ private readonly biometricGate;
232
+ constructor(options: SecureStoreTokenStorageOptions);
233
+ read(): Promise<AuthTokens | null>;
234
+ write(tokens: AuthTokens): Promise<void>;
235
+ clear(): Promise<void>;
236
+ private shouldRunBiometricGate;
237
+ private fullKey;
238
+ }
239
+
240
+ /**
241
+ * Subset of `expo-local-authentication` we use, abstracted so the package
242
+ * itself never imports `expo-local-authentication`.
243
+ *
244
+ * Mobile consumers wire this up at the edge:
245
+ *
246
+ * ```ts
247
+ * import * as LocalAuthentication from 'expo-local-authentication';
248
+ * const adapter: LocalAuthLike = {
249
+ * hasHardwareAsync: LocalAuthentication.hasHardwareAsync,
250
+ * isEnrolledAsync: LocalAuthentication.isEnrolledAsync,
251
+ * authenticateAsync: (opts) => LocalAuthentication.authenticateAsync(opts),
252
+ * };
253
+ * ```
254
+ */
255
+ interface LocalAuthLike {
256
+ hasHardwareAsync(): Promise<boolean>;
257
+ isEnrolledAsync(): Promise<boolean>;
258
+ authenticateAsync(options?: {
259
+ promptMessage?: string;
260
+ cancelLabel?: string;
261
+ disableDeviceFallback?: boolean;
262
+ }): Promise<{
263
+ success: boolean;
264
+ error?: string;
265
+ }>;
266
+ }
267
+ /**
268
+ * Optional persistence so the "enabled" flag survives app restart. Backed by
269
+ * any `TokenStorage`-shaped key/value store via the calling consumer (or the
270
+ * app's settings store). We keep it pluggable to avoid coupling
271
+ * `BiometricGate` to a specific storage adapter.
272
+ */
273
+ interface BiometricFlagStore {
274
+ read(): Promise<boolean>;
275
+ write(enabled: boolean): Promise<void>;
276
+ }
277
+ interface BiometricGateOptions {
278
+ localAuth: LocalAuthLike;
279
+ /** Optional persistence for the user's opt-in choice. */
280
+ flagStore?: BiometricFlagStore;
281
+ /** Default prompt message; consumers usually override. */
282
+ promptMessage?: string;
283
+ /** Max consecutive prompt failures before {@link unlock} throws. Default 3. */
284
+ maxFailures?: number;
285
+ }
286
+ /**
287
+ * Biometric gate wrapping `expo-local-authentication`.
288
+ *
289
+ * Lifecycle:
290
+ *
291
+ * 1. `isAvailable()` — checks hardware + enrolment. Pure read.
292
+ * 2. `setEnabled(true|false)` — consumer's settings UI flips this. Persisted
293
+ * via the optional flag store. Default = disabled (opt-in).
294
+ * 3. `unlock()` — called by `SecureStoreTokenStorage` (when wired) or by
295
+ * consumer code before sensitive operations. Counts consecutive failures;
296
+ * after `maxFailures` (default 3), throws `BiometricLockedOutError` and
297
+ * consumers MUST navigate to login.
298
+ * 4. `prompt()` — one-shot biometric prompt that doesn't change the failure
299
+ * counter. Useful for re-confirming an action mid-session.
300
+ *
301
+ * The failure counter resets on success.
302
+ */
303
+ declare class BiometricGate {
304
+ private readonly localAuth;
305
+ private readonly flagStore;
306
+ private readonly promptMessage;
307
+ private readonly maxFailures;
308
+ private enabled;
309
+ private failureCount;
310
+ private hydrated;
311
+ constructor(options: BiometricGateOptions);
312
+ /** Hardware present AND a fingerprint/face ID is enrolled. */
313
+ isAvailable(): Promise<boolean>;
314
+ /** Synchronous read of the current enabled flag (post-hydration). */
315
+ isEnabled(): boolean;
316
+ /** Read the persisted opt-in flag once at app boot. Idempotent. */
317
+ hydrate(): Promise<void>;
318
+ /**
319
+ * Toggle biometric requirement. Persists via {@link BiometricFlagStore} when
320
+ * configured. Resets the failure counter so a re-enable starts fresh.
321
+ */
322
+ setEnabled(enabled: boolean): Promise<void>;
323
+ /** Reset the failure counter. Tests + consumer recovery flows. */
324
+ resetFailures(): void;
325
+ /**
326
+ * One-shot biometric prompt. Returns `true` on success. Does NOT throw on
327
+ * failure or update the failure counter — useful for action confirmation.
328
+ */
329
+ prompt(): Promise<boolean>;
330
+ /**
331
+ * Required pre-condition for sensitive token reads. No-op when disabled.
332
+ *
333
+ * @throws Error after {@link maxFailures} consecutive failures.
334
+ * @throws Error on a single failure (lower in the count, but still throws so
335
+ * `SecureStoreTokenStorage.read()` short-circuits).
336
+ */
337
+ unlock(): Promise<void>;
338
+ }
339
+
245
340
  /**
246
341
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
247
342
  *
@@ -415,4 +510,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
415
510
  */
416
511
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
417
512
 
418
- export { AuthClient, type AuthClientConfig, type AuthClientFromIssuerInput, type AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type NormalizedUser, type RawTokenResponse, type RefreshTokenBodyInput, type StorageLike, type TokenResponse, type TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };
513
+ export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, InMemoryTokenStorage, KeycloakRoles, type KeycloakUserInfo, type LocalAuthLike, type NormalizedUser, type RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, type TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };