@dloizides/auth-web 1.7.0 → 1.9.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.mjs CHANGED
@@ -235,7 +235,8 @@ var DEFAULT_PASSKEY_LOGIN_LABELS = {
235
235
  signInButton: "Sign in with a passkey",
236
236
  signInHint: "Use your fingerprint, face, or security key to sign in",
237
237
  errorCancelled: "Passkey sign-in was cancelled. You can try again or sign in another way.",
238
- errorFailed: "Passkey sign-in didn't work. Try again, or sign in another way."
238
+ errorFailed: "Passkey sign-in didn't work. Try again, or sign in another way.",
239
+ tryAnotherWay: "Sign in another way"
239
240
  };
240
241
  var DEFAULT_PASSKEY_SETTINGS_LABELS = {
241
242
  title: "Passkeys",
@@ -337,6 +338,7 @@ var AuthTestIds = {
337
338
  passkeyLogin: "auth-passkey-login",
338
339
  passkeyLoginButton: "auth-passkey-login-button",
339
340
  passkeyLoginError: "auth-passkey-login-error",
341
+ passkeyLoginFallback: "auth-passkey-login-fallback",
340
342
  // Passkey settings card
341
343
  passkeySettings: "auth-passkey-settings",
342
344
  passkeySettingsAdd: "auth-passkey-settings-add",
@@ -410,6 +412,11 @@ function useAuthStyles(theme) {
410
412
  primaryButton: {
411
413
  borderRadius: radii.input,
412
414
  paddingVertical: BUTTON_VERTICAL_PADDING,
415
+ paddingHorizontal: spacing.md,
416
+ // Never shrink below the label width: in a row of actions (e.g. the
417
+ // forgot-password modal's Cancel + submit), shrinking would clip a long
418
+ // label like "Send reset link" past the button's rounded edge.
419
+ flexShrink: 0,
413
420
  alignItems: "center",
414
421
  justifyContent: "center",
415
422
  marginTop: spacing.xs,
@@ -452,6 +459,8 @@ function useAuthStyles(theme) {
452
459
  borderWidth: 1,
453
460
  borderColor: colors.primary,
454
461
  paddingVertical: BUTTON_VERTICAL_PADDING,
462
+ paddingHorizontal: spacing.md,
463
+ flexShrink: 0,
455
464
  alignItems: "center",
456
465
  justifyContent: "center",
457
466
  marginTop: spacing.sm,
@@ -894,6 +903,9 @@ function useModalStyles(theme) {
894
903
  flexDirection: "row",
895
904
  justifyContent: "flex-end",
896
905
  alignItems: "center",
906
+ // Wrap (rather than clip) when a long submit label + Cancel can't
907
+ // fit a narrow modal on one line.
908
+ flexWrap: "wrap",
897
909
  marginTop: theme.spacing.md,
898
910
  gap: theme.spacing.sm
899
911
  },
@@ -901,6 +913,8 @@ function useModalStyles(theme) {
901
913
  borderRadius: theme.radii.input,
902
914
  paddingVertical: theme.spacing.sm,
903
915
  paddingHorizontal: theme.spacing.md,
916
+ // Match the submit button: never shrink below the label width.
917
+ flexShrink: 0,
904
918
  borderWidth: SECONDARY_BORDER_WIDTH,
905
919
  borderColor: theme.colors.border
906
920
  },
@@ -2498,6 +2512,13 @@ function DevicePinSettingsCard({
2498
2512
  }
2499
2513
  }
2500
2514
 
2515
+ // src/passkey/PasskeyCeremonyMode.ts
2516
+ var PasskeyCeremonyMode = /* @__PURE__ */ ((PasskeyCeremonyMode2) => {
2517
+ PasskeyCeremonyMode2["Login"] = "login";
2518
+ PasskeyCeremonyMode2["Register"] = "register";
2519
+ return PasskeyCeremonyMode2;
2520
+ })(PasskeyCeremonyMode || {});
2521
+
2501
2522
  // src/passkey/passkeyNavigation.ts
2502
2523
  var PASSKEY_LOGIN_ENDPOINT = "/bff/passkey/login";
2503
2524
  var PASSKEY_REGISTER_ENDPOINT = "/bff/passkey/register";
@@ -2549,6 +2570,230 @@ function readPasskeyRegistered() {
2549
2570
  }
2550
2571
  return params.get(PASSKEY_PARAM) === PASSKEY_REGISTERED_VALUE;
2551
2572
  }
2573
+
2574
+ // src/passkey/PasskeyCeremonyStatus.ts
2575
+ var PasskeyCeremonyStatus = /* @__PURE__ */ ((PasskeyCeremonyStatus2) => {
2576
+ PasskeyCeremonyStatus2["Idle"] = "idle";
2577
+ PasskeyCeremonyStatus2["Running"] = "running";
2578
+ PasskeyCeremonyStatus2["Success"] = "success";
2579
+ PasskeyCeremonyStatus2["Error"] = "error";
2580
+ return PasskeyCeremonyStatus2;
2581
+ })(PasskeyCeremonyStatus || {});
2582
+
2583
+ // src/passkey/base64url.ts
2584
+ function base64UrlToBytes(value) {
2585
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
2586
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
2587
+ const binary = atob(padded);
2588
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
2589
+ for (let i = 0; i < binary.length; i += 1) {
2590
+ bytes[i] = binary.charCodeAt(i);
2591
+ }
2592
+ return bytes;
2593
+ }
2594
+ function bytesToBase64Url(buffer) {
2595
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
2596
+ let binary = "";
2597
+ for (let i = 0; i < bytes.length; i += 1) {
2598
+ binary += String.fromCharCode(bytes[i]);
2599
+ }
2600
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2601
+ }
2602
+
2603
+ // src/passkey/webAuthnCoercion.ts
2604
+ var PUBLIC_KEY_TYPE = "public-key";
2605
+ function toDescriptors(value) {
2606
+ if (!Array.isArray(value)) {
2607
+ return void 0;
2608
+ }
2609
+ return value.map((entry) => {
2610
+ const descriptor = entry;
2611
+ return {
2612
+ type: PUBLIC_KEY_TYPE,
2613
+ id: base64UrlToBytes(descriptor.id),
2614
+ transports: descriptor.transports
2615
+ };
2616
+ });
2617
+ }
2618
+ function toCreationOptions(options) {
2619
+ const user = options.user;
2620
+ return {
2621
+ ...options,
2622
+ challenge: base64UrlToBytes(options.challenge),
2623
+ user: { ...user, id: base64UrlToBytes(user.id) },
2624
+ excludeCredentials: toDescriptors(options.excludeCredentials)
2625
+ };
2626
+ }
2627
+ function toRequestOptions(options) {
2628
+ return {
2629
+ ...options,
2630
+ challenge: base64UrlToBytes(options.challenge),
2631
+ allowCredentials: toDescriptors(options.allowCredentials)
2632
+ };
2633
+ }
2634
+ function readTransports(response) {
2635
+ return typeof response.getTransports === "function" ? response.getTransports() : [];
2636
+ }
2637
+ function encodeAttestation(credential) {
2638
+ const response = credential.response;
2639
+ return {
2640
+ id: credential.id,
2641
+ rawId: bytesToBase64Url(credential.rawId),
2642
+ type: credential.type,
2643
+ response: {
2644
+ attestationObject: bytesToBase64Url(response.attestationObject),
2645
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
2646
+ transports: readTransports(response)
2647
+ },
2648
+ clientExtensionResults: credential.getClientExtensionResults()
2649
+ };
2650
+ }
2651
+ function encodeAssertion(credential) {
2652
+ const response = credential.response;
2653
+ const userHandle = response.userHandle !== null ? bytesToBase64Url(response.userHandle) : null;
2654
+ return {
2655
+ id: credential.id,
2656
+ rawId: bytesToBase64Url(credential.rawId),
2657
+ type: credential.type,
2658
+ response: {
2659
+ authenticatorData: bytesToBase64Url(response.authenticatorData),
2660
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
2661
+ signature: bytesToBase64Url(response.signature),
2662
+ userHandle
2663
+ },
2664
+ clientExtensionResults: credential.getClientExtensionResults()
2665
+ };
2666
+ }
2667
+
2668
+ // src/passkey/usePasskeyCeremony.ts
2669
+ var ERROR_CANCELLED = "cancelled";
2670
+ var ERROR_FAILED = "failed";
2671
+ var CANCEL_ERROR_NAMES = ["NotAllowedError", "AbortError"];
2672
+ var ENDPOINTS = {
2673
+ ["login" /* Login */]: {
2674
+ options: "/bff/passkey/login/options",
2675
+ verify: "/bff/passkey/login/verify"
2676
+ },
2677
+ ["register" /* Register */]: {
2678
+ options: "/bff/passkey/register/options",
2679
+ verify: "/bff/passkey/register/verify"
2680
+ }
2681
+ };
2682
+ function detectSupport() {
2683
+ return typeof window !== "undefined" && typeof navigator !== "undefined" && typeof navigator.credentials !== "undefined" && typeof window.PublicKeyCredential !== "undefined";
2684
+ }
2685
+ function postJson(url, body) {
2686
+ return fetch(url, {
2687
+ method: "POST",
2688
+ credentials: "same-origin",
2689
+ headers: { "Content-Type": "application/json" },
2690
+ body: JSON.stringify(body ?? {})
2691
+ });
2692
+ }
2693
+ async function runCeremony(mode, envelope) {
2694
+ const credential = mode === "register" /* Register */ ? await navigator.credentials.create({
2695
+ publicKey: toCreationOptions(envelope.options)
2696
+ }) : await navigator.credentials.get({
2697
+ publicKey: toRequestOptions(envelope.options)
2698
+ });
2699
+ return credential;
2700
+ }
2701
+ function buildVerifyBody(mode, challengeId, credential) {
2702
+ return mode === "register" /* Register */ ? { challengeId, attestationResponse: encodeAttestation(credential) } : { challengeId, assertionResponse: encodeAssertion(credential) };
2703
+ }
2704
+ function redirectFallback(mode, returnUrl) {
2705
+ if (mode === "register" /* Register */) {
2706
+ startPasskeyRegistration(returnUrl);
2707
+ return;
2708
+ }
2709
+ startPasskeyLogin(returnUrl);
2710
+ }
2711
+ function classifyError(caught) {
2712
+ const name = caught instanceof Error ? caught.name : "";
2713
+ return CANCEL_ERROR_NAMES.includes(name) ? ERROR_CANCELLED : ERROR_FAILED;
2714
+ }
2715
+ function usePasskeyCeremony(optionsArg) {
2716
+ const { mode, returnUrl, onSuccess } = optionsArg;
2717
+ const [status, setStatus] = useState("idle" /* Idle */);
2718
+ const [error, setError] = useState(null);
2719
+ const isSupported = useMemo(detectSupport, []);
2720
+ const succeed = useCallback(
2721
+ (user) => {
2722
+ setStatus("success" /* Success */);
2723
+ if (onSuccess !== void 0) {
2724
+ onSuccess(user);
2725
+ return;
2726
+ }
2727
+ if (mode === "login" /* Login */ && typeof window !== "undefined") {
2728
+ window.location.assign(returnUrl);
2729
+ }
2730
+ },
2731
+ [mode, onSuccess, returnUrl]
2732
+ );
2733
+ const start = useCallback(() => {
2734
+ if (!isSupported) {
2735
+ redirectFallback(mode, returnUrl);
2736
+ return;
2737
+ }
2738
+ setStatus("running" /* Running */);
2739
+ setError(null);
2740
+ void (async () => {
2741
+ try {
2742
+ const optionsResponse = await postJson(ENDPOINTS[mode].options, {});
2743
+ if (!optionsResponse.ok) {
2744
+ redirectFallback(mode, returnUrl);
2745
+ return;
2746
+ }
2747
+ const envelope = await optionsResponse.json();
2748
+ const credential = await runCeremony(mode, envelope);
2749
+ if (credential === null) {
2750
+ if (mode === "login" /* Login */) {
2751
+ redirectFallback(mode, returnUrl);
2752
+ return;
2753
+ }
2754
+ setStatus("error" /* Error */);
2755
+ setError(ERROR_FAILED);
2756
+ return;
2757
+ }
2758
+ const verifyResponse = await postJson(
2759
+ ENDPOINTS[mode].verify,
2760
+ buildVerifyBody(mode, envelope.challengeId, credential)
2761
+ );
2762
+ if (!verifyResponse.ok) {
2763
+ setStatus("error" /* Error */);
2764
+ setError(ERROR_FAILED);
2765
+ return;
2766
+ }
2767
+ if (mode === "register" /* Register */) {
2768
+ const { success } = await verifyResponse.json();
2769
+ if (!success) {
2770
+ setStatus("error" /* Error */);
2771
+ setError(ERROR_FAILED);
2772
+ return;
2773
+ }
2774
+ succeed(void 0);
2775
+ return;
2776
+ }
2777
+ const { user } = await verifyResponse.json();
2778
+ succeed(user);
2779
+ } catch (caught) {
2780
+ setStatus("error" /* Error */);
2781
+ setError(classifyError(caught));
2782
+ }
2783
+ })();
2784
+ }, [isSupported, mode, returnUrl, succeed]);
2785
+ const fallbackToRedirect = useCallback(() => {
2786
+ redirectFallback(mode, returnUrl);
2787
+ }, [mode, returnUrl]);
2788
+ const reset = useCallback(() => {
2789
+ setStatus("idle" /* Idle */);
2790
+ setError(null);
2791
+ }, []);
2792
+ return useMemo(
2793
+ () => ({ status, error, isSupported, start, fallbackToRedirect, reset }),
2794
+ [status, error, isSupported, start, fallbackToRedirect, reset]
2795
+ );
2796
+ }
2552
2797
  var DEFAULT_RETURN_URL = "/";
2553
2798
  function PasskeyLoginButton({
2554
2799
  returnUrl = DEFAULT_RETURN_URL,
@@ -2562,16 +2807,30 @@ function PasskeyLoginButton({
2562
2807
  () => ({ ...DEFAULT_PASSKEY_LOGIN_LABELS, ...labelsProp }),
2563
2808
  [labelsProp]
2564
2809
  );
2565
- const error = readPasskeyError();
2810
+ const ceremony = usePasskeyCeremony({ mode: "login" /* Login */, returnUrl });
2811
+ const error = ceremony.error ?? readPasskeyError();
2566
2812
  return /* @__PURE__ */ jsxs(View, { testID: withTestIdPrefix(AuthTestIds.passkeyLogin, testIdPrefix), children: [
2567
- error !== null ? /* @__PURE__ */ jsx(
2568
- Text,
2569
- {
2570
- style: styles3.errorText,
2571
- testID: withTestIdPrefix(AuthTestIds.passkeyLoginError, testIdPrefix),
2572
- children: error === "cancelled" ? labels.errorCancelled : labels.errorFailed
2573
- }
2574
- ) : null,
2813
+ error !== null ? /* @__PURE__ */ jsxs(Fragment, { children: [
2814
+ /* @__PURE__ */ jsx(
2815
+ Text,
2816
+ {
2817
+ style: styles3.errorText,
2818
+ testID: withTestIdPrefix(AuthTestIds.passkeyLoginError, testIdPrefix),
2819
+ children: error === "cancelled" ? labels.errorCancelled : labels.errorFailed
2820
+ }
2821
+ ),
2822
+ /* @__PURE__ */ jsx(
2823
+ TouchableOpacity,
2824
+ {
2825
+ accessibilityHint: labels.tryAnotherWay,
2826
+ accessibilityLabel: labels.tryAnotherWay,
2827
+ accessibilityRole: "button",
2828
+ testID: withTestIdPrefix(AuthTestIds.passkeyLoginFallback, testIdPrefix),
2829
+ onPress: () => ceremony.fallbackToRedirect(),
2830
+ children: /* @__PURE__ */ jsx(Text, { style: styles3.linkText, children: labels.tryAnotherWay })
2831
+ }
2832
+ )
2833
+ ] }) : null,
2575
2834
  /* @__PURE__ */ jsx(
2576
2835
  TouchableOpacity,
2577
2836
  {
@@ -2580,7 +2839,7 @@ function PasskeyLoginButton({
2580
2839
  accessibilityRole: "button",
2581
2840
  style: styles3.secondaryButton,
2582
2841
  testID: withTestIdPrefix(AuthTestIds.passkeyLoginButton, testIdPrefix),
2583
- onPress: () => startPasskeyLogin(returnUrl),
2842
+ onPress: () => ceremony.start(),
2584
2843
  children: /* @__PURE__ */ jsx(Text, { style: styles3.secondaryButtonText, children: labels.signInButton })
2585
2844
  }
2586
2845
  )
@@ -2605,8 +2864,12 @@ function PasskeySettingsCard({
2605
2864
  () => ({ ...DEFAULT_PASSKEY_SETTINGS_LABELS, ...labelsProp }),
2606
2865
  [labelsProp]
2607
2866
  );
2608
- const justRegistered = readPasskeyRegistered();
2609
2867
  const target = returnUrl ?? readCurrentPath();
2868
+ const ceremony = usePasskeyCeremony({
2869
+ mode: "register" /* Register */,
2870
+ returnUrl: target
2871
+ });
2872
+ const justRegistered = ceremony.status === "success" /* Success */ || readPasskeyRegistered();
2610
2873
  return /* @__PURE__ */ jsxs(
2611
2874
  View,
2612
2875
  {
@@ -2632,7 +2895,7 @@ function PasskeySettingsCard({
2632
2895
  accessibilityRole: "button",
2633
2896
  style: styles3.primaryButton,
2634
2897
  testID: withTestIdPrefix(AuthTestIds.passkeySettingsAdd, testIdPrefix),
2635
- onPress: () => startPasskeyRegistration(target),
2898
+ onPress: () => ceremony.start(),
2636
2899
  children: /* @__PURE__ */ jsx(Text, { style: styles3.primaryButtonText, children: labels.addButton })
2637
2900
  }
2638
2901
  )
@@ -3005,6 +3268,6 @@ function resolveAccessibleRoutes(user, table) {
3005
3268
  return table.routes.filter((entry) => userRoles.has(entry.role));
3006
3269
  }
3007
3270
 
3008
- export { AuthScreen, AuthTestIds, AuthThemeProvider, BffAuthStatus, DEFAULT_AUTH_CARD_MAX_WIDTH, 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_PREFERRED_METHOD_HINT_LABELS, DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEVICE_PIN_ALLOWED_DIGITS, DEVICE_PIN_DEFAULT_DIGITS, DevicePinEnrollErrorKey, DevicePinEnrollForm, DevicePinErrorKey, DevicePinInput, DevicePinLengthPicker, DevicePinOffer, DevicePinSettingsCard, DevicePinUnlockScreen, ForgotPasswordFields, ForgotPasswordForm, LoginForm, OtpForm, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasskeyLoginButton, PasskeySettingsCard, PasswordPolicyError, PinForm, PreferredMethodHint, PreferredMethodSettingsCard, ResetPasswordError, ResetPasswordForm, cachePreferredMethod, clearCachedPreferredMethod, collectUserRoles, createBffAuthClient, createPreferencesClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readCachedPreferredMethod, readPasskeyError, readPasskeyRegistered, resolveAccessibleRoutes, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, syncPreferredMethodFromServer, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePinLogin, usePreferredMethodSetting, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
3271
+ export { AuthScreen, AuthTestIds, AuthThemeProvider, BffAuthStatus, DEFAULT_AUTH_CARD_MAX_WIDTH, 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_PREFERRED_METHOD_HINT_LABELS, DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEVICE_PIN_ALLOWED_DIGITS, DEVICE_PIN_DEFAULT_DIGITS, DevicePinEnrollErrorKey, DevicePinEnrollForm, DevicePinErrorKey, DevicePinInput, DevicePinLengthPicker, DevicePinOffer, DevicePinSettingsCard, DevicePinUnlockScreen, ForgotPasswordFields, ForgotPasswordForm, LoginForm, OtpForm, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasskeyCeremonyMode, PasskeyCeremonyStatus, PasskeyLoginButton, PasskeySettingsCard, PasswordPolicyError, PinForm, PreferredMethodHint, PreferredMethodSettingsCard, ResetPasswordError, ResetPasswordForm, cachePreferredMethod, clearCachedPreferredMethod, collectUserRoles, createBffAuthClient, createPreferencesClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readCachedPreferredMethod, readPasskeyError, readPasskeyRegistered, resolveAccessibleRoutes, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, syncPreferredMethodFromServer, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePasskeyCeremony, usePinLogin, usePreferredMethodSetting, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
3009
3272
  //# sourceMappingURL=index.mjs.map
3010
3273
  //# sourceMappingURL=index.mjs.map