@dloizides/auth-client 3.0.0 → 3.3.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 +78 -0
- package/README.md +38 -1
- package/dist/{AuthClient-BGr8L03W.d.mts → AuthClient-3lu6Y1bY.d.mts} +2 -2
- package/dist/{AuthClient-D95OMajD.d.ts → AuthClient-Bb7N2shJ.d.ts} +2 -2
- package/dist/{TokenResponse-CY1CaU2l.d.mts → TokenResponse-BkIDjenX.d.mts} +7 -0
- package/dist/{TokenResponse-CY1CaU2l.d.ts → TokenResponse-BkIDjenX.d.ts} +7 -0
- package/dist/index.d.mts +222 -5
- package/dist/index.d.ts +222 -5
- package/dist/index.js +256 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +256 -6
- package/dist/index.mjs.map +1 -1
- package/dist/oidc/index.d.mts +1 -1
- package/dist/oidc/index.d.ts +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/package.json +116 -116
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.3.0 (2026-06-02)
|
|
4
|
+
|
|
5
|
+
Additive release for unified-login Increment 3 — device-bound PIN unlock +
|
|
6
|
+
`/bff/config`. Extracts kefi-web's proven device-PIN client (the "second use"),
|
|
7
|
+
giving `@dloizides/auth-web` 1.4.0 a shared same-origin client with
|
|
8
|
+
**discriminated** results. The published `login` / `pinLogin` throw an opaque
|
|
9
|
+
error on any non-2xx, collapsing 401 (wrong PIN) and 429 (locked / rate-limited);
|
|
10
|
+
the new device-PIN methods never throw and route on a discriminated `status`
|
|
11
|
+
instead. No breaking changes.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `BffAuthClient.getLoginConfig()` → `GET /bff/config`. Returns the advertised
|
|
16
|
+
login methods (lowercase, de-duplicated), `registrationEnabled`, and the
|
|
17
|
+
optional per-device state (`rememberedUsername`, `hasPin`, `pinDigits`,
|
|
18
|
+
`preferredMethod`). NEVER throws — on a non-2xx, a network error, or a
|
|
19
|
+
malformed body it returns a safe fallback (`['password']`, registration off,
|
|
20
|
+
empty device-state). Unauthenticated, no CSRF (GET).
|
|
21
|
+
- `BffAuthClient.enrollDevicePin({ pin, digits })` → `POST /bff/pin/enroll`.
|
|
22
|
+
Discriminated `DevicePinEnrollResult`: `success` (200), `unauthorized` (401),
|
|
23
|
+
`forbidden` (403, PIN-established session), `invalidPin` (400), `error`
|
|
24
|
+
(anything else / network). Never throws.
|
|
25
|
+
- `BffAuthClient.unlockWithDevicePin({ pin })` → `POST /bff/pin/unlock`.
|
|
26
|
+
Discriminated `DevicePinUnlockResult`: `success` + `user` (200), `invalid`
|
|
27
|
+
(401), `locked` (429 **with** a JSON body — device lockout), `rateLimited`
|
|
28
|
+
(429 with an **empty** body — per-IP limiter), `error` (anything else /
|
|
29
|
+
malformed 200 / network). Both 429 variants carry the parsed `Retry-After`
|
|
30
|
+
seconds (`null` when absent / unparseable). Never throws. Distinguishing
|
|
31
|
+
`locked` vs `rateLimited` lets UIs poll through rate limits but show lockout
|
|
32
|
+
copy.
|
|
33
|
+
- `BffAuthClient.disableDevicePin()` → `POST /bff/pin/disable`. `true` on a 2xx,
|
|
34
|
+
`false` otherwise. Never throws.
|
|
35
|
+
- Types: `BffLoginConfig`, `BffDeviceState`, `BffDevicePinEnrollRequest`,
|
|
36
|
+
`BffDevicePinUnlockRequest`, `DevicePinUnlockResult`, `DevicePinEnrollResult`.
|
|
37
|
+
- `HttpResponse.header?(name)` — an optional, case-insensitive response-header
|
|
38
|
+
accessor (the bundled `createFetchHttpClient` always provides it). Needed to
|
|
39
|
+
read `Retry-After` off the unlock `429`. Backward-compatible (optional).
|
|
40
|
+
|
|
41
|
+
## 3.2.0 (2026-05-22)
|
|
42
|
+
|
|
43
|
+
Additive release for Phase 3d of the unified-auth plan — event-scoped PIN
|
|
44
|
+
login. Extends `BffAuthClient` with the browser-facing PIN call so the new
|
|
45
|
+
`<PinForm>` in `@dloizides/auth-web` has a same-origin client. No breaking
|
|
46
|
+
changes.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `BffAuthClient.pinLogin({ pin, eventExternalId })` → `POST /bff/pin/login`.
|
|
51
|
+
The BFF runs the event-scoped PIN direct-grant against Keycloak server-side
|
|
52
|
+
(the `(event, pin)` pair resolves to the staff member's KC account + their
|
|
53
|
+
event-scoped role) and sets the httpOnly session cookie. Returns the
|
|
54
|
+
sanitised `BffUser`, exactly like `login` / `verifyOtp`. Throws on a non-2xx
|
|
55
|
+
(`401` for a bad / expired / locked-out PIN or an unknown event, `501` when
|
|
56
|
+
PIN login is not an enabled method). Carries the `X-BFF-Csrf` header like
|
|
57
|
+
every other state-changing call. No `username` / `password` ever leaves the
|
|
58
|
+
browser.
|
|
59
|
+
- Type: `BffPinLoginRequest`.
|
|
60
|
+
|
|
61
|
+
## 3.1.0 (2026-05-22)
|
|
62
|
+
|
|
63
|
+
Additive release for Phase 2d of the unified-auth plan — email-OTP. Extends
|
|
64
|
+
`BffAuthClient` with the two browser-facing OTP calls so the new `<OtpForm>` in
|
|
65
|
+
`@dloizides/auth-web` has a same-origin client. No breaking changes.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- `BffAuthClient.requestOtp({ identifier })` → `POST /bff/otp/request`. The BFF
|
|
70
|
+
proxies to TenantService, which emails a short-TTL code. The endpoint is
|
|
71
|
+
anti-enumeration (a `200` is the normal path), so the method **returns** the
|
|
72
|
+
relayed `{ success, expiresIn, code }` body — the UI uses `expiresIn` for a
|
|
73
|
+
countdown. It still throws on a non-2xx (`501` OTP not enabled, `502` upstream
|
|
74
|
+
down). Carries the `X-BFF-Csrf` header like every other state-changing call.
|
|
75
|
+
- `BffAuthClient.verifyOtp({ username, otp })` → `POST /bff/otp/verify`. The BFF
|
|
76
|
+
runs the OTP direct-grant against Keycloak server-side and sets the httpOnly
|
|
77
|
+
session cookie. Returns the sanitised `BffUser`, exactly like `login`. Throws
|
|
78
|
+
on a non-2xx (e.g. `401` for a bad / expired code).
|
|
79
|
+
- Types: `BffOtpRequestRequest`, `BffOtpVerifyRequest`, `BffOtpRequestResult`.
|
|
80
|
+
|
|
3
81
|
## 3.0.0 (2026-05-19)
|
|
4
82
|
|
|
5
83
|
Major release for Phase 2 of the identity-hardening initiative. Adds the
|
package/README.md
CHANGED
|
@@ -151,6 +151,43 @@ await bff.resetPassword({ token, newPassword });
|
|
|
151
151
|
await bff.logout();
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
### Device-bound PIN unlock (v3.3 — unified-login Increment 3)
|
|
155
|
+
|
|
156
|
+
A returning, remembered-device, logged-OUT user can re-establish a session with a
|
|
157
|
+
4/6/8-digit device PIN. Unlike `login` / `pinLogin` (which throw an opaque error on
|
|
158
|
+
any non-2xx), the device-PIN methods **never throw** — they return discriminated
|
|
159
|
+
results so the UI can route on `status`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Which methods does this BFF advertise + does this device remember a PIN?
|
|
163
|
+
// NEVER throws — safe fallback (['password'], registration off) on any failure.
|
|
164
|
+
const config = await bff.getLoginConfig();
|
|
165
|
+
if (config.deviceState.hasPin) {
|
|
166
|
+
/* render the device-PIN unlock screen */
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Bind a PIN to the current strong session.
|
|
170
|
+
const enroll = await bff.enrollDevicePin({ pin: '482913', digits: 6 });
|
|
171
|
+
// status: 'success' | 'unauthorized' | 'forbidden' | 'invalidPin' | 'error'
|
|
172
|
+
|
|
173
|
+
// Re-establish a session from a remembered device.
|
|
174
|
+
const unlock = await bff.unlockWithDevicePin({ pin: '482913' });
|
|
175
|
+
switch (unlock.status) {
|
|
176
|
+
case 'success': /* unlock.user — a session cookie was set */ break;
|
|
177
|
+
case 'invalid': /* wrong PIN / unknown-or-revoked device */ break;
|
|
178
|
+
case 'locked': /* device lockout — unlock.retryAfterSeconds */ break;
|
|
179
|
+
case 'rateLimited': /* per-IP limiter — may poll through it */ break;
|
|
180
|
+
case 'error': /* network / unexpected */ break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await bff.disableDevicePin(); // true on success, never throws
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The two `429` outcomes are distinct on purpose: the per-IP `BffAuth` limiter
|
|
187
|
+
answers `429` with an **empty** body (`rateLimited` — a UI may poll through it),
|
|
188
|
+
whereas the device-PIN lockout answers `429` with a JSON `{ error }` body + a
|
|
189
|
+
`Retry-After` header (`locked` — show a "try again in N s" message).
|
|
190
|
+
|
|
154
191
|
The direct-KC `AuthClient` / ROPC surface below is retained for consumers not
|
|
155
192
|
yet on a BFF; it is deprecated and removed once every app has migrated.
|
|
156
193
|
|
|
@@ -191,7 +228,7 @@ auth.on('sessionExpired', () => {
|
|
|
191
228
|
|
|
192
229
|
### Core (`@dloizides/auth-client`)
|
|
193
230
|
|
|
194
|
-
- `BffAuthClient` — same-origin client for a per-app Backend-For-Frontend. `login()`, `logout()`, `getCurrentUser()`, `register()`, `forgotPassword()`, `resetPassword()
|
|
231
|
+
- `BffAuthClient` — same-origin client for a per-app Backend-For-Frontend. `login()`, `logout()`, `getCurrentUser()`, `register()`, `forgotPassword()`, `resetPassword()`, `requestOtp()`, `verifyOtp()`, `pinLogin()`, plus the v3.3 device-PIN surface: `getLoginConfig()`, `enrollDevicePin()`, `unlockWithDevicePin()`, `disableDevicePin()` (discriminated, never-throwing results). No token handling — the BFF owns tokens, the browser owns only an httpOnly cookie. **The recommended auth surface (v3).**
|
|
195
232
|
- `AuthClient` — realm-aware orchestrator. `init()`, `refresh()`, `loginWithOtp()`, `loginWithPassword()`, `logout({ everywhere })`, `requestPasswordReset()`, `confirmPasswordReset()`, plus the v1 surface (`getAccessToken`, `getTokens`, `setTokens`, `clearTokens`, `buildAuthorizationUrl`, etc.). Direct-KC ROPC; deprecated in favour of `BffAuthClient`.
|
|
196
233
|
- `AuthApiClient` — typed wrapper for IdentityService auth endpoints.
|
|
197
234
|
- `AuthEventEmitter` — `sessionExpired` event.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HttpClient, T as TokenResponse } from './TokenResponse-
|
|
1
|
+
import { H as HttpClient, T as TokenResponse } from './TokenResponse-BkIDjenX.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Tiny dependency-free event emitter for auth lifecycle events.
|
|
@@ -457,4 +457,4 @@ declare class AuthClient {
|
|
|
457
457
|
private resolveScope;
|
|
458
458
|
}
|
|
459
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
|
|
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 RawAuthLoginResponse as R, type TokenStorage as T, type AuthApiClientOptions as a, AuthClient as b, type AuthClientCollaborators as c, type AuthClientConfig as d, type AuthClientFromIssuerInput as e, AuthEventEmitter as f, type AuthEventListener as g, type AuthEventName as h, type AuthEventUnsubscribe as i, type AuthSessionInfo as j, type AuthTokens as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RefreshFn as o, RefreshInterceptor as p, type RefreshInterceptorOptions as q, type ResetPasswordRequest as r };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HttpClient, T as TokenResponse } from './TokenResponse-
|
|
1
|
+
import { H as HttpClient, T as TokenResponse } from './TokenResponse-BkIDjenX.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Tiny dependency-free event emitter for auth lifecycle events.
|
|
@@ -457,4 +457,4 @@ declare class AuthClient {
|
|
|
457
457
|
private resolveScope;
|
|
458
458
|
}
|
|
459
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
|
|
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 RawAuthLoginResponse as R, type TokenStorage as T, type AuthApiClientOptions as a, AuthClient as b, type AuthClientCollaborators as c, type AuthClientConfig as d, type AuthClientFromIssuerInput as e, AuthEventEmitter as f, type AuthEventListener as g, type AuthEventName as h, type AuthEventUnsubscribe as i, type AuthSessionInfo as j, type AuthTokens as k, InactivityTracker as l, type InactivityTrackerOptions as m, type LogoutOptions as n, type RefreshFn as o, RefreshInterceptor as p, type RefreshInterceptorOptions as q, type ResetPasswordRequest as r };
|
|
@@ -20,6 +20,13 @@ interface HttpResponse {
|
|
|
20
20
|
ok: boolean;
|
|
21
21
|
/** Parsed body (already JSON-decoded). `undefined` for 204 / empty bodies. */
|
|
22
22
|
data?: unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Read a single response header by (case-insensitive) name, or `null` when
|
|
25
|
+
* absent. Optional so existing transports stay source-compatible; the bundled
|
|
26
|
+
* `createFetchHttpClient` always provides it. The device-PIN unlock flow needs
|
|
27
|
+
* it to read `Retry-After` off a `429`.
|
|
28
|
+
*/
|
|
29
|
+
header?: (name: string) => string | null;
|
|
23
30
|
}
|
|
24
31
|
type HttpClient = (request: HttpRequest) => Promise<HttpResponse>;
|
|
25
32
|
/**
|
|
@@ -20,6 +20,13 @@ interface HttpResponse {
|
|
|
20
20
|
ok: boolean;
|
|
21
21
|
/** Parsed body (already JSON-decoded). `undefined` for 204 / empty bodies. */
|
|
22
22
|
data?: unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Read a single response header by (case-insensitive) name, or `null` when
|
|
25
|
+
* absent. Optional so existing transports stay source-compatible; the bundled
|
|
26
|
+
* `createFetchHttpClient` always provides it. The device-PIN unlock flow needs
|
|
27
|
+
* it to read `Retry-After` off a `429`.
|
|
28
|
+
*/
|
|
29
|
+
header?: (name: string) => string | null;
|
|
23
30
|
}
|
|
24
31
|
type HttpClient = (request: HttpRequest) => Promise<HttpResponse>;
|
|
25
32
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { T as TokenStorage,
|
|
2
|
-
export { A as AuthApiClient,
|
|
1
|
+
import { T as TokenStorage, k as AuthTokens } from './AuthClient-3lu6Y1bY.mjs';
|
|
2
|
+
export { A as AuthApiClient, a as AuthApiClientOptions, b as AuthClient, c as AuthClientCollaborators, d as AuthClientConfig, e as AuthClientFromIssuerInput, f as AuthEventEmitter, g as AuthEventListener, h as AuthEventName, i as AuthEventUnsubscribe, j 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, R as RawAuthLoginResponse, o as RefreshFn, p as RefreshInterceptor, q as RefreshInterceptorOptions, r as ResetPasswordRequest } from './AuthClient-3lu6Y1bY.mjs';
|
|
3
3
|
export { ExchangeAuthorizationCodeInput, FetchDiscoveryDocumentInput, OidcDiscoveryDocument, PkcePair, RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken } from './oidc/index.mjs';
|
|
4
|
-
import { H as HttpClient, R as RawTokenResponse, T as TokenResponse } from './TokenResponse-
|
|
5
|
-
export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-
|
|
4
|
+
import { H as HttpClient, R as RawTokenResponse, T as TokenResponse } from './TokenResponse-BkIDjenX.mjs';
|
|
5
|
+
export { a as HttpRequest, b as HttpResponse, c as createFetchHttpClient } from './TokenResponse-BkIDjenX.mjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Roles emitted by Keycloak realms in the dloizides.com portfolio.
|
|
@@ -342,6 +342,143 @@ interface BffResetPasswordRequest {
|
|
|
342
342
|
token: string;
|
|
343
343
|
newPassword: string;
|
|
344
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Payload for `POST /bff/otp/request` — the BFF proxies it to TenantService,
|
|
347
|
+
* which generates a short-TTL code and emails it.
|
|
348
|
+
*/
|
|
349
|
+
interface BffOtpRequestRequest {
|
|
350
|
+
/** The email address (or username) the one-time code is sent to. */
|
|
351
|
+
identifier: string;
|
|
352
|
+
}
|
|
353
|
+
/** Payload for `POST /bff/otp/verify` — the BFF exchanges it for a session. */
|
|
354
|
+
interface BffOtpVerifyRequest {
|
|
355
|
+
/** The email / username the code was requested for. */
|
|
356
|
+
username: string;
|
|
357
|
+
/** The one-time code the user entered. */
|
|
358
|
+
otp: string;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Payload for `POST /bff/pin/login` — the BFF exchanges an event-scoped PIN
|
|
362
|
+
* for a session.
|
|
363
|
+
*
|
|
364
|
+
* The `(event, pin)` pair alone identifies the staff member: no `username` /
|
|
365
|
+
* `password` ever leaves the browser. A PIN entered in an event's context
|
|
366
|
+
* grants that staff member their event-scoped role for that event only
|
|
367
|
+
* (the unified-auth plan §4.4 — event-scoped, per-individual PINs).
|
|
368
|
+
*/
|
|
369
|
+
interface BffPinLoginRequest {
|
|
370
|
+
/** The numeric PIN the staff member entered. */
|
|
371
|
+
pin: string;
|
|
372
|
+
/** External id of the event the PIN is scoped to (supplied by the page/route). */
|
|
373
|
+
eventExternalId: string;
|
|
374
|
+
}
|
|
375
|
+
/** Payload for `POST /bff/pin/enroll` — bind a device PIN to the current session. */
|
|
376
|
+
interface BffDevicePinEnrollRequest {
|
|
377
|
+
/** The numeric PIN the user chose. */
|
|
378
|
+
pin: string;
|
|
379
|
+
/** The chosen PIN length (must be one of 4 / 6 / 8). */
|
|
380
|
+
digits: number;
|
|
381
|
+
}
|
|
382
|
+
/** Payload for `POST /bff/pin/unlock` — re-establish a session from a remembered device. */
|
|
383
|
+
interface BffDevicePinUnlockRequest {
|
|
384
|
+
/** The numeric device PIN the returning user entered. */
|
|
385
|
+
pin: string;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The optional per-device state half of `GET /bff/config`, all fields safe-defaulted.
|
|
389
|
+
*
|
|
390
|
+
* Read off the BFF's per-device record (keyed by the device cookie). Older BFFs /
|
|
391
|
+
* tests omit the fields entirely, in which case the PIN-unlock gate simply never
|
|
392
|
+
* triggers (`hasPin` defaults to `false`, `rememberedUsername` to `null`).
|
|
393
|
+
*/
|
|
394
|
+
interface BffDeviceState {
|
|
395
|
+
/** Non-secret username this device remembers, or `null` when none. */
|
|
396
|
+
rememberedUsername: string | null;
|
|
397
|
+
/** `true` when this device has an enrolled device PIN. */
|
|
398
|
+
hasPin: boolean;
|
|
399
|
+
/** The enrolled PIN length (4 / 6 / 8), or `null` when unknown / no PIN. */
|
|
400
|
+
pinDigits: number | null;
|
|
401
|
+
/** The device-local preferred-method hint (e.g. `"pin"`), or `null`. */
|
|
402
|
+
preferredMethod: string | null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The parsed `GET /bff/config` response: which login methods this BFF advertises,
|
|
406
|
+
* whether self-serve registration is enabled, and the optional per-device state.
|
|
407
|
+
*
|
|
408
|
+
* `methods` are the lowercase strings the server-side `BffLoginMethod` enum
|
|
409
|
+
* serialises to (`"password"` | `"otp"` | `"pin"` | `"passkey"`), de-duplicated
|
|
410
|
+
* and order-preserving. On a network failure or malformed body the client returns
|
|
411
|
+
* a safe fallback (`["password"]`, registration off, empty device-state).
|
|
412
|
+
*/
|
|
413
|
+
interface BffLoginConfig {
|
|
414
|
+
/** The enabled login methods, lowercase, in the order the BFF advertised them. */
|
|
415
|
+
methods: string[];
|
|
416
|
+
/** `true` when this BFF exposes self-serve registration. */
|
|
417
|
+
registrationEnabled: boolean;
|
|
418
|
+
/** The optional per-device state (remembered username + device PIN), safe-defaulted. */
|
|
419
|
+
deviceState: BffDeviceState;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Discriminated result of `unlockWithDevicePin`. Never rejects — the unlock UI
|
|
423
|
+
* routes on `status` (the published `login`/`pinLogin` throw an opaque error on
|
|
424
|
+
* any non-2xx, collapsing 401 vs 429; this is why the device-PIN flow needs its
|
|
425
|
+
* own client surface).
|
|
426
|
+
*
|
|
427
|
+
* The two `429` outcomes are distinct and MUST stay distinct: the BFF's per-IP
|
|
428
|
+
* `BffAuth` sliding-window limiter answers `429` with an EMPTY body
|
|
429
|
+
* (`rateLimited` — the UI may poll through it), whereas the device-PIN lockout
|
|
430
|
+
* answers `429` with a JSON `{ error }` body + a `Retry-After` header (`locked` —
|
|
431
|
+
* the UI shows a "try again in N s" message).
|
|
432
|
+
*/
|
|
433
|
+
type DevicePinUnlockResult = {
|
|
434
|
+
status: 'success';
|
|
435
|
+
user: BffUser;
|
|
436
|
+
} | {
|
|
437
|
+
status: 'invalid';
|
|
438
|
+
} | {
|
|
439
|
+
status: 'locked';
|
|
440
|
+
retryAfterSeconds: number | null;
|
|
441
|
+
} | {
|
|
442
|
+
status: 'rateLimited';
|
|
443
|
+
retryAfterSeconds: number | null;
|
|
444
|
+
} | {
|
|
445
|
+
status: 'error';
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* Discriminated result of `enrollDevicePin`. Never rejects — the enrol form routes
|
|
449
|
+
* on `status`:
|
|
450
|
+
* - `success` — HTTP 200, the device PIN is bound to the current session;
|
|
451
|
+
* - `unauthorized` — HTTP 401, no session to bind to;
|
|
452
|
+
* - `forbidden` — HTTP 403, a PIN-established session can't enrol a new PIN;
|
|
453
|
+
* - `invalidPin` — HTTP 400, the PIN format was rejected;
|
|
454
|
+
* - `error` — anything else (501 disabled / 502 grant failed / network).
|
|
455
|
+
*/
|
|
456
|
+
type DevicePinEnrollResult = {
|
|
457
|
+
status: 'success';
|
|
458
|
+
} | {
|
|
459
|
+
status: 'unauthorized';
|
|
460
|
+
} | {
|
|
461
|
+
status: 'forbidden';
|
|
462
|
+
} | {
|
|
463
|
+
status: 'invalidPin';
|
|
464
|
+
} | {
|
|
465
|
+
status: 'error';
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* The body `POST /bff/otp/request` relays from TenantService.
|
|
469
|
+
*
|
|
470
|
+
* Anti-enumeration: the shape is identical whether or not the identifier is
|
|
471
|
+
* registered. `code` is non-null only outside production (a dev convenience);
|
|
472
|
+
* the UI must never depend on it being present.
|
|
473
|
+
*/
|
|
474
|
+
interface BffOtpRequestResult {
|
|
475
|
+
/** Always `true` on a relayed 200 — the request was accepted. */
|
|
476
|
+
success: boolean;
|
|
477
|
+
/** Seconds until the emitted code expires — drives a countdown in the UI. */
|
|
478
|
+
expiresIn: number;
|
|
479
|
+
/** The code itself, non-production only; `null` (or absent) in production. */
|
|
480
|
+
code: string | null;
|
|
481
|
+
}
|
|
345
482
|
/**
|
|
346
483
|
* The user object returned by `GET /bff/me` and `POST /bff/login`. The BFF
|
|
347
484
|
* returns the sanitised KC claims under a `user` envelope and **never** a
|
|
@@ -411,6 +548,86 @@ declare class BffAuthClient {
|
|
|
411
548
|
* response (e.g. `400` for an invalid / expired token).
|
|
412
549
|
*/
|
|
413
550
|
resetPassword(request: BffResetPasswordRequest): Promise<void>;
|
|
551
|
+
/**
|
|
552
|
+
* `POST /bff/otp/request` — the BFF proxies to TenantService, which generates
|
|
553
|
+
* a short-TTL code and emails it.
|
|
554
|
+
*
|
|
555
|
+
* The endpoint is anti-enumeration: a `200` is the normal path whether or not
|
|
556
|
+
* the identifier is registered. This method therefore **returns** the relayed
|
|
557
|
+
* `{ success, expiresIn, code }` body (so the UI can show the expiry) rather
|
|
558
|
+
* than treating a 200 as opaque. It still throws on a non-2xx — a `501`
|
|
559
|
+
* (OTP not enabled) or `502` (upstream down) is a real failure to surface.
|
|
560
|
+
*/
|
|
561
|
+
requestOtp(request: BffOtpRequestRequest): Promise<BffOtpRequestResult>;
|
|
562
|
+
/**
|
|
563
|
+
* `POST /bff/otp/verify` — the BFF runs the OTP direct-grant against Keycloak
|
|
564
|
+
* server-side, stores the tokens in its Redis vault, and sets the httpOnly
|
|
565
|
+
* session cookie. Returns the sanitised user, exactly like `login`. Throws on
|
|
566
|
+
* a non-2xx (e.g. `401` for a bad / expired code).
|
|
567
|
+
*/
|
|
568
|
+
verifyOtp(request: BffOtpVerifyRequest): Promise<BffUser>;
|
|
569
|
+
/**
|
|
570
|
+
* `POST /bff/pin/login` — the BFF runs the event-scoped PIN direct-grant
|
|
571
|
+
* against Keycloak server-side (the `(event, pin)` pair resolves to the
|
|
572
|
+
* staff member's KC account + event-scoped role), stores the tokens in its
|
|
573
|
+
* Redis vault, and sets the httpOnly session cookie. Returns the sanitised
|
|
574
|
+
* user, exactly like `login` / `verifyOtp`. Throws on a non-2xx — `401` for
|
|
575
|
+
* a bad / expired / locked-out PIN or an unknown event, `501` when PIN login
|
|
576
|
+
* is not an enabled method for this BFF.
|
|
577
|
+
*/
|
|
578
|
+
pinLogin(request: BffPinLoginRequest): Promise<BffUser>;
|
|
579
|
+
/**
|
|
580
|
+
* `GET /bff/config` — which login methods this BFF advertises, whether
|
|
581
|
+
* registration is enabled, and the optional per-device state (remembered
|
|
582
|
+
* username + device PIN). Unauthenticated, no CSRF (GET).
|
|
583
|
+
*
|
|
584
|
+
* NEVER throws: on a non-2xx, a network error, or a malformed body it returns
|
|
585
|
+
* a safe fallback (`{ methods: ['password'], registrationEnabled: false,
|
|
586
|
+
* deviceState: { rememberedUsername: null, hasPin: false, pinDigits: null,
|
|
587
|
+
* preferredMethod: null } }`) so the login surface stays usable even when the
|
|
588
|
+
* config endpoint is unreachable.
|
|
589
|
+
*/
|
|
590
|
+
getLoginConfig(): Promise<BffLoginConfig>;
|
|
591
|
+
/**
|
|
592
|
+
* `POST /bff/pin/enroll` — bind a device PIN to the CURRENT strong session.
|
|
593
|
+
* Requires an authenticated session (the cookie travels via
|
|
594
|
+
* `credentials: 'include'`); the BFF requests an offline token, hashes the
|
|
595
|
+
* PIN, and sets the device cookie.
|
|
596
|
+
*
|
|
597
|
+
* NEVER throws — resolves a discriminated {@link DevicePinEnrollResult}:
|
|
598
|
+
* - 200 → `success`;
|
|
599
|
+
* - 401 → `unauthorized` (no session);
|
|
600
|
+
* - 403 → `forbidden` (a PIN-established session can't enrol);
|
|
601
|
+
* - 400 → `invalidPin` (bad format);
|
|
602
|
+
* - anything else (501 disabled / 502 grant failed) / network → `error`.
|
|
603
|
+
*/
|
|
604
|
+
enrollDevicePin(request: BffDevicePinEnrollRequest): Promise<DevicePinEnrollResult>;
|
|
605
|
+
/**
|
|
606
|
+
* `POST /bff/pin/unlock` — re-establish a session from a remembered device.
|
|
607
|
+
* No prior session; the device cookie travels via `credentials: 'include'`.
|
|
608
|
+
*
|
|
609
|
+
* NEVER throws — resolves a discriminated {@link DevicePinUnlockResult}:
|
|
610
|
+
* - 200 + `{ user }` → `success` (a session cookie was set);
|
|
611
|
+
* - 401 → `invalid` (wrong PIN / unknown-or-revoked device);
|
|
612
|
+
* - 429 + JSON body → `locked` (device lockout; `Retry-After` parsed);
|
|
613
|
+
* - 429 + empty body → `rateLimited` (per-IP limiter; `Retry-After` parsed);
|
|
614
|
+
* - anything else / malformed 200 body / network → `error`.
|
|
615
|
+
*/
|
|
616
|
+
unlockWithDevicePin(request: BffDevicePinUnlockRequest): Promise<DevicePinUnlockResult>;
|
|
617
|
+
/**
|
|
618
|
+
* `POST /bff/pin/disable` — drop the device PIN for the CURRENT session. The
|
|
619
|
+
* BFF revokes the offline token at Keycloak, deletes the device record, and
|
|
620
|
+
* clears the device cookie. Requires an authenticated session.
|
|
621
|
+
*
|
|
622
|
+
* NEVER throws — resolves `true` on a 2xx, `false` on anything else.
|
|
623
|
+
*/
|
|
624
|
+
disableDevicePin(): Promise<boolean>;
|
|
625
|
+
/**
|
|
626
|
+
* Shared POST for the never-throw device-PIN calls: same-origin, cookie
|
|
627
|
+
* included, `X-BFF-Csrf` header attached. Returns the raw {@link HttpResponse}
|
|
628
|
+
* (status + body + headers) so the caller can route on it instead of throwing.
|
|
629
|
+
*/
|
|
630
|
+
private postRaw;
|
|
414
631
|
/**
|
|
415
632
|
* Shared POST for every state-changing `/bff/*` call: same-origin, cookie
|
|
416
633
|
* included, `X-BFF-Csrf` header attached. Throws a labelled error on non-2xx.
|
|
@@ -591,4 +808,4 @@ declare function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse;
|
|
|
591
808
|
*/
|
|
592
809
|
declare function tokenResponseToAuthTokens(response: TokenResponse, now?: number): AuthTokens;
|
|
593
810
|
|
|
594
|
-
export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffForgotPasswordRequest, type BffLoginRequest, type BffRegisterRequest, type BffResetPasswordRequest, type BffUser, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, HttpClient, 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 };
|
|
811
|
+
export { AuthTokens, type AuthorizationCodeBodyInput, type AuthorizationResponseLike, type AuthorizationUrlInput, BffAuthClient, type BffAuthClientOptions, type BffDevicePinEnrollRequest, type BffDevicePinUnlockRequest, type BffDeviceState, type BffForgotPasswordRequest, type BffLoginConfig, type BffLoginRequest, type BffOtpRequestRequest, type BffOtpRequestResult, type BffOtpVerifyRequest, type BffPinLoginRequest, type BffRegisterRequest, type BffResetPasswordRequest, type BffUser, type BiometricFlagStore, BiometricGate, type BiometricGateLike, type BiometricGateOptions, BrowserStorageTokenStorage, type BrowserStorageTokenStorageOptions, CookieTokenStorage, type DevicePinEnrollResult, type DevicePinUnlockResult, HttpClient, 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 };
|