@dloizides/auth-client 1.0.0 → 2.1.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.ts CHANGED
@@ -1,111 +1,8 @@
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, c as AuthTokens } from './AuthClient-D95OMajD.js';
2
+ export { A as AuthApiClient, d as AuthApiClientOptions, b as AuthClient, e as AuthClientCollaborators, f as AuthClientConfig, g as AuthClientFromIssuerInput, h as AuthEventEmitter, i as AuthEventListener, j as AuthEventName, k as AuthEventUnsubscribe, a as AuthSessionInfo, D as DirectKcOptions, F as ForgotPasswordRequest, I as InactivityStore, l as InactivityTracker, m as InactivityTrackerOptions, L as LoginOptions, n as LogoutOptions, O as OtpLoginRequest, P as PasswordLoginRequest, o as RawAuthLoginResponse, p as RefreshFn, q as RefreshInterceptor, r as RefreshInterceptorOptions, R as ResetPasswordRequest } from './AuthClient-D95OMajD.js';
3
+ export { ExchangeAuthorizationCodeInput, FetchDiscoveryDocumentInput, OidcDiscoveryDocument, PkcePair, RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken } from './oidc/index.js';
4
+ import { R as RawTokenResponse, T as TokenResponse } from './TokenResponse-CY1CaU2l.js';
5
+ export { H as HttpClient, a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-CY1CaU2l.js';
109
6
 
110
7
  /**
111
8
  * Roles emitted by Keycloak realms in the dloizides.com portfolio.
@@ -171,31 +68,6 @@ interface NormalizedUser {
171
68
  raw?: KeycloakUserInfo;
172
69
  }
173
70
 
174
- /**
175
- * Raw token endpoint response (snake_case, OIDC standard).
176
- */
177
- interface RawTokenResponse {
178
- access_token: string;
179
- refresh_token?: string;
180
- id_token?: string;
181
- expires_in?: number;
182
- token_type?: string;
183
- scope?: string;
184
- [key: string]: unknown;
185
- }
186
- /**
187
- * Application-friendly camelCase view of a token endpoint response.
188
- */
189
- interface TokenResponse {
190
- accessToken: string;
191
- refreshToken?: string;
192
- idToken?: string;
193
- /** Seconds until expiry, as returned by Keycloak. */
194
- expiresIn?: number;
195
- tokenType?: string;
196
- scope?: string;
197
- }
198
-
199
71
  /**
200
72
  * Subset of `Storage` we actually use. Lets callers inject `localStorage`,
201
73
  * `sessionStorage`, or any compatible polyfill.
@@ -242,6 +114,207 @@ declare class InMemoryTokenStorage implements TokenStorage {
242
114
  clear(): Promise<void>;
243
115
  }
244
116
 
117
+ /**
118
+ * Web token storage that pairs an in-memory access token with a backend-managed
119
+ * httpOnly + Secure + SameSite=Lax refresh-token cookie.
120
+ *
121
+ * The browser handles the refresh cookie (`__Host-refresh` by default — set by
122
+ * the IdentityService on login and rotated on every `/auth/refresh-cookie`
123
+ * call). JavaScript MUST NOT have access to it, so this adapter intentionally
124
+ * does NOT persist `refreshToken` into the cookie itself; that's the backend's
125
+ * job. The adapter just keeps the access token in memory and exposes the same
126
+ * `TokenStorage` interface as `BrowserStorageTokenStorage` so the rest of the
127
+ * library doesn't need to know which transport is in use.
128
+ *
129
+ * Page reloads drop the access token (memory clears), but the refresh cookie
130
+ * survives — `RefreshInterceptor` swaps it for a new access token via
131
+ * `/auth/refresh-cookie` with `credentials: 'include'`.
132
+ */
133
+ declare class CookieTokenStorage implements TokenStorage {
134
+ private accessToken;
135
+ private idToken;
136
+ private expiresAt;
137
+ read(): Promise<AuthTokens | null>;
138
+ write(tokens: AuthTokens): Promise<void>;
139
+ clear(): Promise<void>;
140
+ }
141
+
142
+ /**
143
+ * Subset of `expo-secure-store` we use, abstracted so the package itself never
144
+ * imports `expo-secure-store` (and so web bundles never pull it in).
145
+ *
146
+ * Mobile consumers wire this up at the edge:
147
+ *
148
+ * ```ts
149
+ * import * as SecureStore from 'expo-secure-store';
150
+ * const adapter: SecureStoreLike = {
151
+ * getItemAsync: SecureStore.getItemAsync,
152
+ * setItemAsync: SecureStore.setItemAsync,
153
+ * deleteItemAsync: SecureStore.deleteItemAsync,
154
+ * };
155
+ * ```
156
+ */
157
+ interface SecureStoreLike {
158
+ getItemAsync(key: string, options?: {
159
+ requireAuthentication?: boolean;
160
+ }): Promise<string | null>;
161
+ setItemAsync(key: string, value: string, options?: {
162
+ requireAuthentication?: boolean;
163
+ }): Promise<void>;
164
+ deleteItemAsync(key: string, options?: {
165
+ requireAuthentication?: boolean;
166
+ }): Promise<void>;
167
+ }
168
+ /**
169
+ * Optional biometric gate. When provided AND `requireBiometric` is `true`, the
170
+ * gate's `unlock()` is called before reading the refresh token. Used by mobile
171
+ * consumers that opt in to biometric-protected sessions.
172
+ */
173
+ interface BiometricGateLike {
174
+ unlock(): Promise<void>;
175
+ isEnabled(): boolean;
176
+ }
177
+ interface SecureStoreTokenStorageOptions {
178
+ secureStore: SecureStoreLike;
179
+ /** Defaults applied to every key — usually `'auth'`. */
180
+ keyPrefix?: string;
181
+ /**
182
+ * When true, secure-store reads use `requireAuthentication: true`, prompting
183
+ * the OS biometric / device-passcode dialog (iOS Keychain access control,
184
+ * Android Keystore strongbox).
185
+ */
186
+ requireAuthentication?: boolean;
187
+ /**
188
+ * Optional biometric gate run BEFORE the secure-store read. Belt-and-braces
189
+ * with `requireAuthentication`: the gate enforces our own retry/lockout
190
+ * semantics, while `requireAuthentication` enforces the OS keychain ACL.
191
+ */
192
+ biometricGate?: BiometricGateLike;
193
+ }
194
+ /**
195
+ * Persist tokens in iOS Keychain / Android Keystore via `expo-secure-store`.
196
+ *
197
+ * Keys are split (access / refresh / id / expiresAt) rather than stored as a
198
+ * single JSON blob so the OS-level ACL on the refresh token slot can be
199
+ * tightened independently. With `requireAuthentication: true`, reads of any
200
+ * key trigger the OS biometric prompt — that's why we keep it OFF for writes
201
+ * (login flows must not prompt) and ON for reads (boot-time session restore).
202
+ *
203
+ * Storage key shape: `{prefix}.{slot}` (e.g. `auth.refresh`).
204
+ */
205
+ declare class SecureStoreTokenStorage implements TokenStorage {
206
+ private readonly secureStore;
207
+ private readonly prefix;
208
+ private readonly requireAuthentication;
209
+ private readonly biometricGate;
210
+ constructor(options: SecureStoreTokenStorageOptions);
211
+ read(): Promise<AuthTokens | null>;
212
+ write(tokens: AuthTokens): Promise<void>;
213
+ clear(): Promise<void>;
214
+ private shouldRunBiometricGate;
215
+ private fullKey;
216
+ }
217
+
218
+ /**
219
+ * Subset of `expo-local-authentication` we use, abstracted so the package
220
+ * itself never imports `expo-local-authentication`.
221
+ *
222
+ * Mobile consumers wire this up at the edge:
223
+ *
224
+ * ```ts
225
+ * import * as LocalAuthentication from 'expo-local-authentication';
226
+ * const adapter: LocalAuthLike = {
227
+ * hasHardwareAsync: LocalAuthentication.hasHardwareAsync,
228
+ * isEnrolledAsync: LocalAuthentication.isEnrolledAsync,
229
+ * authenticateAsync: (opts) => LocalAuthentication.authenticateAsync(opts),
230
+ * };
231
+ * ```
232
+ */
233
+ interface LocalAuthLike {
234
+ hasHardwareAsync(): Promise<boolean>;
235
+ isEnrolledAsync(): Promise<boolean>;
236
+ authenticateAsync(options?: {
237
+ promptMessage?: string;
238
+ cancelLabel?: string;
239
+ disableDeviceFallback?: boolean;
240
+ }): Promise<{
241
+ success: boolean;
242
+ error?: string;
243
+ }>;
244
+ }
245
+ /**
246
+ * Optional persistence so the "enabled" flag survives app restart. Backed by
247
+ * any `TokenStorage`-shaped key/value store via the calling consumer (or the
248
+ * app's settings store). We keep it pluggable to avoid coupling
249
+ * `BiometricGate` to a specific storage adapter.
250
+ */
251
+ interface BiometricFlagStore {
252
+ read(): Promise<boolean>;
253
+ write(enabled: boolean): Promise<void>;
254
+ }
255
+ interface BiometricGateOptions {
256
+ localAuth: LocalAuthLike;
257
+ /** Optional persistence for the user's opt-in choice. */
258
+ flagStore?: BiometricFlagStore;
259
+ /** Default prompt message; consumers usually override. */
260
+ promptMessage?: string;
261
+ /** Max consecutive prompt failures before {@link unlock} throws. Default 3. */
262
+ maxFailures?: number;
263
+ }
264
+ /**
265
+ * Biometric gate wrapping `expo-local-authentication`.
266
+ *
267
+ * Lifecycle:
268
+ *
269
+ * 1. `isAvailable()` — checks hardware + enrolment. Pure read.
270
+ * 2. `setEnabled(true|false)` — consumer's settings UI flips this. Persisted
271
+ * via the optional flag store. Default = disabled (opt-in).
272
+ * 3. `unlock()` — called by `SecureStoreTokenStorage` (when wired) or by
273
+ * consumer code before sensitive operations. Counts consecutive failures;
274
+ * after `maxFailures` (default 3), throws `BiometricLockedOutError` and
275
+ * consumers MUST navigate to login.
276
+ * 4. `prompt()` — one-shot biometric prompt that doesn't change the failure
277
+ * counter. Useful for re-confirming an action mid-session.
278
+ *
279
+ * The failure counter resets on success.
280
+ */
281
+ declare class BiometricGate {
282
+ private readonly localAuth;
283
+ private readonly flagStore;
284
+ private readonly promptMessage;
285
+ private readonly maxFailures;
286
+ private enabled;
287
+ private failureCount;
288
+ private hydrated;
289
+ constructor(options: BiometricGateOptions);
290
+ /** Hardware present AND a fingerprint/face ID is enrolled. */
291
+ isAvailable(): Promise<boolean>;
292
+ /** Synchronous read of the current enabled flag (post-hydration). */
293
+ isEnabled(): boolean;
294
+ /** Read the persisted opt-in flag once at app boot. Idempotent. */
295
+ hydrate(): Promise<void>;
296
+ /**
297
+ * Toggle biometric requirement. Persists via {@link BiometricFlagStore} when
298
+ * configured. Resets the failure counter so a re-enable starts fresh.
299
+ */
300
+ setEnabled(enabled: boolean): Promise<void>;
301
+ /** Reset the failure counter. Tests + consumer recovery flows. */
302
+ resetFailures(): void;
303
+ /**
304
+ * One-shot biometric prompt. Returns `true` on success. Does NOT throw on
305
+ * failure or update the failure counter — useful for action confirmation.
306
+ */
307
+ prompt(): Promise<boolean>;
308
+ /**
309
+ * Required pre-condition for sensitive token reads. No-op when disabled.
310
+ *
311
+ * @throws Error after {@link maxFailures} consecutive failures.
312
+ * @throws Error on a single failure (lower in the count, but still throws so
313
+ * `SecureStoreTokenStorage.read()` short-circuits).
314
+ */
315
+ unlock(): Promise<void>;
316
+ }
317
+
245
318
  /**
246
319
  * Convert a Keycloak `/userinfo` payload into a flat, app-friendly user object.
247
320
  *
@@ -415,4 +488,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
415
488
  */
416
489
  declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
417
490
 
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 };
491
+ 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, RawTokenResponse, type RefreshTokenBodyInput, type SecureStoreLike, SecureStoreTokenStorage, type SecureStoreTokenStorageOptions, type StorageLike, TokenResponse, TokenStorage, buildAuthorizationCodeBody, buildAuthorizationEndpoint, buildAuthorizationUrl, buildIssuerUrl, buildLogoutEndpoint, buildRefreshTokenBody, buildTokenEndpoint, buildUserInfoEndpoint, computeExpiresAt, decodeJwt, extractAuthCode, isKeycloakRole, isTokenExpired, normalizeKeycloakUser, normalizeTokenResponse, parseBaseUrlFromIssuer, parseRealmFromIssuer, tokenResponseToAuthTokens };