@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.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",
@@ -411,6 +413,11 @@ function useAuthStyles(theme) {
411
413
  primaryButton: {
412
414
  borderRadius: radii.input,
413
415
  paddingVertical: BUTTON_VERTICAL_PADDING,
416
+ paddingHorizontal: spacing.md,
417
+ // Never shrink below the label width: in a row of actions (e.g. the
418
+ // forgot-password modal's Cancel + submit), shrinking would clip a long
419
+ // label like "Send reset link" past the button's rounded edge.
420
+ flexShrink: 0,
414
421
  alignItems: "center",
415
422
  justifyContent: "center",
416
423
  marginTop: spacing.xs,
@@ -453,6 +460,8 @@ function useAuthStyles(theme) {
453
460
  borderWidth: 1,
454
461
  borderColor: colors.primary,
455
462
  paddingVertical: BUTTON_VERTICAL_PADDING,
463
+ paddingHorizontal: spacing.md,
464
+ flexShrink: 0,
456
465
  alignItems: "center",
457
466
  justifyContent: "center",
458
467
  marginTop: spacing.sm,
@@ -895,6 +904,9 @@ function useModalStyles(theme) {
895
904
  flexDirection: "row",
896
905
  justifyContent: "flex-end",
897
906
  alignItems: "center",
907
+ // Wrap (rather than clip) when a long submit label + Cancel can't
908
+ // fit a narrow modal on one line.
909
+ flexWrap: "wrap",
898
910
  marginTop: theme.spacing.md,
899
911
  gap: theme.spacing.sm
900
912
  },
@@ -902,6 +914,8 @@ function useModalStyles(theme) {
902
914
  borderRadius: theme.radii.input,
903
915
  paddingVertical: theme.spacing.sm,
904
916
  paddingHorizontal: theme.spacing.md,
917
+ // Match the submit button: never shrink below the label width.
918
+ flexShrink: 0,
905
919
  borderWidth: SECONDARY_BORDER_WIDTH,
906
920
  borderColor: theme.colors.border
907
921
  },
@@ -2499,6 +2513,13 @@ function DevicePinSettingsCard({
2499
2513
  }
2500
2514
  }
2501
2515
 
2516
+ // src/passkey/PasskeyCeremonyMode.ts
2517
+ var PasskeyCeremonyMode = /* @__PURE__ */ ((PasskeyCeremonyMode2) => {
2518
+ PasskeyCeremonyMode2["Login"] = "login";
2519
+ PasskeyCeremonyMode2["Register"] = "register";
2520
+ return PasskeyCeremonyMode2;
2521
+ })(PasskeyCeremonyMode || {});
2522
+
2502
2523
  // src/passkey/passkeyNavigation.ts
2503
2524
  var PASSKEY_LOGIN_ENDPOINT = "/bff/passkey/login";
2504
2525
  var PASSKEY_REGISTER_ENDPOINT = "/bff/passkey/register";
@@ -2550,6 +2571,230 @@ function readPasskeyRegistered() {
2550
2571
  }
2551
2572
  return params.get(PASSKEY_PARAM) === PASSKEY_REGISTERED_VALUE;
2552
2573
  }
2574
+
2575
+ // src/passkey/PasskeyCeremonyStatus.ts
2576
+ var PasskeyCeremonyStatus = /* @__PURE__ */ ((PasskeyCeremonyStatus2) => {
2577
+ PasskeyCeremonyStatus2["Idle"] = "idle";
2578
+ PasskeyCeremonyStatus2["Running"] = "running";
2579
+ PasskeyCeremonyStatus2["Success"] = "success";
2580
+ PasskeyCeremonyStatus2["Error"] = "error";
2581
+ return PasskeyCeremonyStatus2;
2582
+ })(PasskeyCeremonyStatus || {});
2583
+
2584
+ // src/passkey/base64url.ts
2585
+ function base64UrlToBytes(value) {
2586
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
2587
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
2588
+ const binary = atob(padded);
2589
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
2590
+ for (let i = 0; i < binary.length; i += 1) {
2591
+ bytes[i] = binary.charCodeAt(i);
2592
+ }
2593
+ return bytes;
2594
+ }
2595
+ function bytesToBase64Url(buffer) {
2596
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
2597
+ let binary = "";
2598
+ for (let i = 0; i < bytes.length; i += 1) {
2599
+ binary += String.fromCharCode(bytes[i]);
2600
+ }
2601
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2602
+ }
2603
+
2604
+ // src/passkey/webAuthnCoercion.ts
2605
+ var PUBLIC_KEY_TYPE = "public-key";
2606
+ function toDescriptors(value) {
2607
+ if (!Array.isArray(value)) {
2608
+ return void 0;
2609
+ }
2610
+ return value.map((entry) => {
2611
+ const descriptor = entry;
2612
+ return {
2613
+ type: PUBLIC_KEY_TYPE,
2614
+ id: base64UrlToBytes(descriptor.id),
2615
+ transports: descriptor.transports
2616
+ };
2617
+ });
2618
+ }
2619
+ function toCreationOptions(options) {
2620
+ const user = options.user;
2621
+ return {
2622
+ ...options,
2623
+ challenge: base64UrlToBytes(options.challenge),
2624
+ user: { ...user, id: base64UrlToBytes(user.id) },
2625
+ excludeCredentials: toDescriptors(options.excludeCredentials)
2626
+ };
2627
+ }
2628
+ function toRequestOptions(options) {
2629
+ return {
2630
+ ...options,
2631
+ challenge: base64UrlToBytes(options.challenge),
2632
+ allowCredentials: toDescriptors(options.allowCredentials)
2633
+ };
2634
+ }
2635
+ function readTransports(response) {
2636
+ return typeof response.getTransports === "function" ? response.getTransports() : [];
2637
+ }
2638
+ function encodeAttestation(credential) {
2639
+ const response = credential.response;
2640
+ return {
2641
+ id: credential.id,
2642
+ rawId: bytesToBase64Url(credential.rawId),
2643
+ type: credential.type,
2644
+ response: {
2645
+ attestationObject: bytesToBase64Url(response.attestationObject),
2646
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
2647
+ transports: readTransports(response)
2648
+ },
2649
+ clientExtensionResults: credential.getClientExtensionResults()
2650
+ };
2651
+ }
2652
+ function encodeAssertion(credential) {
2653
+ const response = credential.response;
2654
+ const userHandle = response.userHandle !== null ? bytesToBase64Url(response.userHandle) : null;
2655
+ return {
2656
+ id: credential.id,
2657
+ rawId: bytesToBase64Url(credential.rawId),
2658
+ type: credential.type,
2659
+ response: {
2660
+ authenticatorData: bytesToBase64Url(response.authenticatorData),
2661
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
2662
+ signature: bytesToBase64Url(response.signature),
2663
+ userHandle
2664
+ },
2665
+ clientExtensionResults: credential.getClientExtensionResults()
2666
+ };
2667
+ }
2668
+
2669
+ // src/passkey/usePasskeyCeremony.ts
2670
+ var ERROR_CANCELLED = "cancelled";
2671
+ var ERROR_FAILED = "failed";
2672
+ var CANCEL_ERROR_NAMES = ["NotAllowedError", "AbortError"];
2673
+ var ENDPOINTS = {
2674
+ ["login" /* Login */]: {
2675
+ options: "/bff/passkey/login/options",
2676
+ verify: "/bff/passkey/login/verify"
2677
+ },
2678
+ ["register" /* Register */]: {
2679
+ options: "/bff/passkey/register/options",
2680
+ verify: "/bff/passkey/register/verify"
2681
+ }
2682
+ };
2683
+ function detectSupport() {
2684
+ return typeof window !== "undefined" && typeof navigator !== "undefined" && typeof navigator.credentials !== "undefined" && typeof window.PublicKeyCredential !== "undefined";
2685
+ }
2686
+ function postJson(url, body) {
2687
+ return fetch(url, {
2688
+ method: "POST",
2689
+ credentials: "same-origin",
2690
+ headers: { "Content-Type": "application/json" },
2691
+ body: JSON.stringify(body ?? {})
2692
+ });
2693
+ }
2694
+ async function runCeremony(mode, envelope) {
2695
+ const credential = mode === "register" /* Register */ ? await navigator.credentials.create({
2696
+ publicKey: toCreationOptions(envelope.options)
2697
+ }) : await navigator.credentials.get({
2698
+ publicKey: toRequestOptions(envelope.options)
2699
+ });
2700
+ return credential;
2701
+ }
2702
+ function buildVerifyBody(mode, challengeId, credential) {
2703
+ return mode === "register" /* Register */ ? { challengeId, attestationResponse: encodeAttestation(credential) } : { challengeId, assertionResponse: encodeAssertion(credential) };
2704
+ }
2705
+ function redirectFallback(mode, returnUrl) {
2706
+ if (mode === "register" /* Register */) {
2707
+ startPasskeyRegistration(returnUrl);
2708
+ return;
2709
+ }
2710
+ startPasskeyLogin(returnUrl);
2711
+ }
2712
+ function classifyError(caught) {
2713
+ const name = caught instanceof Error ? caught.name : "";
2714
+ return CANCEL_ERROR_NAMES.includes(name) ? ERROR_CANCELLED : ERROR_FAILED;
2715
+ }
2716
+ function usePasskeyCeremony(optionsArg) {
2717
+ const { mode, returnUrl, onSuccess } = optionsArg;
2718
+ const [status, setStatus] = react.useState("idle" /* Idle */);
2719
+ const [error, setError] = react.useState(null);
2720
+ const isSupported = react.useMemo(detectSupport, []);
2721
+ const succeed = react.useCallback(
2722
+ (user) => {
2723
+ setStatus("success" /* Success */);
2724
+ if (onSuccess !== void 0) {
2725
+ onSuccess(user);
2726
+ return;
2727
+ }
2728
+ if (mode === "login" /* Login */ && typeof window !== "undefined") {
2729
+ window.location.assign(returnUrl);
2730
+ }
2731
+ },
2732
+ [mode, onSuccess, returnUrl]
2733
+ );
2734
+ const start = react.useCallback(() => {
2735
+ if (!isSupported) {
2736
+ redirectFallback(mode, returnUrl);
2737
+ return;
2738
+ }
2739
+ setStatus("running" /* Running */);
2740
+ setError(null);
2741
+ void (async () => {
2742
+ try {
2743
+ const optionsResponse = await postJson(ENDPOINTS[mode].options, {});
2744
+ if (!optionsResponse.ok) {
2745
+ redirectFallback(mode, returnUrl);
2746
+ return;
2747
+ }
2748
+ const envelope = await optionsResponse.json();
2749
+ const credential = await runCeremony(mode, envelope);
2750
+ if (credential === null) {
2751
+ if (mode === "login" /* Login */) {
2752
+ redirectFallback(mode, returnUrl);
2753
+ return;
2754
+ }
2755
+ setStatus("error" /* Error */);
2756
+ setError(ERROR_FAILED);
2757
+ return;
2758
+ }
2759
+ const verifyResponse = await postJson(
2760
+ ENDPOINTS[mode].verify,
2761
+ buildVerifyBody(mode, envelope.challengeId, credential)
2762
+ );
2763
+ if (!verifyResponse.ok) {
2764
+ setStatus("error" /* Error */);
2765
+ setError(ERROR_FAILED);
2766
+ return;
2767
+ }
2768
+ if (mode === "register" /* Register */) {
2769
+ const { success } = await verifyResponse.json();
2770
+ if (!success) {
2771
+ setStatus("error" /* Error */);
2772
+ setError(ERROR_FAILED);
2773
+ return;
2774
+ }
2775
+ succeed(void 0);
2776
+ return;
2777
+ }
2778
+ const { user } = await verifyResponse.json();
2779
+ succeed(user);
2780
+ } catch (caught) {
2781
+ setStatus("error" /* Error */);
2782
+ setError(classifyError(caught));
2783
+ }
2784
+ })();
2785
+ }, [isSupported, mode, returnUrl, succeed]);
2786
+ const fallbackToRedirect = react.useCallback(() => {
2787
+ redirectFallback(mode, returnUrl);
2788
+ }, [mode, returnUrl]);
2789
+ const reset = react.useCallback(() => {
2790
+ setStatus("idle" /* Idle */);
2791
+ setError(null);
2792
+ }, []);
2793
+ return react.useMemo(
2794
+ () => ({ status, error, isSupported, start, fallbackToRedirect, reset }),
2795
+ [status, error, isSupported, start, fallbackToRedirect, reset]
2796
+ );
2797
+ }
2553
2798
  var DEFAULT_RETURN_URL = "/";
2554
2799
  function PasskeyLoginButton({
2555
2800
  returnUrl = DEFAULT_RETURN_URL,
@@ -2563,16 +2808,30 @@ function PasskeyLoginButton({
2563
2808
  () => ({ ...DEFAULT_PASSKEY_LOGIN_LABELS, ...labelsProp }),
2564
2809
  [labelsProp]
2565
2810
  );
2566
- const error = readPasskeyError();
2811
+ const ceremony = usePasskeyCeremony({ mode: "login" /* Login */, returnUrl });
2812
+ const error = ceremony.error ?? readPasskeyError();
2567
2813
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { testID: withTestIdPrefix(AuthTestIds.passkeyLogin, testIdPrefix), children: [
2568
- error !== null ? /* @__PURE__ */ jsxRuntime.jsx(
2569
- reactNative.Text,
2570
- {
2571
- style: styles3.errorText,
2572
- testID: withTestIdPrefix(AuthTestIds.passkeyLoginError, testIdPrefix),
2573
- children: error === "cancelled" ? labels.errorCancelled : labels.errorFailed
2574
- }
2575
- ) : null,
2814
+ error !== null ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2815
+ /* @__PURE__ */ jsxRuntime.jsx(
2816
+ reactNative.Text,
2817
+ {
2818
+ style: styles3.errorText,
2819
+ testID: withTestIdPrefix(AuthTestIds.passkeyLoginError, testIdPrefix),
2820
+ children: error === "cancelled" ? labels.errorCancelled : labels.errorFailed
2821
+ }
2822
+ ),
2823
+ /* @__PURE__ */ jsxRuntime.jsx(
2824
+ reactNative.TouchableOpacity,
2825
+ {
2826
+ accessibilityHint: labels.tryAnotherWay,
2827
+ accessibilityLabel: labels.tryAnotherWay,
2828
+ accessibilityRole: "button",
2829
+ testID: withTestIdPrefix(AuthTestIds.passkeyLoginFallback, testIdPrefix),
2830
+ onPress: () => ceremony.fallbackToRedirect(),
2831
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.linkText, children: labels.tryAnotherWay })
2832
+ }
2833
+ )
2834
+ ] }) : null,
2576
2835
  /* @__PURE__ */ jsxRuntime.jsx(
2577
2836
  reactNative.TouchableOpacity,
2578
2837
  {
@@ -2581,7 +2840,7 @@ function PasskeyLoginButton({
2581
2840
  accessibilityRole: "button",
2582
2841
  style: styles3.secondaryButton,
2583
2842
  testID: withTestIdPrefix(AuthTestIds.passkeyLoginButton, testIdPrefix),
2584
- onPress: () => startPasskeyLogin(returnUrl),
2843
+ onPress: () => ceremony.start(),
2585
2844
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.secondaryButtonText, children: labels.signInButton })
2586
2845
  }
2587
2846
  )
@@ -2606,8 +2865,12 @@ function PasskeySettingsCard({
2606
2865
  () => ({ ...DEFAULT_PASSKEY_SETTINGS_LABELS, ...labelsProp }),
2607
2866
  [labelsProp]
2608
2867
  );
2609
- const justRegistered = readPasskeyRegistered();
2610
2868
  const target = returnUrl ?? readCurrentPath();
2869
+ const ceremony = usePasskeyCeremony({
2870
+ mode: "register" /* Register */,
2871
+ returnUrl: target
2872
+ });
2873
+ const justRegistered = ceremony.status === "success" /* Success */ || readPasskeyRegistered();
2611
2874
  return /* @__PURE__ */ jsxRuntime.jsxs(
2612
2875
  reactNative.View,
2613
2876
  {
@@ -2633,7 +2896,7 @@ function PasskeySettingsCard({
2633
2896
  accessibilityRole: "button",
2634
2897
  style: styles3.primaryButton,
2635
2898
  testID: withTestIdPrefix(AuthTestIds.passkeySettingsAdd, testIdPrefix),
2636
- onPress: () => startPasskeyRegistration(target),
2899
+ onPress: () => ceremony.start(),
2637
2900
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.primaryButtonText, children: labels.addButton })
2638
2901
  }
2639
2902
  )
@@ -3049,6 +3312,8 @@ exports.OtpForm = OtpForm;
3049
3312
  exports.OtpLoginStep = OtpLoginStep;
3050
3313
  exports.PASSWORD_MAX_LENGTH = PASSWORD_MAX_LENGTH;
3051
3314
  exports.PASSWORD_MIN_LENGTH = PASSWORD_MIN_LENGTH;
3315
+ exports.PasskeyCeremonyMode = PasskeyCeremonyMode;
3316
+ exports.PasskeyCeremonyStatus = PasskeyCeremonyStatus;
3052
3317
  exports.PasskeyLoginButton = PasskeyLoginButton;
3053
3318
  exports.PasskeySettingsCard = PasskeySettingsCard;
3054
3319
  exports.PasswordPolicyError = PasswordPolicyError;
@@ -3084,6 +3349,7 @@ exports.useDevicePinEnroll = useDevicePinEnroll;
3084
3349
  exports.useDevicePinUnlock = useDevicePinUnlock;
3085
3350
  exports.useForgotPasswordSubmit = useForgotPasswordSubmit;
3086
3351
  exports.useOtpLogin = useOtpLogin;
3352
+ exports.usePasskeyCeremony = usePasskeyCeremony;
3087
3353
  exports.usePinLogin = usePinLogin;
3088
3354
  exports.usePreferredMethodSetting = usePreferredMethodSetting;
3089
3355
  exports.useResetPasswordForm = useResetPasswordForm;