@dloizides/auth-web 1.3.0 → 1.4.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
@@ -115,6 +115,123 @@ interface PinFormLabels {
115
115
  /** Generic "that PIN was wrong / expired / locked out" message. */
116
116
  invalidPin: string;
117
117
  }
118
+ /**
119
+ * Strings rendered by `<DevicePinUnlockScreen>` — the returning-device unlock
120
+ * gate. `{name}` / `{count}` placeholders are substituted by the component.
121
+ */
122
+ interface DevicePinUnlockLabels {
123
+ /** Greeting with the remembered username; `{name}` is substituted. */
124
+ title: string;
125
+ /** Greeting when no username is remembered. */
126
+ titleNoName: string;
127
+ /** Copy above the PIN pad; `{count}` is substituted with the PIN length. */
128
+ description: string;
129
+ pinLabel: string;
130
+ pinPlaceholder: string;
131
+ /** The "unlock" submit button. */
132
+ submit: string;
133
+ /** Shown while the unlock is in flight. */
134
+ submitting: string;
135
+ /** The "sign in with password instead" escape link. */
136
+ usePasswordInstead: string;
137
+ /** Accessibility hint for the escape link. */
138
+ usePasswordHint: string;
139
+ /** Error: not enough digits entered; `{count}` is substituted. */
140
+ errorIncomplete: string;
141
+ /** Error: wrong PIN / unknown device (generic). */
142
+ errorInvalid: string;
143
+ /** Error: locked out, no retry hint. */
144
+ errorLockedOut: string;
145
+ /** Error: locked out with a retry hint; `{count}` is the seconds. */
146
+ errorLockedOutRetry: string;
147
+ /** Error: rate-limited, no retry hint. */
148
+ errorRateLimited: string;
149
+ /** Error: rate-limited with a retry hint; `{count}` is the seconds. */
150
+ errorRateLimitedRetry: string;
151
+ /** Error: anything else (network / unexpected). */
152
+ errorGeneric: string;
153
+ /** Accessibility label for a filled PIN dot. */
154
+ digitFilledHint: string;
155
+ /** Accessibility label for an empty PIN dot. */
156
+ digitEmptyHint: string;
157
+ }
158
+ /**
159
+ * Strings rendered by `<DevicePinEnrollForm>` (and the length picker it hosts)
160
+ * plus the `<DevicePinOffer>` wrapper. `{count}` placeholders are substituted.
161
+ */
162
+ interface DevicePinEnrollLabels {
163
+ offerTitle: string;
164
+ offerDescription: string;
165
+ offerAccept: string;
166
+ offerAcceptHint: string;
167
+ offerSkip: string;
168
+ offerSkipHint: string;
169
+ formTitle: string;
170
+ /** Copy above the form; `{count}` is substituted with the chosen length. */
171
+ formDescription: string;
172
+ lengthLabel: string;
173
+ /** Accessibility hint for a length pill; `{count}` is substituted. */
174
+ lengthOptionHint: string;
175
+ pinLabel: string;
176
+ pinPlaceholder: string;
177
+ confirmLabel: string;
178
+ confirmPlaceholder: string;
179
+ submit: string;
180
+ submitting: string;
181
+ cancel: string;
182
+ cancelHint: string;
183
+ /** Error: PINs do not match / wrong length; `{count}` is substituted. */
184
+ errorMismatch: string;
185
+ /** Error: the current session is not authenticated. */
186
+ errorUnauthorized: string;
187
+ /** Error: the session already has a PIN / is forbidden. */
188
+ errorForbidden: string;
189
+ /** Error: the PIN failed server-side validation. */
190
+ errorInvalidPin: string;
191
+ /** Error: any other failure. */
192
+ errorFailed: string;
193
+ }
194
+ /** Strings rendered by `<DevicePinSettingsCard>` — the account toggle. */
195
+ interface DevicePinSettingsLabels {
196
+ title: string;
197
+ description: string;
198
+ /** Status line when a device PIN is enabled. */
199
+ statusEnabled: string;
200
+ /** Status line when no device PIN is set. */
201
+ statusDisabled: string;
202
+ /** The "enable" button. */
203
+ enable: string;
204
+ enableHint: string;
205
+ /** The "disable" button. */
206
+ disable: string;
207
+ disableHint: string;
208
+ /** Shown while disabling is in flight. */
209
+ disabling: string;
210
+ /** Error shown when disabling fails. */
211
+ disableFailed: string;
212
+ }
213
+ /** Strings rendered by `<PasskeyLoginButton>` — the login-surface passkey CTA. */
214
+ interface PasskeyLoginLabels {
215
+ /** The "sign in with a passkey" button. */
216
+ signInButton: string;
217
+ signInHint: string;
218
+ /** Error banner when the ceremony was cancelled. */
219
+ errorCancelled: string;
220
+ /** Error banner when the ceremony failed. */
221
+ errorFailed: string;
222
+ }
223
+ /** Strings rendered by `<PasskeySettingsCard>` — the account passkey card. */
224
+ interface PasskeySettingsLabels {
225
+ title: string;
226
+ description: string;
227
+ /** Italic hint about the re-auth step. */
228
+ reauthHint: string;
229
+ /** The "add a passkey" button. */
230
+ addButton: string;
231
+ addHint: string;
232
+ /** Success line shown after returning from a registration. */
233
+ registeredSuccess: string;
234
+ }
118
235
  /** Strings rendered by `<ResetPasswordForm>`. */
