@dloizides/auth-web 1.8.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -345,6 +345,13 @@ interface PasskeyLoginLabels {
345
345
  errorCancelled: string;
346
346
  /** Error banner when the ceremony failed. */
347
347
  errorFailed: string;
348
+ /**
349
+ * The "try another way" affordance shown after an in-page ceremony error — it
350
+ * hands off to the Keycloak-hosted passkey/password flow. This is the seam that
351
+ * rescues a user whose credential is only enrolled in Keycloak (not the in-page
352
+ * RP store): the in-page ceremony can't see it, so they fall back to KC.
353
+ */
354
+ tryAnotherWay: string;
348
355
  }
349
356
  /** Strings rendered by `<PasskeySettingsCard>` — the account passkey card. */
350
357
  interface PasskeySettingsLabels {
@@ -1301,6 +1308,7 @@ declare const AuthTestIds: {
1301
1308
  readonly passkeyLogin: "auth-passkey-login";
1302
1309
  readonly passkeyLoginButton: "auth-passkey-login-button";
1303
1310
  readonly passkeyLoginError: "auth-passkey-login-error";
1311
+ readonly passkeyLoginFallback: "auth-passkey-login-fallback";
1304
1312
  readonly passkeySettings: "auth-passkey-settings";
1305
1313
  readonly passkeySettingsAdd: "auth-passkey-settings-add";
1306
1314
  readonly passkeySettingsSuccess: "auth-passkey-settings-success";
@@ -1686,30 +1694,6 @@ declare const enum PasskeyCeremonyStatus {
1686
1694
  Error = "error"
1687
1695
  }
1688
1696
 
1689
- /**
1690
- * Navigation helpers for the passkey (WebAuthn) BFF endpoints.
1691
- *
1692
- * Unlike the device-PIN client (which is plain `fetch`), the passkey login and
1693
- * registration ceremonies are NOT fetches — they are full-page BROWSER
1694
- * NAVIGATIONS. The BFF (`Bff.AspNetCore` 1.3.x) drives the WebAuthn ceremony
1695
- * through Keycloak's hosted pages, then redirects the browser back:
1696
- * - login: `/bff/passkey/login?returnUrl=<rel>` → on success the browser
1697
- * lands on `returnUrl` with the session cookie already set;
1698
- * - registration: `/bff/passkey/register?returnUrl=<rel>` → Keycloak re-auths
1699
- * the user (password) then runs the credential-creation ceremony, returning
1700
- * the browser to `returnUrl?passkey=registered`;
1701
- * - failure / cancel: the BFF redirects to `<login>?passkeyError=cancelled` or
1702
- * `<login>?passkeyError=failed`.
1703
- *
1704
- * react-query-FREE by construction: there is NO fetch here at all — every
1705
- * function is a pure `window.location` read or `window.location.assign`. The
1706
- * login route group has no QueryClient provider, so the passkey login button
1707
- * (which lives there) MUST avoid any react-query primitive; navigation satisfies
1708
- * that trivially.
1709
- *
1710
- * All reads are SSR-safe: when `window` is undefined (server render / native)
1711
- * they return a benign default rather than throwing.
1712
- */
1713
1697
  /** The recognised passkey error codes the BFF sends back via `?passkeyError=`. */
1714
1698
  type PasskeyErrorCode = 'cancelled' | 'failed';
1715
1699
  /**
@@ -1768,6 +1752,13 @@ interface UsePasskeyCeremonyResult {
1768
1752
  isSupported: boolean;
1769
1753
  /** Start the ceremony (or redirect-fallback when the RP is off / unsupported). */
1770
1754
  start: () => void;
1755
+ /**
1756
+ * Hand off to the legacy Keycloak redirect explicitly. Wired to the "try
1757
+ * another way" affordance a surface shows after an in-page error — the escape
1758
+ * hatch for a credential enrolled only in Keycloak (invisible to the in-page
1759
+ * RP store), which the in-page ceremony cannot use.
1760
+ */
1761
+ fallbackToRedirect: () => void;
1771
1762
  /** Clear a terminal state so the affordance can be retried. */
1772
1763
  reset: () => void;
1773
1764
  }
package/dist/index.d.ts CHANGED
@@ -345,6 +345,13 @@ interface PasskeyLoginLabels {
345
345
  errorCancelled: string;
346
346
  /** Error banner when the ceremony failed. */
347
347
  errorFailed: string;
348
+ /**
349
+ * The "try another way" affordance shown after an in-page ceremony error — it
350
+ * hands off to the Keycloak-hosted passkey/password flow. This is the seam that
351
+ * rescues a user whose credential is only enrolled in Keycloak (not the in-page
352
+ * RP store): the in-page ceremony can't see it, so they fall back to KC.
353
+ */
354
+ tryAnotherWay: string;
348
355
  }
349
356
  /** Strings rendered by `<PasskeySettingsCard>` — the account passkey card. */
350
357
  interface PasskeySettingsLabels {
@@ -1301,6 +1308,7 @@ declare const AuthTestIds: {
1301
1308
  readonly passkeyLogin: "auth-passkey-login";
1302
1309
  readonly passkeyLoginButton: "auth-passkey-login-button";
1303
1310
  readonly passkeyLoginError: "auth-passkey-login-error";
1311
+ readonly passkeyLoginFallback: "auth-passkey-login-fallback";
1304
1312
  readonly passkeySettings: "auth-passkey-settings";
1305
1313
  readonly passkeySettingsAdd: "auth-passkey-settings-add";
1306
1314
  readonly passkeySettingsSuccess: "auth-passkey-settings-success";
@@ -1686,30 +1694,6 @@ declare const enum PasskeyCeremonyStatus {
1686
1694
  Error = "error"
1687
1695
  }
1688
1696
 
1689
- /**
1690
- * Navigation helpers for the passkey (WebAuthn) BFF endpoints.
1691
- *
1692
- * Unlike the device-PIN client (which is plain `fetch`), the passkey login and
1693
- * registration ceremonies are NOT fetches — they are full-page BROWSER
1694
- * NAVIGATIONS. The BFF (`Bff.AspNetCore` 1.3.x) drives the WebAuthn ceremony
1695
- * through Keycloak's hosted pages, then redirects the browser back:
1696
- * - login: `/bff/passkey/login?returnUrl=<rel>` → on success the browser
1697
- * lands on `returnUrl` with the session cookie already set;
1698
- * - registration: `/bff/passkey/register?returnUrl=<rel>` → Keycloak re-auths
1699
- * the user (password) then runs the credential-creation ceremony, returning
1700
- * the browser to `returnUrl?passkey=registered`;
1701
- * - failure / cancel: the BFF redirects to `<login>?passkeyError=cancelled` or
1702
- * `<login>?passkeyError=failed`.
1703
- *
1704
- * react-query-FREE by construction: there is NO fetch here at all — every
1705
- * function is a pure `window.location` read or `window.location.assign`. The
1706
- * login route group has no QueryClient provider, so the passkey login button
1707
- * (which lives there) MUST avoid any react-query primitive; navigation satisfies
1708
- * that trivially.
1709
- *
1710
- * All reads are SSR-safe: when `window` is undefined (server render / native)
1711
- * they return a benign default rather than throwing.
1712
- */
1713
1697
  /** The recognised passkey error codes the BFF sends back via `?passkeyError=`. */
1714
1698
  type PasskeyErrorCode = 'cancelled' | 'failed';
1715
1699
  /**
@@ -1768,6 +1752,13 @@ interface UsePasskeyCeremonyResult {
1768
1752
  isSupported: boolean;
1769
1753
  /** Start the ceremony (or redirect-fallback when the RP is off / unsupported). */
1770
1754
  start: () => void;
1755
+ /**
1756
+ * Hand off to the legacy Keycloak redirect explicitly. Wired to the "try
1757
+ * another way" affordance a surface shows after an in-page error — the escape
1758
+ * hatch for a credential enrolled only in Keycloak (invisible to the in-page
1759
+ * RP store), which the in-page ceremony cannot use.
1760
+ */
1761
+ fallbackToRedirect: () => void;
1771
1762
  /** Clear a terminal state so the affordance can be retried. */
1772
1763
  reset: () => void;
1773
1764
  }
package/dist/index.js CHANGED
@@ -236,7 +236,8 @@ var DEFAULT_PASSKEY_LOGIN_LABELS = {
236
236
  signInButton: "Sign in with a passkey",
237
237
  signInHint: "Use your fingerprint, face, or security key to sign in",
238
238
  errorCancelled: "Passkey sign-in was cancelled. You can try again or sign in another way.",
239
- errorFailed: "Passkey sign-in didn't work. Try again, or sign in another way."
239
+ errorFailed: "Passkey sign-in didn't work. Try again, or sign in another way.",
240
+ tryAnotherWay: "Sign in another way"
240
241
  };
241
242
  var DEFAULT_PASSKEY_SETTINGS_LABELS = {
242
243
  title: "Passkeys",
@@ -338,6 +339,7 @@ var AuthTestIds = {
338
339
  passkeyLogin: "auth-passkey-login",
339
340
  passkeyLoginButton: "auth-passkey-login-button",
340
341
  passkeyLoginError: "auth-passkey-login-error",
342
+ passkeyLoginFallback: "auth-passkey-login-fallback",
341
343
  // Passkey settings card
342
344
  passkeySettings: "auth-passkey-settings",
343
345
  passkeySettingsAdd: "auth-passkey-settings-add",
@@ -2518,6 +2520,18 @@ var PasskeyCeremonyMode = /* @__PURE__ */ ((PasskeyCeremonyMode2) => {
2518
2520
  return PasskeyCeremonyMode2;
2519
2521
  })(PasskeyCeremonyMode || {});
2520
2522
 
2523
+ // src/passkey/browserLocation.ts
2524
+ function getBrowserLocation() {
2525
+ if (typeof window === "undefined") {
2526
+ return null;
2527
+ }
2528
+ const browserLocation = window.location;
2529
+ if (browserLocation === void 0) {
2530
+ return null;
2531
+ }
2532
+ return browserLocation;
2533
+ }
2534
+
2521
2535
  // src/passkey/passkeyNavigation.ts
2522
2536
  var PASSKEY_LOGIN_ENDPOINT = "/bff/passkey/login";
2523
2537
  var PASSKEY_REGISTER_ENDPOINT = "/bff/passkey/register";
@@ -2531,22 +2545,25 @@ function buildNavigationUrl(endpoint, returnUrl) {
2531
2545
  return `${endpoint}?${RETURN_URL_PARAM}=${encodeURIComponent(returnUrl)}`;
2532
2546
  }
2533
2547
  function readSearchParams() {
2534
- if (typeof window === "undefined") {
2548
+ const browserLocation = getBrowserLocation();
2549
+ if (browserLocation === null) {
2535
2550
  return null;
2536
2551
  }
2537
- return new URLSearchParams(window.location.search);
2552
+ return new URLSearchParams(browserLocation.search);
2538
2553
  }
2539
2554
  function startPasskeyLogin(returnUrl) {
2540
- if (typeof window === "undefined") {
2555
+ const browserLocation = getBrowserLocation();
2556
+ if (browserLocation === null) {
2541
2557
  return;
2542
2558
  }
2543
- window.location.assign(buildNavigationUrl(PASSKEY_LOGIN_ENDPOINT, returnUrl));
2559
+ browserLocation.assign(buildNavigationUrl(PASSKEY_LOGIN_ENDPOINT, returnUrl));
2544
2560
  }
2545
2561
  function startPasskeyRegistration(returnUrl) {
2546
- if (typeof window === "undefined") {
2562
+ const browserLocation = getBrowserLocation();
2563
+ if (browserLocation === null) {
2547
2564
  return;
2548
2565
  }
2549
- window.location.assign(buildNavigationUrl(PASSKEY_REGISTER_ENDPOINT, returnUrl));
2566
+ browserLocation.assign(buildNavigationUrl(PASSKEY_REGISTER_ENDPOINT, returnUrl));
2550
2567
  }
2551
2568
  function readPasskeyError() {
2552
2569
  const params = readSearchParams();
@@ -2723,8 +2740,9 @@ function usePasskeyCeremony(optionsArg) {
2723
2740
  onSuccess(user);
2724
2741
  return;
2725
2742
  }
2726
- if (mode === "login" /* Login */ && typeof window !== "undefined") {
2727
- window.location.assign(returnUrl);
2743
+ const browserLocation = getBrowserLocation();
2744
+ if (mode === "login" /* Login */ && browserLocation !== null) {
2745
+ browserLocation.assign(returnUrl);
2728
2746
  }
2729
2747
  },
2730
2748
  [mode, onSuccess, returnUrl]
@@ -2746,6 +2764,10 @@ function usePasskeyCeremony(optionsArg) {
2746
2764
  const envelope = await optionsResponse.json();
2747
2765
  const credential = await runCeremony(mode, envelope);
2748
2766
  if (credential === null) {
2767
+ if (mode === "login" /* Login */) {
2768
+ redirectFallback(mode, returnUrl);
2769
+ return;
2770
+ }
2749
2771
  setStatus("error" /* Error */);
2750
2772
  setError(ERROR_FAILED);
2751
2773
  return;
@@ -2777,13 +2799,16 @@ function usePasskeyCeremony(optionsArg) {
2777
2799
  }
2778
2800
  })();
2779
2801
  }, [isSupported, mode, returnUrl, succeed]);
2802
+ const fallbackToRedirect = react.useCallback(() => {
2803
+ redirectFallback(mode, returnUrl);
2804
+ }, [mode, returnUrl]);
2780
2805
  const reset = react.useCallback(() => {
2781
2806
  setStatus("idle" /* Idle */);
2782
2807
  setError(null);
2783
2808
  }, []);
2784
2809
  return react.useMemo(
2785
- () => ({ status, error, isSupported, start, reset }),
2786
- [status, error, isSupported, start, reset]
2810
+ () => ({ status, error, isSupported, start, fallbackToRedirect, reset }),
2811
+ [status, error, isSupported, start, fallbackToRedirect, reset]
2787
2812
  );
2788
2813
  }
2789
2814
  var DEFAULT_RETURN_URL = "/";
@@ -2802,14 +2827,27 @@ function PasskeyLoginButton({
2802
2827
  const ceremony = usePasskeyCeremony({ mode: "login" /* Login */, returnUrl });
2803
2828
  const error = ceremony.error ?? readPasskeyError();
2804
2829
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { testID: withTestIdPrefix(AuthTestIds.passkeyLogin, testIdPrefix), children: [
2805
- error !== null ? /* @__PURE__ */ jsxRuntime.jsx(
2806
- reactNative.Text,
2807
- {
2808
- style: styles3.errorText,
2809
- testID: withTestIdPrefix(AuthTestIds.passkeyLoginError, testIdPrefix),
2810
- children: error === "cancelled" ? labels.errorCancelled : labels.errorFailed
2811
- }
2812
- ) : null,
2830
+ error !== null ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2831
+ /* @__PURE__ */ jsxRuntime.jsx(
2832
+ reactNative.Text,
2833
+ {
2834
+ style: styles3.errorText,
2835
+ testID: withTestIdPrefix(AuthTestIds.passkeyLoginError, testIdPrefix),
2836
+ children: error === "cancelled" ? labels.errorCancelled : labels.errorFailed
2837
+ }
2838
+ ),
2839
+ /* @__PURE__ */ jsxRuntime.jsx(
2840
+ reactNative.TouchableOpacity,
2841
+ {
2842
+ accessibilityHint: labels.tryAnotherWay,
2843
+ accessibilityLabel: labels.tryAnotherWay,
2844
+ accessibilityRole: "button",
2845
+ testID: withTestIdPrefix(AuthTestIds.passkeyLoginFallback, testIdPrefix),
2846
+ onPress: () => ceremony.fallbackToRedirect(),
2847
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.linkText, children: labels.tryAnotherWay })
2848
+ }
2849
+ )
2850
+ ] }) : null,
2813
2851
  /* @__PURE__ */ jsxRuntime.jsx(
2814
2852
  reactNative.TouchableOpacity,
2815
2853
  {
@@ -2826,10 +2864,11 @@ function PasskeyLoginButton({
2826
2864
  }
2827
2865
  var DEFAULT_RETURN_PATH = "/";
2828
2866
  function readCurrentPath() {
2829
- if (typeof window === "undefined") {
2867
+ const browserLocation = getBrowserLocation();
2868
+ if (browserLocation === null) {
2830
2869
  return DEFAULT_RETURN_PATH;
2831
2870
  }
2832
- return window.location.pathname;
2871
+ return browserLocation.pathname;
2833
2872
  }
2834
2873
  function PasskeySettingsCard({
2835
2874
  returnUrl,