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