@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/CHANGELOG.md +176 -0
- package/README.md +138 -42
- package/dist/AuthClient-BGr8L03W.d.mts +460 -0
- package/dist/AuthClient-D95OMajD.d.ts +460 -0
- package/dist/TokenResponse-CY1CaU2l.d.mts +59 -0
- package/dist/TokenResponse-CY1CaU2l.d.ts +59 -0
- package/dist/index.d.mts +207 -134
- package/dist/index.d.ts +207 -134
- package/dist/index.js +799 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +785 -53
- package/dist/index.mjs.map +1 -1
- package/dist/oidc/index.d.mts +127 -0
- package/dist/oidc/index.d.ts +127 -0
- package/dist/oidc/index.js +192 -0
- package/dist/oidc/index.js.map +1 -0
- package/dist/oidc/index.mjs +184 -0
- package/dist/oidc/index.mjs.map +1 -0
- package/dist/react.d.mts +63 -0
- package/dist/react.d.ts +63 -0
- package/dist/react.js +65 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +58 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +53 -5
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { H as HttpClient, T as TokenResponse } from './TokenResponse-CY1CaU2l.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tiny dependency-free event emitter for auth lifecycle events.
|
|
5
|
+
*
|
|
6
|
+
* Consumers subscribe to `onSessionExpired` to navigate to the login screen
|
|
7
|
+
* when refresh fails or the inactivity timeout fires. We don't reach for
|
|
8
|
+
* `EventTarget`/`EventEmitter` because we want one consistent API across web,
|
|
9
|
+
* React Native, and node test environments without polyfills.
|
|
10
|
+
*/
|
|
11
|
+
type AuthEventName = 'sessionExpired';
|
|
12
|
+
type AuthEventListener = () => void;
|
|
13
|
+
interface AuthEventUnsubscribe {
|
|
14
|
+
(): void;
|
|
15
|
+
}
|
|
16
|
+
declare class AuthEventEmitter {
|
|
17
|
+
private readonly listeners;
|
|
18
|
+
on(event: AuthEventName, listener: AuthEventListener): AuthEventUnsubscribe;
|
|
19
|
+
emit(event: AuthEventName): void;
|
|
20
|
+
/** Remove all listeners. Useful for `AuthClient.dispose()` and tests. */
|
|
21
|
+
clear(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Backend session record returned by `GET /me/sessions`.
|
|
26
|
+
*
|
|
27
|
+
* Shape mirrors the existing IdentityService response — see
|
|
28
|
+
* `Services/Identity/.../GetSessions.cs`. Defined as a permissive interface
|
|
29
|
+
* so newer fields (added server-side) flow through without a package bump.
|
|
30
|
+
*/
|
|
31
|
+
interface AuthSessionInfo {
|
|
32
|
+
id: string;
|
|
33
|
+
isCurrent?: boolean;
|
|
34
|
+
ipAddress?: string;
|
|
35
|
+
userAgent?: string;
|
|
36
|
+
createdAt?: string;
|
|
37
|
+
lastSeenAt?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
interface AuthApiClientOptions {
|
|
41
|
+
http: HttpClient;
|
|
42
|
+
/** API base, e.g. `https://api.dloizides.com`. No trailing slash needed. */
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional supplier of the current access token, used as a Bearer header on
|
|
46
|
+
* authenticated calls (sessions list, revoke, logout). When omitted those
|
|
47
|
+
* calls send no Authorization header — typical for cookie-based web auth.
|
|
48
|
+
*/
|
|
49
|
+
getAccessToken?: () => Promise<string | null>;
|
|
50
|
+
/**
|
|
51
|
+
* When true, every request adds `credentials: 'include'`. Required for
|
|
52
|
+
* cookie-based web auth (`__Host-refresh` lives in an httpOnly cookie).
|
|
53
|
+
*/
|
|
54
|
+
useCredentials?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface OtpLoginRequest {
|
|
57
|
+
email: string;
|
|
58
|
+
otp: string;
|
|
59
|
+
tenantId?: string;
|
|
60
|
+
offlineAccess?: boolean;
|
|
61
|
+
}
|
|
62
|
+
interface PasswordLoginRequest {
|
|
63
|
+
email: string;
|
|
64
|
+
password: string;
|
|
65
|
+
tenantId?: string;
|
|
66
|
+
offlineAccess?: boolean;
|
|
67
|
+
}
|
|
68
|
+
interface ForgotPasswordRequest {
|
|
69
|
+
email: string;
|
|
70
|
+
tenantId?: string;
|
|
71
|
+
}
|
|
72
|
+
interface ResetPasswordRequest {
|
|
73
|
+
token: string;
|
|
74
|
+
newPassword: string;
|
|
75
|
+
}
|
|
76
|
+
interface RawAuthLoginResponse {
|
|
77
|
+
access_token?: string;
|
|
78
|
+
refresh_token?: string;
|
|
79
|
+
id_token?: string;
|
|
80
|
+
expires_in?: number;
|
|
81
|
+
token_type?: string;
|
|
82
|
+
scope?: string;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Thin HTTP client for the IdentityService auth surface.
|
|
87
|
+
*
|
|
88
|
+
* Endpoint paths match the backend task `auth-password-reset-backend.md`:
|
|
89
|
+
*
|
|
90
|
+
* - `POST /auth/verify-otp`
|
|
91
|
+
* - `POST /auth/login` (password)
|
|
92
|
+
* - `POST /auth/logout` and `POST /auth/logout?everywhere=true`
|
|
93
|
+
* - `POST /auth/refresh-cookie` (web cookie flow)
|
|
94
|
+
* - `POST /auth/forgot-password`
|
|
95
|
+
* - `POST /auth/reset-password`
|
|
96
|
+
* - `GET /me/sessions`
|
|
97
|
+
* - `POST /me/sessions/{id}/revoke`
|
|
98
|
+
*
|
|
99
|
+
* Doesn't touch token storage — that's `AuthClient`'s job. Doesn't decide
|
|
100
|
+
* what to do with errors — callers handle them. Just builds requests and
|
|
101
|
+
* deserialises responses.
|
|
102
|
+
*/
|
|
103
|
+
declare class AuthApiClient {
|
|
104
|
+
private readonly http;
|
|
105
|
+
private readonly baseUrl;
|
|
106
|
+
private readonly getAccessToken;
|
|
107
|
+
private readonly useCredentials;
|
|
108
|
+
constructor(options: AuthApiClientOptions);
|
|
109
|
+
loginWithOtp(request: OtpLoginRequest): Promise<RawAuthLoginResponse>;
|
|
110
|
+
loginWithPassword(request: PasswordLoginRequest): Promise<RawAuthLoginResponse>;
|
|
111
|
+
/** Web cookie-flow refresh. Sends no body; cookie travels via `credentials`. */
|
|
112
|
+
refreshCookie(): Promise<RawAuthLoginResponse>;
|
|
113
|
+
logout(everywhere?: boolean): Promise<void>;
|
|
114
|
+
forgotPassword(request: ForgotPasswordRequest): Promise<void>;
|
|
115
|
+
resetPassword(request: ResetPasswordRequest): Promise<void>;
|
|
116
|
+
listSessions(): Promise<AuthSessionInfo[]>;
|
|
117
|
+
revokeSession(sessionId: string): Promise<void>;
|
|
118
|
+
private postLogin;
|
|
119
|
+
private authHeaders;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Configuration for {@link AuthClient}.
|
|
124
|
+
*
|
|
125
|
+
* `realm` and `clientId` are **required** and never have sensible portfolio-wide
|
|
126
|
+
* defaults. Each consumer (Questioner web, OnlineMenu web, future apps) supplies
|
|
127
|
+
* its own values. This is the contract that enables the cross-realm hard wall
|
|
128
|
+
* planned in Phase 2 of the product split.
|
|
129
|
+
*/
|
|
130
|
+
interface AuthClientConfig {
|
|
131
|
+
/** Keycloak base URL, e.g. `https://identity.dloizides.com`. Trailing slash optional. */
|
|
132
|
+
baseUrl: string;
|
|
133
|
+
/** Realm name, e.g. `OnlineMenu` or `Questioner`. NEVER hardcoded. */
|
|
134
|
+
realm: string;
|
|
135
|
+
/** OAuth client id registered in the realm. */
|
|
136
|
+
clientId: string;
|
|
137
|
+
/** Where Keycloak should redirect after authorization (PKCE / authorization-code flow). */
|
|
138
|
+
redirectUri?: string;
|
|
139
|
+
/** Space-separated OAuth scopes. Defaults to `openid profile email`. */
|
|
140
|
+
scope?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Persisted token bundle.
|
|
145
|
+
*
|
|
146
|
+
* `expiresAt` is an absolute UNIX millisecond timestamp computed at
|
|
147
|
+
* acquisition time (`Date.now() + expires_in * 1000`) so consumers can
|
|
148
|
+
* trivially compare against `Date.now()` without re-deriving an expiry clock.
|
|
149
|
+
*/
|
|
150
|
+
interface AuthTokens {
|
|
151
|
+
accessToken: string;
|
|
152
|
+
refreshToken?: string;
|
|
153
|
+
idToken?: string;
|
|
154
|
+
/** Absolute UNIX millis. `0` or missing means unknown — treat as expired. */
|
|
155
|
+
expiresAt: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Pluggable token persistence.
|
|
160
|
+
*
|
|
161
|
+
* Consumers (web, native, server-side) provide an implementation tailored to
|
|
162
|
+
* their platform: `localStorage`, `sessionStorage`, `expo-secure-store`,
|
|
163
|
+
* `AsyncStorage`, or an in-memory map for tests. Keeping the interface narrow
|
|
164
|
+
* (read / write / clear) keeps the package transport-agnostic.
|
|
165
|
+
*/
|
|
166
|
+
interface TokenStorage {
|
|
167
|
+
read(): Promise<AuthTokens | null>;
|
|
168
|
+
write(tokens: AuthTokens): Promise<void>;
|
|
169
|
+
clear(): Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pluggable persistence for the `lastRefreshedAt` timestamp.
|
|
174
|
+
*
|
|
175
|
+
* Decoupled from `TokenStorage` so consumers can pick a different backend
|
|
176
|
+
* (e.g., write through `AsyncStorage` on RN where the secure store would
|
|
177
|
+
* gate every read on biometric).
|
|
178
|
+
*/
|
|
179
|
+
interface InactivityStore {
|
|
180
|
+
read(): Promise<number | null>;
|
|
181
|
+
write(timestamp: number): Promise<void>;
|
|
182
|
+
clear(): Promise<void>;
|
|
183
|
+
}
|
|
184
|
+
interface InactivityTrackerOptions {
|
|
185
|
+
store: InactivityStore;
|
|
186
|
+
/**
|
|
187
|
+
* Maximum days the user can be inactive (no successful refresh) before
|
|
188
|
+
* sessions are forcibly cleared. Default 90 (matches mobile decision).
|
|
189
|
+
*/
|
|
190
|
+
maxInactivityDays?: number;
|
|
191
|
+
/** Inject for tests; defaults to `Date.now`. */
|
|
192
|
+
now?: () => number;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Tracks the last time a refresh succeeded and decides whether the session
|
|
196
|
+
* has aged past its inactivity threshold.
|
|
197
|
+
*
|
|
198
|
+
* - `markActive(now?)` is called by `RefreshInterceptor` after every
|
|
199
|
+
* successful token swap.
|
|
200
|
+
* - `isExpired()` is called from `AuthClient.init()` at boot. If true,
|
|
201
|
+
* consumers clear tokens and emit `sessionExpired`.
|
|
202
|
+
*
|
|
203
|
+
* Choosing days (not e.g. minutes) makes the policy match what users
|
|
204
|
+
* understand: a session left untouched for 90 days needs re-auth.
|
|
205
|
+
*/
|
|
206
|
+
declare class InactivityTracker {
|
|
207
|
+
private readonly store;
|
|
208
|
+
private readonly maxInactivityMs;
|
|
209
|
+
private readonly now;
|
|
210
|
+
constructor(options: InactivityTrackerOptions);
|
|
211
|
+
markActive(timestamp?: number): Promise<void>;
|
|
212
|
+
getLastActive(): Promise<number | null>;
|
|
213
|
+
isExpired(): Promise<boolean>;
|
|
214
|
+
clear(): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The pluggable refresh function the interceptor calls when an access token
|
|
219
|
+
* is missing or expired. Implementations differ per transport:
|
|
220
|
+
*
|
|
221
|
+
* - **Mobile (SecureStore)**: posts to `/auth/refresh` with the refresh token
|
|
222
|
+
* from `AuthTokens.refreshToken`.
|
|
223
|
+
* - **Web (Cookie)**: posts to `/auth/refresh-cookie` with `credentials:
|
|
224
|
+
* 'include'` — the refresh token rides on the httpOnly cookie; `current`
|
|
225
|
+
* carries only the access token (refresh token will be undefined).
|
|
226
|
+
*
|
|
227
|
+
* Returns the new token bundle, or `null` when refresh failed in a way that
|
|
228
|
+
* means "session over" (e.g., 401 from the auth server).
|
|
229
|
+
*/
|
|
230
|
+
type RefreshFn = (current: AuthTokens | null) => Promise<AuthTokens | null>;
|
|
231
|
+
interface RefreshInterceptorOptions {
|
|
232
|
+
storage: TokenStorage;
|
|
233
|
+
refresh: RefreshFn;
|
|
234
|
+
events: AuthEventEmitter;
|
|
235
|
+
/**
|
|
236
|
+
* Optional callback fired AFTER tokens are persisted on a successful
|
|
237
|
+
* refresh. Used by `AuthClient` to update the inactivity tracker.
|
|
238
|
+
*/
|
|
239
|
+
onRefreshSuccess?: (tokens: AuthTokens) => Promise<void> | void;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Coordinates refresh-token swaps so concurrent 401s don't trigger N parallel
|
|
243
|
+
* refreshes. The first caller to hit `refreshTokens()` while no refresh is
|
|
244
|
+
* already in flight wins the role of "refresher"; everyone else awaits the
|
|
245
|
+
* same promise.
|
|
246
|
+
*
|
|
247
|
+
* On failure, storage is cleared and `sessionExpired` is emitted exactly once
|
|
248
|
+
* per refresh attempt.
|
|
249
|
+
*/
|
|
250
|
+
declare class RefreshInterceptor {
|
|
251
|
+
private readonly storage;
|
|
252
|
+
private readonly refresh;
|
|
253
|
+
private readonly events;
|
|
254
|
+
private readonly onRefreshSuccess;
|
|
255
|
+
private inflight;
|
|
256
|
+
constructor(options: RefreshInterceptorOptions);
|
|
257
|
+
/**
|
|
258
|
+
* Trigger (or join) a refresh. Returns the new tokens, or `null` if the
|
|
259
|
+
* refresh failed — in which case storage has already been cleared and
|
|
260
|
+
* `sessionExpired` already fired.
|
|
261
|
+
*/
|
|
262
|
+
refreshTokens(): Promise<AuthTokens | null>;
|
|
263
|
+
/**
|
|
264
|
+
* Whether a refresh is currently in flight. Exposed for tests / debug.
|
|
265
|
+
*/
|
|
266
|
+
get isRefreshing(): boolean;
|
|
267
|
+
private runRefresh;
|
|
268
|
+
private failHard;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Inputs to {@link AuthClient.fromIssuerUrl}.
|
|
273
|
+
*
|
|
274
|
+
* Used by consumers that store only an issuer URL and want to derive `realm`
|
|
275
|
+
* + `baseUrl` rather than configure them separately.
|
|
276
|
+
*/
|
|
277
|
+
interface AuthClientFromIssuerInput {
|
|
278
|
+
issuerUrl: string;
|
|
279
|
+
clientId: string;
|
|
280
|
+
redirectUri?: string;
|
|
281
|
+
scope?: string;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Optional collaborators wired into {@link AuthClient} for the v2 surface.
|
|
285
|
+
*
|
|
286
|
+
* - `api` enables `loginWith*`, `logout`, `requestPasswordReset`,
|
|
287
|
+
* `confirmPasswordReset`. Without it, those methods throw.
|
|
288
|
+
* - `interceptor` enables `init()` to silently refresh tokens at boot, and is
|
|
289
|
+
* used by `loginWithOtp/Password` to mark inactivity-active.
|
|
290
|
+
* - `inactivityTracker` enforces the 90-day timeout at `init()`.
|
|
291
|
+
*
|
|
292
|
+
* Consumers can omit any/all of these — the v1 PKCE / token-storage surface
|
|
293
|
+
* keeps working unchanged.
|
|
294
|
+
*/
|
|
295
|
+
interface AuthClientCollaborators {
|
|
296
|
+
api?: AuthApiClient;
|
|
297
|
+
interceptor?: RefreshInterceptor;
|
|
298
|
+
inactivityTracker?: InactivityTracker;
|
|
299
|
+
events?: AuthEventEmitter;
|
|
300
|
+
/**
|
|
301
|
+
* Observability hook fired when a fresh token bundle has been acquired
|
|
302
|
+
* (any login path: OTP, password, or direct-KC PKCE). For app-side
|
|
303
|
+
* analytics/logging only — NOT for BFF integration (Phase 2 designs that
|
|
304
|
+
* fresh).
|
|
305
|
+
*/
|
|
306
|
+
onTokenAcquired?: (tokens: AuthTokens) => void;
|
|
307
|
+
/**
|
|
308
|
+
* Observability hook fired when an existing token bundle has been
|
|
309
|
+
* refreshed. For app-side analytics/logging only.
|
|
310
|
+
*/
|
|
311
|
+
onTokenRefreshed?: (tokens: AuthTokens) => void;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Direct-to-KC (PKCE) routing flag added in v2.1.0.
|
|
315
|
+
*
|
|
316
|
+
* When `true`, `AuthClient` consumers can route their PKCE auth code through
|
|
317
|
+
* the shared OIDC primitives (`exchangeAuthorizationCodeViaOidc`,
|
|
318
|
+
* `refreshTokensViaOidc`) instead of the proxied identity-api `/auth/login`
|
|
319
|
+
* + `/auth/refresh` flow.
|
|
320
|
+
*
|
|
321
|
+
* Default `false` — v2.0 behavior unchanged.
|
|
322
|
+
*
|
|
323
|
+
* The flag is read-only at runtime (`isDirectMode()`) so apps can render
|
|
324
|
+
* conditionally on whether they've opted in.
|
|
325
|
+
*/
|
|
326
|
+
interface DirectKcOptions {
|
|
327
|
+
useDirectKcAuth?: boolean;
|
|
328
|
+
}
|
|
329
|
+
interface LoginOptions {
|
|
330
|
+
/** When true, request `offline_access` scope so the IdP issues a long-lived refresh token. */
|
|
331
|
+
offlineAccess?: boolean;
|
|
332
|
+
}
|
|
333
|
+
interface LogoutOptions {
|
|
334
|
+
/** Revoke all sessions on the IdP, not just the current one. */
|
|
335
|
+
everywhere?: boolean;
|
|
336
|
+
}
|
|
337
|
+
declare class AuthClient {
|
|
338
|
+
private readonly config;
|
|
339
|
+
private readonly directKcAuth;
|
|
340
|
+
private readonly tokenStorage;
|
|
341
|
+
private readonly api;
|
|
342
|
+
private readonly interceptor;
|
|
343
|
+
private readonly inactivityTracker;
|
|
344
|
+
private readonly events;
|
|
345
|
+
private readonly onTokenAcquired;
|
|
346
|
+
private readonly onTokenRefreshed;
|
|
347
|
+
/**
|
|
348
|
+
* @throws Error when `baseUrl`, `realm`, or `clientId` is missing or empty.
|
|
349
|
+
*/
|
|
350
|
+
constructor(config: AuthClientConfig & DirectKcOptions, storage: TokenStorage, collaborators?: AuthClientCollaborators);
|
|
351
|
+
/**
|
|
352
|
+
* Whether this client is configured to route auth flows directly to
|
|
353
|
+
* Keycloak (v2.1.0 direct-KC path) instead of through the proxied
|
|
354
|
+
* identity-api `/auth/*` endpoints.
|
|
355
|
+
*
|
|
356
|
+
* Apps can render conditionally on this — e.g. to swap a login form for
|
|
357
|
+
* a "Sign in with Keycloak" redirect button.
|
|
358
|
+
*/
|
|
359
|
+
isDirectMode(): boolean;
|
|
360
|
+
/**
|
|
361
|
+
* Persist a token bundle produced by an external flow (e.g. the
|
|
362
|
+
* app-side `useKeycloakExchange` hook that consumes the shared
|
|
363
|
+
* `exchangeAuthorizationCode` primitive). Fires `onTokenAcquired` after
|
|
364
|
+
* persistence and marks the inactivity tracker active.
|
|
365
|
+
*
|
|
366
|
+
* Designed for the v2.1.0 direct-KC path where the PKCE code exchange
|
|
367
|
+
* happens in the app's React-Query hook (which needs `useDispatch`/etc.)
|
|
368
|
+
* but the token persistence + observability should still flow through
|
|
369
|
+
* the shared client.
|
|
370
|
+
*/
|
|
371
|
+
acceptDirectKcTokens(response: TokenResponse): Promise<AuthTokens>;
|
|
372
|
+
/**
|
|
373
|
+
* Same as {@link acceptDirectKcTokens} but fires `onTokenRefreshed`.
|
|
374
|
+
* Use after a `refreshAccessToken()` swap to keep observability counts
|
|
375
|
+
* separated between "fresh login" and "silent refresh".
|
|
376
|
+
*/
|
|
377
|
+
acceptDirectKcRefresh(response: TokenResponse): Promise<AuthTokens>;
|
|
378
|
+
/**
|
|
379
|
+
* Build an {@link AuthClient} from a standalone issuer URL by parsing the
|
|
380
|
+
* realm and base URL. Useful when migrating from the legacy
|
|
381
|
+
* `KEYCLOAK_ISSUER` env var convention.
|
|
382
|
+
*
|
|
383
|
+
* @throws Error when the issuer URL doesn't match `{base}/realms/{realm}`.
|
|
384
|
+
*/
|
|
385
|
+
static fromIssuerUrl(input: AuthClientFromIssuerInput, storage: TokenStorage, collaborators?: AuthClientCollaborators): AuthClient;
|
|
386
|
+
private static validateConfig;
|
|
387
|
+
get realm(): string;
|
|
388
|
+
get clientId(): string;
|
|
389
|
+
get baseUrl(): string;
|
|
390
|
+
get scope(): string;
|
|
391
|
+
get redirectUri(): string | undefined;
|
|
392
|
+
/** Issuer URL: `{baseUrl}/realms/{realm}`. */
|
|
393
|
+
get issuerUrl(): string;
|
|
394
|
+
get authorizationEndpoint(): string;
|
|
395
|
+
get tokenEndpoint(): string;
|
|
396
|
+
get userInfoEndpoint(): string;
|
|
397
|
+
get logoutEndpoint(): string;
|
|
398
|
+
/**
|
|
399
|
+
* Build a fully-formed authorization URL the user agent can navigate to.
|
|
400
|
+
*
|
|
401
|
+
* @throws Error when `redirectUri` is not configured.
|
|
402
|
+
*/
|
|
403
|
+
buildAuthorizationUrl(input?: {
|
|
404
|
+
state?: string;
|
|
405
|
+
codeChallenge?: string;
|
|
406
|
+
codeChallengeMethod?: 'S256' | 'plain';
|
|
407
|
+
offlineAccess?: boolean;
|
|
408
|
+
}): string;
|
|
409
|
+
getTokens(): Promise<AuthTokens | null>;
|
|
410
|
+
setTokens(tokens: AuthTokens): Promise<void>;
|
|
411
|
+
clearTokens(): Promise<void>;
|
|
412
|
+
/**
|
|
413
|
+
* Read the current access token if it exists and is not expired.
|
|
414
|
+
* Returns `null` for "no usable token".
|
|
415
|
+
*/
|
|
416
|
+
getAccessToken(now?: number): Promise<string | null>;
|
|
417
|
+
/** Subscribe to lifecycle events (currently `sessionExpired` only). */
|
|
418
|
+
on(event: AuthEventName, listener: AuthEventListener): AuthEventUnsubscribe;
|
|
419
|
+
/**
|
|
420
|
+
* Boot-time wiring. Checks the inactivity tracker; if expired, clears
|
|
421
|
+
* tokens and emits `sessionExpired`. Returns whether a usable session
|
|
422
|
+
* survived.
|
|
423
|
+
*/
|
|
424
|
+
init(): Promise<{
|
|
425
|
+
hasSession: boolean;
|
|
426
|
+
}>;
|
|
427
|
+
/**
|
|
428
|
+
* Trigger a refresh via the configured interceptor. Returns the new tokens
|
|
429
|
+
* or `null` when the refresh failed (in which case `sessionExpired` has
|
|
430
|
+
* already fired).
|
|
431
|
+
*
|
|
432
|
+
* @throws Error when no interceptor is configured.
|
|
433
|
+
*/
|
|
434
|
+
refresh(): Promise<AuthTokens | null>;
|
|
435
|
+
loginWithOtp(input: {
|
|
436
|
+
email: string;
|
|
437
|
+
otp: string;
|
|
438
|
+
tenantId?: string;
|
|
439
|
+
} & LoginOptions): Promise<AuthTokens>;
|
|
440
|
+
loginWithPassword(input: {
|
|
441
|
+
email: string;
|
|
442
|
+
password: string;
|
|
443
|
+
tenantId?: string;
|
|
444
|
+
} & LoginOptions): Promise<AuthTokens>;
|
|
445
|
+
logout(options?: LogoutOptions): Promise<void>;
|
|
446
|
+
requestPasswordReset(input: {
|
|
447
|
+
email: string;
|
|
448
|
+
tenantId?: string;
|
|
449
|
+
}): Promise<void>;
|
|
450
|
+
confirmPasswordReset(input: {
|
|
451
|
+
token: string;
|
|
452
|
+
newPassword: string;
|
|
453
|
+
}): Promise<void>;
|
|
454
|
+
/** Internal: run a login HTTP call, persist tokens, mark inactivity-active. */
|
|
455
|
+
private runLogin;
|
|
456
|
+
private requireApi;
|
|
457
|
+
private resolveScope;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export { AuthApiClient as A, type DirectKcOptions as D, type ForgotPasswordRequest as F, type InactivityStore as I, type LoginOptions as L, type OtpLoginRequest as O, type PasswordLoginRequest as P, type ResetPasswordRequest as R, type TokenStorage as T, type AuthSessionInfo as a, AuthClient as b, type AuthTokens as c, type AuthApiClientOptions as d, type AuthClientCollaborators as e, type AuthClientConfig as f, type AuthClientFromIssuerInput as g, AuthEventEmitter as h, type AuthEventListener as i, type AuthEventName as j, type AuthEventUnsubscribe as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RawAuthLoginResponse as o, type RefreshFn as p, RefreshInterceptor as q, type RefreshInterceptorOptions as r };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HTTP transport the package depends on.
|
|
3
|
+
*
|
|
4
|
+
* `AuthClient` orchestrates token-related HTTP calls (login, refresh, logout,
|
|
5
|
+
* password reset) but doesn't import `fetch` directly — keeping the package
|
|
6
|
+
* runtime-agnostic. Consumers wire native fetch, axios, ky, or whatever their
|
|
7
|
+
* platform exposes.
|
|
8
|
+
*/
|
|
9
|
+
interface HttpRequest {
|
|
10
|
+
url: string;
|
|
11
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** When set, body is sent as the request body; serialization is the caller's job. */
|
|
14
|
+
body?: string;
|
|
15
|
+
/** Browser fetch only — pass through `credentials: 'include'` for cookie auth. */
|
|
16
|
+
credentials?: 'include' | 'same-origin' | 'omit';
|
|
17
|
+
}
|
|
18
|
+
interface HttpResponse {
|
|
19
|
+
status: number;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** Parsed body (already JSON-decoded). `undefined` for 204 / empty bodies. */
|
|
22
|
+
data?: unknown;
|
|
23
|
+
}
|
|
24
|
+
type HttpClient = (request: HttpRequest) => Promise<HttpResponse>;
|
|
25
|
+
/**
|
|
26
|
+
* Wrap the platform's native `fetch` into the package's `HttpClient` shape.
|
|
27
|
+
* Decoded JSON when `Content-Type` is JSON, otherwise leaves data undefined.
|
|
28
|
+
*
|
|
29
|
+
* Errors thrown by `fetch` (network / abort) are NOT swallowed — callers
|
|
30
|
+
* decide whether to treat them as session-ending.
|
|
31
|
+
*/
|
|
32
|
+
declare function createFetchHttpClient(fetchImpl: typeof fetch): HttpClient;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Raw token endpoint response (snake_case, OIDC standard).
|
|
36
|
+
*/
|
|
37
|
+
interface RawTokenResponse {
|
|
38
|
+
access_token: string;
|
|
39
|
+
refresh_token?: string;
|
|
40
|
+
id_token?: string;
|
|
41
|
+
expires_in?: number;
|
|
42
|
+
token_type?: string;
|
|
43
|
+
scope?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Application-friendly camelCase view of a token endpoint response.
|
|
48
|
+
*/
|
|
49
|
+
interface TokenResponse {
|
|
50
|
+
accessToken: string;
|
|
51
|
+
refreshToken?: string;
|
|
52
|
+
idToken?: string;
|
|
53
|
+
/** Seconds until expiry, as returned by Keycloak. */
|
|
54
|
+
expiresIn?: number;
|
|
55
|
+
tokenType?: string;
|
|
56
|
+
scope?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { type HttpClient as H, type RawTokenResponse as R, type TokenResponse as T, type HttpRequest as a, type HttpResponse as b, createFetchHttpClient as c };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HTTP transport the package depends on.
|
|
3
|
+
*
|
|
4
|
+
* `AuthClient` orchestrates token-related HTTP calls (login, refresh, logout,
|
|
5
|
+
* password reset) but doesn't import `fetch` directly — keeping the package
|
|
6
|
+
* runtime-agnostic. Consumers wire native fetch, axios, ky, or whatever their
|
|
7
|
+
* platform exposes.
|
|
8
|
+
*/
|
|
9
|
+
interface HttpRequest {
|
|
10
|
+
url: string;
|
|
11
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/** When set, body is sent as the request body; serialization is the caller's job. */
|
|
14
|
+
body?: string;
|
|
15
|
+
/** Browser fetch only — pass through `credentials: 'include'` for cookie auth. */
|
|
16
|
+
credentials?: 'include' | 'same-origin' | 'omit';
|
|
17
|
+
}
|
|
18
|
+
interface HttpResponse {
|
|
19
|
+
status: number;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** Parsed body (already JSON-decoded). `undefined` for 204 / empty bodies. */
|
|
22
|
+
data?: unknown;
|
|
23
|
+
}
|
|
24
|
+
type HttpClient = (request: HttpRequest) => Promise<HttpResponse>;
|
|
25
|
+
/**
|
|
26
|
+
* Wrap the platform's native `fetch` into the package's `HttpClient` shape.
|
|
27
|
+
* Decoded JSON when `Content-Type` is JSON, otherwise leaves data undefined.
|
|
28
|
+
*
|
|
29
|
+
* Errors thrown by `fetch` (network / abort) are NOT swallowed — callers
|
|
30
|
+
* decide whether to treat them as session-ending.
|
|
31
|
+
*/
|
|
32
|
+
declare function createFetchHttpClient(fetchImpl: typeof fetch): HttpClient;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Raw token endpoint response (snake_case, OIDC standard).
|
|
36
|
+
*/
|
|
37
|
+
interface RawTokenResponse {
|
|
38
|
+
access_token: string;
|
|
39
|
+
refresh_token?: string;
|
|
40
|
+
id_token?: string;
|
|
41
|
+
expires_in?: number;
|
|
42
|
+
token_type?: string;
|
|
43
|
+
scope?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Application-friendly camelCase view of a token endpoint response.
|
|
48
|
+
*/
|
|
49
|
+
interface TokenResponse {
|
|
50
|
+
accessToken: string;
|
|
51
|
+
refreshToken?: string;
|
|
52
|
+
idToken?: string;
|
|
53
|
+
/** Seconds until expiry, as returned by Keycloak. */
|
|
54
|
+
expiresIn?: number;
|
|
55
|
+
tokenType?: string;
|
|
56
|
+
scope?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { type HttpClient as H, type RawTokenResponse as R, type TokenResponse as T, type HttpRequest as a, type HttpResponse as b, createFetchHttpClient as c };
|