119
236
  interface ResetPasswordFormLabels {
120
237
  title: string;
@@ -141,6 +258,11 @@ declare const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordFormLabels;
141
258
  declare const DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS: ForgotPasswordFieldsLabels;
142
259
  declare const DEFAULT_OTP_LABELS: OtpFormLabels;
143
260
  declare const DEFAULT_PIN_LABELS: PinFormLabels;
261
+ declare const DEFAULT_DEVICE_PIN_UNLOCK_LABELS: DevicePinUnlockLabels;
262
+ declare const DEFAULT_DEVICE_PIN_ENROLL_LABELS: DevicePinEnrollLabels;
263
+ declare const DEFAULT_DEVICE_PIN_SETTINGS_LABELS: DevicePinSettingsLabels;
264
+ declare const DEFAULT_PASSKEY_LOGIN_LABELS: PasskeyLoginLabels;
265
+ declare const DEFAULT_PASSKEY_SETTINGS_LABELS: PasskeySettingsLabels;
144
266
  declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordFormLabels;
145
267
 
146
268
  /**
@@ -447,6 +569,421 @@ interface PinFormProps {
447
569
  /** Themeable single-step event-PIN login form built on `usePinLogin`. */
448
570
  declare function PinForm({ client, eventExternalId, theme: themeProp, labels: labelsProp, onSuccess, testIdPrefix, }: Readonly<PinFormProps>): ReactElement;
449
571
 
572
+ /**
573
+ * A masked numeric PIN input for the device-PIN unlock + enrol surfaces.
574
+ *
575
+ * A single hidden secure `TextInput` captures the digits (numeric keyboard,
576
+ * length-capped, digits-only); a row of dot cells visualises how many have been
577
+ * entered. Tapping anywhere on the row focuses the field. Purely presentational
578
+ * — all submit / error logic lives in the unlock / enrol hooks. Styled from the
579
+ * `AuthTheme` token bag (no hardcoded brand colours).
580
+ */
581
+
582
+ interface DevicePinInputProps {
583
+ /** The current PIN value (digits only). */
584
+ value: string;
585
+ /** Number of dot cells to render — the enrolled / chosen PIN length. */
586
+ length: number;
587
+ /** Disable input while a request is in flight. */
588
+ disabled: boolean;
589
+ /** Base testID for the underlying field (a11y + E2E targeting). */
590
+ testID: string;
591
+ /** Optional prefix applied to the testID. */
592
+ testIdPrefix?: string;
593
+ /** Accessibility label for the field. */
594
+ accessibilityLabel: string;
595
+ /** Accessibility hint for the field. */
596
+ accessibilityHint: string;
597
+ /** Accessibility label for a filled dot. */
598
+ filledHint: string;
599
+ /** Accessibility label for an empty dot. */
600
+ emptyHint: string;
601
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
602
+ theme?: AuthTheme;
603
+ /** Emit the next digits-only value (already length-capped). */
604
+ onChange: (next: string) => void;
605
+ }
606
+ /** Themeable masked PIN pad — dots reflect entered digits; a hidden field captures them. */
607
+ declare function DevicePinInput({ value, length, disabled, testID, testIdPrefix, accessibilityLabel, accessibilityHint, filledHint, emptyHint, theme: themeProp, onChange, }: Readonly<DevicePinInputProps>): ReactElement;
608
+
609
+ /**
610
+ * The PIN-length (4/6/8) picker row for `<DevicePinEnrollForm>`.
611
+ *
612
+ * Presentational: renders one pill per allowed length and reports the chosen
613
+ * one. Styled from the `AuthTheme` token bag. Extracted from the enrol form to
614
+ * keep that component focused.
615
+ */
616
+
617
+ interface DevicePinLengthPickerProps {
618
+ /** The currently-selected PIN length. */
619
+ selected: number;
620
+ /** Disable while a request is in flight. */
621
+ disabled: boolean;
622
+ /** Accessibility hint template for a pill; `{count}` is substituted. */
623
+ optionHint: string;
624
+ /** Optional prefix applied to each pill's testID. */
625
+ testIdPrefix?: string;
626
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
627
+ theme?: AuthTheme;
628
+ /** Report the newly-chosen length. */
629
+ onSelect: (digits: number) => void;
630
+ }
631
+ /** Themeable 4/6/8 length chooser. */
632
+ declare function DevicePinLengthPicker({ selected, disabled, optionHint, testIdPrefix, theme: themeProp, onSelect, }: Readonly<DevicePinLengthPickerProps>): ReactElement;
633
+
634
+ /**
635
+ * The localizable error states the device-PIN UNLOCK surface can show.
636
+ *
637
+ * Kept deliberately coarse so the UI never reveals whether a failed unlock was a
638
+ * wrong PIN or an unknown / revoked device (both map to `Invalid`). `LockedOut`
639
+ * and `RateLimited` carry no count — the human-readable "try again later" copy
640
+ * is built from the `retryAfterSeconds` hint at the call site, not encoded here.
641
+ */
642
+ declare const enum DevicePinErrorKey {
643
+ /** The PIN entered was too short for the enrolled length. */
644
+ Incomplete = "incomplete",
645
+ /** Wrong PIN, or unknown / revoked device. Generic on purpose. */
646
+ Invalid = "invalid",
647
+ /** Locked out after too many wrong attempts. */
648
+ LockedOut = "locked_out",
649
+ /** Rate-limited by the BFF (too many requests). */
650
+ RateLimited = "rate_limited",
651
+ /** Anything else (network / unexpected). */
652
+ Generic = "generic"
653
+ }
654
+
655
+ /**
656
+ * Structural prop/result contracts for the device-PIN surfaces.
657
+ *
658
+ * `@dloizides/auth-web` deliberately does NOT take a hard dependency on the
659
+ * device-PIN methods that `@dloizides/auth-client` 3.3.0 adds. Instead the
660
+ * components depend on the **structural** shapes below — exactly what each one
661
+ * needs and no more. A real client (auth-client 3.3.0) satisfies
662
+ * {@link DevicePinCapableClient} by duck-typing; an app can equally pass a thin
663
+ * hand-rolled object. This keeps the two packages decoupled and lets a consumer
664
+ * upgrade either independently.
665
+ *
666
+ * The discriminated result shapes mirror the BFF's meaningfully-distinct
667
+ * outcomes (a wrong PIN, a lockout with a retry hint, a rate-limit, a generic
668
+ * error) so the unlock / enrol UIs can route on each one rather than collapsing
669
+ * them into a single opaque failure.
670
+ */
671
+ /**
672
+ * The per-device half of `GET /bff/config` — what this device remembers about
673
+ * the last user, all fields safe-defaulted by the parser when absent.
674
+ */
675
+ interface BffDeviceState {
676
+ /** Non-secret username this device remembers, or `null` when none. */
677
+ rememberedUsername: string | null;
678
+ /** `true` when this device has an enrolled device PIN. */
679
+ hasPin: boolean;
680
+ /** The enrolled PIN length (4/6/8), or `null` when unknown / no PIN. */
681
+ pinDigits: number | null;
682
+ /** The device-local preferred-method hint (e.g. `"pin"`), or `null`. */
683
+ preferredMethod: string | null;
684
+ }
685
+ /** The shape `GET /bff/config` resolves to (as the client parses it). */
686
+ interface BffLoginConfig {
687
+ /** The enabled login methods, lower-cased (e.g. `["password", "otp"]`). */
688
+ methods: string[];
689
+ /** `true` when self-serve registration is enabled for this BFF. */
690
+ registrationEnabled: boolean;
691
+ /** The per-device remembered state. */
692
+ deviceState: BffDeviceState;
693
+ }
694
+ /**
695
+ * Discriminated result of a device-PIN UNLOCK attempt. The client never rejects;
696
+ * it resolves one of these so the UI can route on the distinct outcomes (a
697
+ * lockout / rate-limit carries an optional `retryAfterSeconds` hint).
698
+ */
699
+ type DevicePinUnlockResult = {
700
+ status: 'success';
701
+ user: {
702
+ [claim: string]: unknown;
703
+ };
704
+ } | {
705
+ status: 'invalid';
706
+ } | {
707
+ status: 'locked';
708
+ retryAfterSeconds: number | null;
709
+ } | {
710
+ status: 'rateLimited';
711
+ retryAfterSeconds: number | null;
712
+ } | {
713
+ status: 'error';
714
+ };
715
+ /**
716
+ * Discriminated result of a device-PIN ENROL attempt. The client never rejects;
717
+ * it resolves one of these so the form can show the precise failure cause.
718
+ */
719
+ type DevicePinEnrollResult = {
720
+ status: 'success';
721
+ } | {
722
+ status: 'unauthorized';
723
+ } | {
724
+ status: 'forbidden';
725
+ } | {
726
+ status: 'invalidPin';
727
+ } | {
728
+ status: 'error';
729
+ };
730
+ /**
731
+ * The client capability the device-PIN components depend on. `auth-client`
732
+ * 3.3.0 satisfies this structurally; a consuming app may also pass a thin
733
+ * hand-rolled object exposing exactly these four methods.
734
+ */
735
+ interface DevicePinCapableClient {
736
+ /** Read `GET /bff/config` (methods + per-device state). */
737
+ getLoginConfig(): Promise<BffLoginConfig>;
738
+ /** Bind a device PIN of `digits` length to the current session. */
739
+ enrollDevicePin(request: {
740
+ pin: string;
741
+ digits: number;
742
+ }): Promise<DevicePinEnrollResult>;
743
+ /** Re-establish a session from a remembered device using `pin`. */
744
+ unlockWithDevicePin(request: {
745
+ pin: string;
746
+ }): Promise<DevicePinUnlockResult>;
747
+ /** Drop the device PIN for the current session. Resolves `true` on success. */
748
+ disableDevicePin(): Promise<boolean>;
749
+ }
750
+ /** The minimal slice {@link useBffLoginConfig} needs from a client. */
751
+ interface LoginConfigCapableClient {
752
+ getLoginConfig(): Promise<BffLoginConfig>;
753
+ }
754
+ /** The minimal slice {@link useDevicePinUnlock} needs from a client. */
755
+ interface DevicePinUnlockCapableClient {
756
+ unlockWithDevicePin(request: {
757
+ pin: string;
758
+ }): Promise<DevicePinUnlockResult>;
759
+ }
760
+ /** The minimal slice {@link useDevicePinEnroll} needs from a client. */
761
+ interface DevicePinEnrollCapableClient {
762
+ enrollDevicePin(request: {
763
+ pin: string;
764
+ digits: number;
765
+ }): Promise<DevicePinEnrollResult>;
766
+ }
767
+ /** The minimal slice {@link DevicePinSettingsCard} needs to disable a PIN. */
768
+ interface DevicePinDisableCapableClient {
769
+ disableDevicePin(): Promise<boolean>;
770
+ }
771
+
772
+ /** The signed-in user shape the success path hands back (claim bag). */
773
+ type DevicePinUnlockedUser = {
774
+ [claim: string]: unknown;
775
+ };
776
+ interface UseDevicePinUnlockArgs {
777
+ /** The client exposing `unlockWithDevicePin`. */
778
+ client: DevicePinUnlockCapableClient;
779
+ /** Enrolled PIN length (4/6/8) — a shorter entry is rejected before any call. */
780
+ digits: number;
781
+ /** Called with the fresh user after a successful unlock. */
782
+ onSignedIn: (user: DevicePinUnlockedUser) => void;
783
+ }
784
+ interface UseDevicePinUnlockResult {
785
+ /** The PIN currently entered. */
786
+ pin: string;
787
+ /** `true` while an unlock call is in flight. */
788
+ submitting: boolean;
789
+ /** The current error key, or `null` when there is none. */
790
+ errorKey: DevicePinErrorKey | null;
791
+ /** Seconds until retry, set with a `LockedOut`/`RateLimited` error; else `null`. */
792
+ retryAfterSeconds: number | null;
793
+ /** Replace the entered PIN. Clears a stale error. */
794
+ setPin: (next: string) => void;
795
+ /** Run the unlock attempt for the current PIN. */
796
+ submit: () => void;
797
+ }
798
+ declare function useDevicePinUnlock({ client, digits, onSignedIn, }: UseDevicePinUnlockArgs): UseDevicePinUnlockResult;
799
+
800
+ /**
801
+ * The device-PIN UNLOCK gate — shown on the login route for a returning,
802
+ * logged-OUT user whose device is remembered (`hasPin && rememberedUsername`).
803
+ *
804
+ * Renders a masked PIN pad of the enrolled length; on submit it calls
805
+ * `client.unlockWithDevicePin` (via {@link useDevicePinUnlock}) and, on success,
806
+ * routes through the SAME `onSignedIn` path as the password tab. A
807
+ * "Sign in with password instead" escape calls `onUsePassword` (the caller swaps
808
+ * this screen out). All five result statuses are handled, including the lockout /
809
+ * rate-limit retry countdown surfaced from `retryAfterSeconds`.
810
+ *
811
+ * react-query-FREE: all logic lives in `useDevicePinUnlock`, which uses plain
812
+ * `useState` + `.then()` over the client — never `useMutation`. Presentational
813
+ * only here. Distinct from the event-staff PIN (`PinForm`); this is `devicePin`.
814
+ */
815
+
816
+ interface DevicePinUnlockScreenProps {
817
+ /** The client exposing `unlockWithDevicePin`. */
818
+ client: DevicePinUnlockCapableClient;
819
+ /** Enrolled PIN length (4/6/8). */
820
+ digits: number;
821
+ /** Remembered username, shown in the greeting (may be empty). */
822
+ rememberedUsername: string;
823
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
824
+ theme?: AuthTheme;
825
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
826
+ labels?: Partial<DevicePinUnlockLabels>;
827
+ /** Prefix applied to every `testID`. */
828
+ testIdPrefix?: string;
829
+ /** Called with the fresh user after a successful unlock. */
830
+ onSignedIn: (user: DevicePinUnlockedUser) => void;
831
+ /** Escape hatch → render the normal password/OTP/event-PIN tabs. */
832
+ onUsePassword: () => void;
833
+ }
834
+ /** Themeable device-PIN unlock gate built on `useDevicePinUnlock`. */
835
+ declare function DevicePinUnlockScreen({ client, digits, rememberedUsername, theme: themeProp, labels: labelsProp, testIdPrefix, onSignedIn, onUsePassword, }: Readonly<DevicePinUnlockScreenProps>): ReactElement;
836
+
837
+ /**
838
+ * The shared device-PIN ENROL form — reused by the post-login offer
839
+ * ({@link DevicePinOffer}) and the settings toggle ({@link DevicePinSettingsCard}).
840
+ *
841
+ * Lets the user pick a PIN length (4/6/8), enter + confirm a PIN, and submit via
842
+ * `client.enrollDevicePin` (through {@link useDevicePinEnroll}). On success it
843
+ * calls `onEnrolled`; `onCancel` dismisses without saving. Presentational only —
844
+ * all submit / mismatch / status logic lives in the hook (react-query-free).
845
+ * Styled from the `AuthTheme` token bag.
846
+ */
847
+
848
+ interface DevicePinEnrollFormProps {
849
+ /** The client exposing `enrollDevicePin`. */
850
+ client: DevicePinEnrollCapableClient;
851
+ /** Initial PIN length (defaults to {@link DEVICE_PIN_DEFAULT_DIGITS}). */
852
+ initialDigits?: number;
853
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
854
+ theme?: AuthTheme;
855
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
856
+ labels?: Partial<DevicePinEnrollLabels>;
857
+ /** Prefix applied to every `testID`. */
858
+ testIdPrefix?: string;
859
+ /** Called after a successful enrol. */
860
+ onEnrolled: () => void;
861
+ /** Called when the user cancels without saving. */
862
+ onCancel: () => void;
863
+ }
864
+ /** Themeable device-PIN enrol form built on `useDevicePinEnroll`. */
865
+ declare function DevicePinEnrollForm({ client, initialDigits, theme: themeProp, labels: labelsProp, testIdPrefix, onEnrolled, onCancel, }: Readonly<DevicePinEnrollFormProps>): ReactElement;
866
+
867
+ /**
868
+ * The skippable post-login "Set a PIN for faster sign-in?" offer.
869
+ *
870
+ * Mount on the post-login landing for an authenticated user who does NOT already
871
+ * have a device PIN. It is dismissible and non-blocking: an initial prompt with
872
+ * "Set up a PIN" / "Not now"; tapping accept reveals the shared
873
+ * {@link DevicePinEnrollForm}; on success or skip it calls `onDismiss` so the
874
+ * host can hide it (and remember the dismissal). Self-contained, react-query-free.
875
+ */
876
+
877
+ interface DevicePinOfferProps {
878
+ /** The client exposing `enrollDevicePin`. */
879
+ client: DevicePinEnrollCapableClient;
880
+ /** Initial PIN length offered in the enrol form. */
881
+ initialDigits?: number;
882
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
883
+ theme?: AuthTheme;
884
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
885
+ labels?: Partial<DevicePinEnrollLabels>;
886
+ /** Prefix applied to every `testID`. */
887
+ testIdPrefix?: string;
888
+ /** Called when the offer is finished — accepted+enrolled, or skipped. */
889
+ onDismiss: () => void;
890
+ }
891
+ /** Themeable, skippable post-login device-PIN setup offer. */
892
+ declare function DevicePinOffer({ client, initialDigits, theme: themeProp, labels: labelsProp, testIdPrefix, onDismiss, }: Readonly<DevicePinOfferProps>): ReactElement;
893
+
894
+ /**
895
+ * The authenticated "Quick PIN sign-in" settings control.
896
+ *
897
+ * Mount on an authenticated settings/account surface. Seeded with `initialHasPin`
898
+ * (from `GET /bff/config`); it tracks its own enabled/disabled state thereafter
899
+ * and offers either:
900
+ * - Enable → reveals the shared {@link DevicePinEnrollForm} → `client.enrollDevicePin`;
901
+ * - Disable → `client.disableDevicePin` (revokes the offline token + clears the
902
+ * device record server-side).
903
+ *
904
+ * `onChanged(hasPin)` fires whenever the enabled state flips, so the host can
905
+ * persist / re-fetch. react-query-free: the disable logic lives in
906
+ * {@link useDevicePinDisable}; the enrol logic in the shared form's hook.
907
+ */
908
+
909
+ interface DevicePinSettingsCardProps {
910
+ /** The client exposing both `enrollDevicePin` and `disableDevicePin`. */
911
+ client: DevicePinEnrollCapableClient & DevicePinDisableCapableClient;
912
+ /** Whether this device already has an enrolled PIN (seeded from config). */
913
+ initialHasPin: boolean;
914
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
915
+ theme?: AuthTheme;
916
+ /** Settings-card copy. Partial — unspecified keys fall back to English. */
917
+ labels?: Partial<DevicePinSettingsLabels>;
918
+ /** Enrol-form copy passed through to the embedded form. */
919
+ enrollLabels?: Partial<DevicePinEnrollLabels>;
920
+ /** Prefix applied to every `testID`. */
921
+ testIdPrefix?: string;
922
+ /** Called whenever the enabled state flips (enabled / disabled). */
923
+ onChanged?: (hasPin: boolean) => void;
924
+ }
925
+ /** Themeable authenticated device-PIN enable/disable card. */
926
+ declare function DevicePinSettingsCard({ client, initialHasPin, theme: themeProp, labels: labelsProp, enrollLabels, testIdPrefix, onChanged, }: Readonly<DevicePinSettingsCardProps>): ReactElement;
927
+
928
+ /**
929
+ * The "Sign in with a passkey" button rendered on the unified login surface when
930
+ * the BFF advertises the `passkey` method.
931
+ *
932
+ * A passkey login is a full-page BROWSER NAVIGATION, not a fetch: tapping the
933
+ * button hands off to `/bff/passkey/login`, which drives the WebAuthn ceremony
934
+ * through Keycloak and returns the browser to `returnUrl` (default `'/'`, which
935
+ * routes an authenticated user to their dashboard) with a session cookie set. On
936
+ * a cancelled / failed ceremony the BFF bounces back to `?passkeyError=…`; we
937
+ * surface that as an inline error banner above the button.
938
+ *
939
+ * react-query-free: it is pure `window.location` navigation (see
940
+ * `passkeyNavigation`), so it is safe to mount inside the login route group which
941
+ * has no QueryClient provider.
942
+ */
943
+
944
+ interface PasskeyLoginButtonProps {
945
+ /** Relative app path the BFF returns to on success. Defaults to `'/'`. */
946
+ returnUrl?: string;
947
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
948
+ theme?: AuthTheme;
949
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
950
+ labels?: Partial<PasskeyLoginLabels>;
951
+ /** Prefix applied to every `testID`. */
952
+ testIdPrefix?: string;
953
+ }
954
+ /** Themeable passkey login CTA + error banner. */
955
+ declare function PasskeyLoginButton({ returnUrl, theme: themeProp, labels: labelsProp, testIdPrefix, }: Readonly<PasskeyLoginButtonProps>): ReactElement;
956
+
957
+ /**
958
+ * The authenticated "Passkeys" settings card.
959
+ *
960
+ * Mount on an authenticated settings / account surface (alongside the device-PIN
961
+ * card). Adding a passkey is a full-page BROWSER NAVIGATION to
962
+ * `/bff/passkey/register`: Keycloak re-authenticates the user (password) then
963
+ * runs the WebAuthn credential-creation ceremony, returning the browser to the
964
+ * SAME settings page with `?passkey=registered` appended. On mount we read that
965
+ * param and, when present, show a success line.
966
+ *
967
+ * react-query-free: the add action is pure `window.location` navigation (see
968
+ * `passkeyNavigation`); there is no fetch here. Mirrors `DevicePinSettingsCard`.
969
+ */
970
+
971
+ interface PasskeySettingsCardProps {
972
+ /**
973
+ * Relative app path the BFF returns to after registration. Defaults to the
974
+ * current `window.location.pathname` so the user lands back on this page.
975
+ */
976
+ returnUrl?: string;
977
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
978
+ theme?: AuthTheme;
979
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
980
+ labels?: Partial<PasskeySettingsLabels>;
981
+ /** Prefix applied to every `testID`. */
982
+ testIdPrefix?: string;
983
+ }
984
+ /** Themeable authenticated passkey add card. */
985
+ declare function PasskeySettingsCard({ returnUrl, theme: themeProp, labels: labelsProp, testIdPrefix, }: Readonly<PasskeySettingsCardProps>): ReactElement;
986
+
450
987
  /**
451
988
  * React context for the `AuthTheme`.
452
989
  *
@@ -521,6 +1058,32 @@ declare const AuthTestIds: {
521
1058
  readonly pinInput: "auth-pin-input";
522
1059
  readonly pinSubmitButton: "auth-pin-submit";
523
1060
  readonly pinError: "auth-pin-error";
1061
+ readonly devicePinUnlock: "auth-device-pin-unlock";
1062
+ readonly devicePinUnlockInput: "auth-device-pin-unlock-input";
1063
+ readonly devicePinUnlockSubmit: "auth-device-pin-unlock-submit";
1064
+ readonly devicePinUnlockUsePassword: "auth-device-pin-unlock-use-password";
1065
+ readonly devicePinUnlockError: "auth-device-pin-unlock-error";
1066
+ readonly devicePinEnrollForm: "auth-device-pin-enroll-form";
1067
+ readonly devicePinEnrollLength: "auth-device-pin-enroll-length";
1068
+ readonly devicePinEnrollPin: "auth-device-pin-enroll-pin";
1069
+ readonly devicePinEnrollConfirm: "auth-device-pin-enroll-confirm";
1070
+ readonly devicePinEnrollSubmit: "auth-device-pin-enroll-submit";
1071
+ readonly devicePinEnrollCancel: "auth-device-pin-enroll-cancel";
1072
+ readonly devicePinEnrollError: "auth-device-pin-enroll-error";
1073
+ readonly devicePinOffer: "auth-device-pin-offer";
1074
+ readonly devicePinOfferAccept: "auth-device-pin-offer-accept";
1075
+ readonly devicePinOfferSkip: "auth-device-pin-offer-skip";
1076
+ readonly devicePinSettings: "auth-device-pin-settings";
1077
+ readonly devicePinSettingsStatus: "auth-device-pin-settings-status";
1078
+ readonly devicePinSettingsEnable: "auth-device-pin-settings-enable";
1079
+ readonly devicePinSettingsDisable: "auth-device-pin-settings-disable";
1080
+ readonly devicePinSettingsError: "auth-device-pin-settings-error";
1081
+ readonly passkeyLogin: "auth-passkey-login";
1082
+ readonly passkeyLoginButton: "auth-passkey-login-button";
1083
+ readonly passkeyLoginError: "auth-passkey-login-error";
1084
+ readonly passkeySettings: "auth-passkey-settings";
1085
+ readonly passkeySettingsAdd: "auth-passkey-settings-add";
1086
+ readonly passkeySettingsSuccess: "auth-passkey-settings-success";
524
1087
  };
525
1088
  /** Apply an optional prefix to a base test ID. */
526
1089
  declare function withTestIdPrefix(baseId: string, prefix?: string): string;
@@ -775,6 +1338,148 @@ interface UsePinLoginResult {
775
1338
  */
776
1339
  declare function usePinLogin(options: UsePinLoginOptions): UsePinLoginResult;
777
1340
 
1341
+ /** What {@link useBffLoginConfig} returns. */
1342
+ interface UseBffLoginConfigResult {
1343
+ /** The parsed login config; the empty fallback until the fetch resolves. */
1344
+ config: BffLoginConfig;
1345
+ /** `true` until the initial `getLoginConfig` call settles. */
1346
+ loading: boolean;
1347
+ }
1348
+ /**
1349
+ * Fetch `getLoginConfig()` once on mount and expose `{ config, loading }`. The
1350
+ * empty fallback config is used until the call resolves and on any failure. The
1351
+ * effect guards against a `setState` after the consumer unmounts mid-request.
1352
+ */
1353
+ declare function useBffLoginConfig(client: LoginConfigCapableClient): UseBffLoginConfigResult;
1354
+
1355
+ /**
1356
+ * The localizable failure states the device-PIN ENROL form can show — shared by
1357
+ * the post-login offer and the settings toggle.
1358
+ */
1359
+ declare const enum DevicePinEnrollErrorKey {
1360
+ /** PIN length wrong or the confirmation didn't match (no network call). */
1361
+ Mismatch = "mismatch",
1362
+ /** The current session is not authenticated (401). */
1363
+ Unauthorized = "unauthorized",
1364
+ /** The session already has a PIN, or is otherwise forbidden (403). */
1365
+ Forbidden = "forbidden",
1366
+ /** The PIN failed server-side validation (400). */
1367
+ InvalidPin = "invalid_pin",
1368
+ /** Any other failure (grant / network / unexpected). */
1369
+ Failed = "failed"
1370
+ }
1371
+
1372
+ interface UseDevicePinEnrollArgs {
1373
+ /** The client exposing `enrollDevicePin`. */
1374
+ client: DevicePinEnrollCapableClient;
1375
+ /** Chosen PIN length (4/6/8). */
1376
+ digits: number;
1377
+ /** Called after a successful enrol so the surface can dismiss / flip state. */
1378
+ onEnrolled: () => void;
1379
+ }
1380
+ interface UseDevicePinEnrollResult {
1381
+ pin: string;
1382
+ confirmPin: string;
1383
+ submitting: boolean;
1384
+ errorKey: DevicePinEnrollErrorKey | null;
1385
+ setPin: (next: string) => void;
1386
+ setConfirmPin: (next: string) => void;
1387
+ submit: () => void;
1388
+ }
1389
+ declare function useDevicePinEnroll({ client, digits, onEnrolled, }: UseDevicePinEnrollArgs): UseDevicePinEnrollResult;
1390
+
1391
+ interface UseDevicePinDisableArgs {
1392
+ /** The client exposing `disableDevicePin`. */
1393
+ client: DevicePinDisableCapableClient;
1394
+ /** Whether this device already has an enrolled PIN (seeded from config). */
1395
+ initialHasPin: boolean;
1396
+ /** Called whenever the enabled state changes (enabled / disabled). */
1397
+ onChanged?: (hasPin: boolean) => void;
1398
+ }
1399
+ interface UseDevicePinDisableResult {
1400
+ /** `true` when the device currently has an enrolled PIN. */
1401
+ hasPin: boolean;
1402
+ /** `true` while a disable call is in flight. */
1403
+ disabling: boolean;
1404
+ /** `true` when the last disable attempt failed. */
1405
+ failed: boolean;
1406
+ /** Run the disable call for the current session. */
1407
+ disable: () => void;
1408
+ /** Flip the card to enabled (called by the host after a successful enrol). */
1409
+ markEnabled: () => void;
1410
+ }
1411
+ declare function useDevicePinDisable({ client, initialHasPin, onChanged, }: UseDevicePinDisableArgs): UseDevicePinDisableResult;
1412
+
1413
+ /**
1414
+ * Shared device-PIN constants for the unlock + enrol surfaces.
1415
+ *
1416
+ * The allowed PIN lengths (4 / 6 / 8) mirror the BFF's accepted `digits` values
1417
+ * (`POST /bff/pin/enroll`). Centralised here so neither the enrol form nor the
1418
+ * unlock screen carries magic numbers, and so the default length is defined once.
1419
+ */
1420
+ /** The PIN lengths the BFF accepts on enrol (`digits` ∈ {4, 6, 8}). */
1421
+ declare const DEVICE_PIN_ALLOWED_DIGITS: readonly number[];
1422
+ /** Default PIN length offered when the BFF reports no stored `pinDigits`. */
1423
+ declare const DEVICE_PIN_DEFAULT_DIGITS = 4;
1424
+ /** Guard: is `value` one of the BFF-accepted PIN lengths? */
1425
+ declare function isAllowedPinDigits(value: number): boolean;
1426
+
1427
+ /**
1428
+ * Navigation helpers for the passkey (WebAuthn) BFF endpoints.
1429
+ *
1430
+ * Unlike the device-PIN client (which is plain `fetch`), the passkey login and
1431
+ * registration ceremonies are NOT fetches — they are full-page BROWSER
1432
+ * NAVIGATIONS. The BFF (`Bff.AspNetCore` 1.3.x) drives the WebAuthn ceremony
1433
+ * through Keycloak's hosted pages, then redirects the browser back:
1434
+ * - login: `/bff/passkey/login?returnUrl=<rel>` → on success the browser
1435
+ * lands on `returnUrl` with the session cookie already set;
1436
+ * - registration: `/bff/passkey/register?returnUrl=<rel>` → Keycloak re-auths
1437
+ * the user (password) then runs the credential-creation ceremony, returning
1438
+ * the browser to `returnUrl?passkey=registered`;
1439
+ * - failure / cancel: the BFF redirects to `<login>?passkeyError=cancelled` or
1440
+ * `<login>?passkeyError=failed`.
1441
+ *
1442
+ * react-query-FREE by construction: there is NO fetch here at all — every
1443
+ * function is a pure `window.location` read or `window.location.assign`. The
1444
+ * login route group has no QueryClient provider, so the passkey login button
1445
+ * (which lives there) MUST avoid any react-query primitive; navigation satisfies
1446
+ * that trivially.
1447
+ *
1448
+ * All reads are SSR-safe: when `window` is undefined (server render / native)
1449
+ * they return a benign default rather than throwing.
1450
+ */
1451
+ /** The recognised passkey error codes the BFF sends back via `?passkeyError=`. */
1452
+ type PasskeyErrorCode = 'cancelled' | 'failed';
1453
+ /**
1454
+ * Start the passkey LOGIN ceremony by navigating the browser to the BFF's
1455
+ * `/bff/passkey/login` endpoint. The BFF redirects through Keycloak's WebAuthn
1456
+ * ceremony and, on success, returns the browser to `returnUrl` with a session
1457
+ * cookie set. No-op off-web (no `window`). `returnUrl` should be a relative
1458
+ * app path (e.g. `'/'`).
1459
+ */
1460
+ declare function startPasskeyLogin(returnUrl: string): void;
1461
+ /**
1462
+ * Start the passkey REGISTRATION ceremony by navigating the browser to the BFF's
1463
+ * `/bff/passkey/register` endpoint. Requires an authenticated session; Keycloak
1464
+ * re-authenticates the user (password) then runs the credential-creation
1465
+ * ceremony, returning the browser to `returnUrl?passkey=registered`. No-op
1466
+ * off-web. `returnUrl` should be a relative app path so the user lands back on
1467
+ * the same settings surface.
1468
+ */
1469
+ declare function startPasskeyRegistration(returnUrl: string): void;
1470
+ /**
1471
+ * Read the `?passkeyError=` query param the BFF appends on a failed / cancelled
1472
+ * ceremony. Returns `'cancelled'` or `'failed'` for the two known values, or
1473
+ * `null` for an absent / unknown value. SSR-safe (returns `null` off-web).
1474
+ */
1475
+ declare function readPasskeyError(): PasskeyErrorCode | null;
1476
+ /**
1477
+ * `true` when the browser has just returned from a successful passkey
1478
+ * registration, i.e. the `?passkey=registered` query param is present. SSR-safe
1479
+ * (returns `false` off-web).
1480
+ */
1481
+ declare function readPasskeyRegistered(): boolean;
1482
+
778
1483
  /**
779
1484
  * `createBffAuthClient` — the one-line wiring of a same-origin `BffAuthClient`.
780
1485
  *
@@ -910,4 +1615,4 @@ declare function validatePasswordPolicy(password: string): PasswordPolicyError[]
910
1615
  /** `true` when the password satisfies every policy rule. */
911
1616
  declare function isPasswordValid(password: string): boolean;
912
1617
 
913
- export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type CreateBffAuthClientOptions, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordFields, type ForgotPasswordFieldsLabels, type ForgotPasswordFieldsProps, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffResetPasswordOptions, type UseForgotPasswordSubmitArgs, type UseForgotPasswordSubmitResult, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, isValidForgotPasswordEmail, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useForgotPasswordSubmit, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
1618
+ export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type BffDeviceState, type BffLoginConfig, type CreateBffAuthClientOptions, DEFAULT_DEVICE_PIN_ENROLL_LABELS, DEFAULT_DEVICE_PIN_SETTINGS_LABELS, DEFAULT_DEVICE_PIN_UNLOCK_LABELS, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PASSKEY_LOGIN_LABELS, DEFAULT_PASSKEY_SETTINGS_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEVICE_PIN_ALLOWED_DIGITS, DEVICE_PIN_DEFAULT_DIGITS, type DevicePinCapableClient, type DevicePinDisableCapableClient, type DevicePinEnrollCapableClient, DevicePinEnrollErrorKey, DevicePinEnrollForm, type DevicePinEnrollFormProps, type DevicePinEnrollLabels, type DevicePinEnrollResult, DevicePinErrorKey, DevicePinInput, type DevicePinInputProps, DevicePinLengthPicker, type DevicePinLengthPickerProps, DevicePinOffer, type DevicePinOfferProps, DevicePinSettingsCard, type DevicePinSettingsCardProps, type DevicePinSettingsLabels, type DevicePinUnlockCapableClient, type DevicePinUnlockLabels, type DevicePinUnlockResult, DevicePinUnlockScreen, type DevicePinUnlockScreenProps, type DevicePinUnlockedUser, ForgotPasswordFields, type ForgotPasswordFieldsLabels, type ForgotPasswordFieldsProps, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, type LoginConfigCapableClient, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, type PasskeyErrorCode, PasskeyLoginButton, type PasskeyLoginButtonProps, type PasskeyLoginLabels, PasskeySettingsCard, type PasskeySettingsCardProps, type PasskeySettingsLabels, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffLoginConfigResult, type UseBffResetPasswordOptions, type UseDevicePinDisableArgs, type UseDevicePinDisableResult, type UseDevicePinEnrollArgs, type UseDevicePinEnrollResult, type UseDevicePinUnlockArgs, type UseDevicePinUnlockResult, type UseForgotPasswordSubmitArgs, type UseForgotPasswordSubmitResult, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readPasskeyError, readPasskeyRegistered, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };