@authowl/react 0.25.2 → 0.25.3

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.cjs CHANGED
@@ -4649,26 +4649,32 @@ var KNOWN_METHODS = [
4649
4649
  function emailAutocomplete(isPasskeyHost) {
4650
4650
  return isPasskeyHost ? "email webauthn" : "email";
4651
4651
  }
4652
- function passkeyReachableFrom(authBaseUrl, pageHost, relyingPartyId) {
4653
- if (pageHost === void 0) return true;
4654
- let rpId = relyingPartyId ?? void 0;
4655
- if (!rpId) {
4656
- if (!authBaseUrl) return true;
4657
- try {
4658
- rpId = new URL(authBaseUrl).hostname;
4659
- } catch {
4660
- return true;
4661
- }
4652
+ function hostCoveredByRelyingParty(pageHost, rpId) {
4653
+ const host = pageHost.toLowerCase();
4654
+ const id = rpId.toLowerCase();
4655
+ if (id !== "localhost" && !id.includes(".")) return false;
4656
+ return host === id || host.endsWith(`.${id}`);
4657
+ }
4658
+ function resolveRelyingPartyId(authBaseUrl, relyingPartyId) {
4659
+ if (relyingPartyId) return relyingPartyId;
4660
+ if (!authBaseUrl) return void 0;
4661
+ try {
4662
+ return new URL(authBaseUrl).hostname || void 0;
4663
+ } catch {
4664
+ return void 0;
4662
4665
  }
4663
- if (rpId !== "localhost" && !rpId.includes(".")) return false;
4664
- return pageHost === rpId || pageHost.endsWith(`.${rpId}`);
4665
4666
  }
4666
- function passkeyReachableForConfig(config, pageHost) {
4667
- return passkeyReachableFrom(
4667
+ function passkeyBlockingDomain(config, pageHost) {
4668
+ if (pageHost === void 0) return void 0;
4669
+ const rpId = resolveRelyingPartyId(
4668
4670
  config?.authBaseUrl,
4669
- pageHost,
4670
4671
  config?.authentication?.passkey?.relyingPartyId
4671
4672
  );
4673
+ if (rpId === void 0) return void 0;
4674
+ return hostCoveredByRelyingParty(pageHost, rpId) ? void 0 : rpId;
4675
+ }
4676
+ function passkeyReachableForConfig(config, pageHost) {
4677
+ return passkeyBlockingDomain(config, pageHost) === void 0;
4672
4678
  }
4673
4679
  function resolveSignInMethods(config, pageHost) {
4674
4680
  const methods = config?.enabledMethods ?? ["password"];
@@ -4713,6 +4719,11 @@ var React20 = __toESM(require("react"), 1);
4713
4719
  init_hooks();
4714
4720
  init_i18n();
4715
4721
 
4722
+ // src/components/page-host.ts
4723
+ function currentPageHost() {
4724
+ return typeof window === "undefined" ? void 0 : window.location.hostname;
4725
+ }
4726
+
4716
4727
  // src/components/redirect.ts
4717
4728
  function isSafeRedirect(redirectTo, origin) {
4718
4729
  try {
@@ -4950,6 +4961,10 @@ function PasskeyButton({ redirectTo, onSignedIn }) {
4950
4961
  const { sessionStore } = useAuthClient();
4951
4962
  const { signInPasskey } = useSignIn();
4952
4963
  const { pending, error, run } = useSubmitAction();
4964
+ const { config, isLoading: configLoading, isError: configError } = usePublicConfig();
4965
+ const configPending = configLoading || config === null && !configError;
4966
+ const blocked = passkeyBlockingDomain(config, currentPageHost()) !== void 0;
4967
+ if (configPending || blocked) return null;
4953
4968
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ba-fields", children: [
4954
4969
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4955
4970
  "button",
@@ -6103,10 +6118,7 @@ function SignIn({
6103
6118
  recordSignInMethod("phone-otp");
6104
6119
  onSignedIn?.();
6105
6120
  }, [onSignedIn, recordSignInMethod]);
6106
- const plan = resolveSignInMethods(
6107
- config,
6108
- typeof window === "undefined" ? void 0 : window.location.hostname
6109
- );
6121
+ const plan = resolveSignInMethods(config, currentPageHost());
6110
6122
  usePasskeyAutofill({
6111
6123
  enabled: credentialMode === "email" && plan.autofillHost !== null,
6112
6124
  redirectTo,
@@ -7625,6 +7637,9 @@ function PasskeyManagerBody({
7625
7637
  const api = usePasskeys();
7626
7638
  const apiRef = React31.useRef(api);
7627
7639
  apiRef.current = api;
7640
+ const { config, isLoading: configLoading, isError: configError } = usePublicConfig();
7641
+ const blockedBy = passkeyBlockingDomain(config, currentPageHost());
7642
+ const configPending = configLoading || config === null && !configError;
7628
7643
  const { pending, error, setError, run } = useSubmitAction();
7629
7644
  const [passkeys, setPasskeys] = React31.useState(null);
7630
7645
  const loadTokenRef = React31.useRef(0);
@@ -7697,7 +7712,12 @@ function PasskeyManagerBody({
7697
7712
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(FormError, { children: error }),
7698
7713
  passkeys === null && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { className: "ba-link-button", type: "button", onClick: () => void load(), children: t("userProfile.retry") })
7699
7714
  ] }),
7700
- passkeys !== null && allowAdd && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
7715
+ passkeys !== null && allowAdd && !configPending && (blockedBy !== void 0 ? (
7716
+ // The add button is HIDDEN, not disabled, and the list stays: a user
7717
+ // may hold passkeys enrolled on the relying-party origin and must
7718
+ // still be able to rename or remove them from here.
7719
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "ba-muted", "data-testid": "passkey-add-elsewhere", children: t("passkeys.addElsewhere", { domain: blockedBy }) })
7720
+ ) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
7701
7721
  "button",
7702
7722
  {
7703
7723
  className: "ba-button",
@@ -7710,7 +7730,7 @@ function PasskeyManagerBody({
7710
7730
  }),
7711
7731
  children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Busy, { busy: pending, label: t("common.working"), children: t("passkeys.add") })
7712
7732
  }
7713
- )
7733
+ ))
7714
7734
  ] });
7715
7735
  }
7716
7736
 
@@ -7729,7 +7749,7 @@ function usePasskeyOffer() {
7729
7749
  const api = React32.useRef(listPasskeys);
7730
7750
  api.current = listPasskeys;
7731
7751
  const projectId = config?.environmentId ?? null;
7732
- const eligible = projectId !== null && isSignedIn && (0, import_core4.resolveProjectCapabilities)(config).passkeyAdd && typeof window !== "undefined" && "PublicKeyCredential" in window && passkeyReachableForConfig(config, window.location.hostname);
7752
+ const eligible = projectId !== null && isSignedIn && (0, import_core4.resolveProjectCapabilities)(config).passkeyAdd && typeof window !== "undefined" && "PublicKeyCredential" in window && passkeyReachableForConfig(config, currentPageHost());
7733
7753
  const userId = user?.id ?? null;
7734
7754
  const sessionId = sessionData?.session?.id ?? null;
7735
7755
  const subject = React32.useMemo(
package/dist/index.d.cts CHANGED
@@ -372,6 +372,11 @@ type SignInPlan = {
372
372
  * passkeys inline. One place owns the token string.
373
373
  */
374
374
  declare function emailAutocomplete(isPasskeyHost: boolean): string;
375
+ /**
376
+ * Resolve the sign-in surfaces from a project's public config. A null config
377
+ * gets a structural password default for callers that resolve before the fetch
378
+ * settles; the drop-in components fail closed when the fetch itself errors.
379
+ */
375
380
  declare function resolveSignInMethods(config: PublicConfig | null,
376
381
  /** The hostname the form is rendering on; omit when it is not knowable. */
377
382
  pageHost?: string): SignInPlan;
@@ -581,7 +586,7 @@ type PasskeyButtonProps = {
581
586
  * click. Complements the inline conditional-mediation autofill armed by
582
587
  * <SignIn/> - the button always works even where autofill is unavailable.
583
588
  */
584
- declare function PasskeyButton({ redirectTo, onSignedIn }: PasskeyButtonProps): React.JSX.Element;
589
+ declare function PasskeyButton({ redirectTo, onSignedIn }: PasskeyButtonProps): React.JSX.Element | null;
585
590
 
586
591
  type ForgotPasswordProps = {
587
592
  /**
package/dist/index.d.ts CHANGED
@@ -372,6 +372,11 @@ type SignInPlan = {
372
372
  * passkeys inline. One place owns the token string.
373
373
  */
374
374
  declare function emailAutocomplete(isPasskeyHost: boolean): string;
375
+ /**
376
+ * Resolve the sign-in surfaces from a project's public config. A null config
377
+ * gets a structural password default for callers that resolve before the fetch
378
+ * settles; the drop-in components fail closed when the fetch itself errors.
379
+ */
375
380
  declare function resolveSignInMethods(config: PublicConfig | null,
376
381
  /** The hostname the form is rendering on; omit when it is not knowable. */
377
382
  pageHost?: string): SignInPlan;
@@ -581,7 +586,7 @@ type PasskeyButtonProps = {
581
586
  * click. Complements the inline conditional-mediation autofill armed by
582
587
  * <SignIn/> - the button always works even where autofill is unavailable.
583
588
  */
584
- declare function PasskeyButton({ redirectTo, onSignedIn }: PasskeyButtonProps): React.JSX.Element;
589
+ declare function PasskeyButton({ redirectTo, onSignedIn }: PasskeyButtonProps): React.JSX.Element | null;
585
590
 
586
591
  type ForgotPasswordProps = {
587
592
  /**
package/dist/index.js CHANGED
@@ -60,26 +60,32 @@ var KNOWN_METHODS = [
60
60
  function emailAutocomplete(isPasskeyHost) {
61
61
  return isPasskeyHost ? "email webauthn" : "email";
62
62
  }
63
- function passkeyReachableFrom(authBaseUrl, pageHost, relyingPartyId) {
64
- if (pageHost === void 0) return true;
65
- let rpId = relyingPartyId ?? void 0;
66
- if (!rpId) {
67
- if (!authBaseUrl) return true;
68
- try {
69
- rpId = new URL(authBaseUrl).hostname;
70
- } catch {
71
- return true;
72
- }
63
+ function hostCoveredByRelyingParty(pageHost, rpId) {
64
+ const host = pageHost.toLowerCase();
65
+ const id = rpId.toLowerCase();
66
+ if (id !== "localhost" && !id.includes(".")) return false;
67
+ return host === id || host.endsWith(`.${id}`);
68
+ }
69
+ function resolveRelyingPartyId(authBaseUrl, relyingPartyId) {
70
+ if (relyingPartyId) return relyingPartyId;
71
+ if (!authBaseUrl) return void 0;
72
+ try {
73
+ return new URL(authBaseUrl).hostname || void 0;
74
+ } catch {
75
+ return void 0;
73
76
  }
74
- if (rpId !== "localhost" && !rpId.includes(".")) return false;
75
- return pageHost === rpId || pageHost.endsWith(`.${rpId}`);
76
77
  }
77
- function passkeyReachableForConfig(config, pageHost) {
78
- return passkeyReachableFrom(
78
+ function passkeyBlockingDomain(config, pageHost) {
79
+ if (pageHost === void 0) return void 0;
80
+ const rpId = resolveRelyingPartyId(
79
81
  config?.authBaseUrl,
80
- pageHost,
81
82
  config?.authentication?.passkey?.relyingPartyId
82
83
  );
84
+ if (rpId === void 0) return void 0;
85
+ return hostCoveredByRelyingParty(pageHost, rpId) ? void 0 : rpId;
86
+ }
87
+ function passkeyReachableForConfig(config, pageHost) {
88
+ return passkeyBlockingDomain(config, pageHost) === void 0;
83
89
  }
84
90
  function resolveSignInMethods(config, pageHost) {
85
91
  const methods = config?.enabledMethods ?? ["password"];
@@ -122,6 +128,11 @@ function resolveSignInMethods(config, pageHost) {
122
128
  // src/components/SignIn.tsx
123
129
  import * as React13 from "react";
124
130
 
131
+ // src/components/page-host.ts
132
+ function currentPageHost() {
133
+ return typeof window === "undefined" ? void 0 : window.location.hostname;
134
+ }
135
+
125
136
  // src/components/redirect.ts
126
137
  function isSafeRedirect(redirectTo, origin) {
127
138
  try {
@@ -342,6 +353,10 @@ function PasskeyButton({ redirectTo, onSignedIn }) {
342
353
  const { sessionStore } = useAuthClient();
343
354
  const { signInPasskey } = useSignIn();
344
355
  const { pending, error, run } = useSubmitAction();
356
+ const { config, isLoading: configLoading, isError: configError } = usePublicConfig();
357
+ const configPending = configLoading || config === null && !configError;
358
+ const blocked = passkeyBlockingDomain(config, currentPageHost()) !== void 0;
359
+ if (configPending || blocked) return null;
345
360
  return /* @__PURE__ */ jsxs3("div", { className: "ba-fields", children: [
346
361
  /* @__PURE__ */ jsx3(
347
362
  "button",
@@ -1452,10 +1467,7 @@ function SignIn({
1452
1467
  recordSignInMethod("phone-otp");
1453
1468
  onSignedIn?.();
1454
1469
  }, [onSignedIn, recordSignInMethod]);
1455
- const plan = resolveSignInMethods(
1456
- config,
1457
- typeof window === "undefined" ? void 0 : window.location.hostname
1458
- );
1470
+ const plan = resolveSignInMethods(config, currentPageHost());
1459
1471
  usePasskeyAutofill({
1460
1472
  enabled: credentialMode === "email" && plan.autofillHost !== null,
1461
1473
  redirectTo,
@@ -2912,6 +2924,9 @@ function PasskeyManagerBody({
2912
2924
  const api = usePasskeys();
2913
2925
  const apiRef = React24.useRef(api);
2914
2926
  apiRef.current = api;
2927
+ const { config, isLoading: configLoading, isError: configError } = usePublicConfig();
2928
+ const blockedBy = passkeyBlockingDomain(config, currentPageHost());
2929
+ const configPending = configLoading || config === null && !configError;
2915
2930
  const { pending, error, setError, run } = useSubmitAction();
2916
2931
  const [passkeys, setPasskeys] = React24.useState(null);
2917
2932
  const loadTokenRef = React24.useRef(0);
@@ -2984,7 +2999,12 @@ function PasskeyManagerBody({
2984
2999
  /* @__PURE__ */ jsx33(FormError, { children: error }),
2985
3000
  passkeys === null && /* @__PURE__ */ jsx33("button", { className: "ba-link-button", type: "button", onClick: () => void load(), children: t("userProfile.retry") })
2986
3001
  ] }),
2987
- passkeys !== null && allowAdd && /* @__PURE__ */ jsx33(
3002
+ passkeys !== null && allowAdd && !configPending && (blockedBy !== void 0 ? (
3003
+ // The add button is HIDDEN, not disabled, and the list stays: a user
3004
+ // may hold passkeys enrolled on the relying-party origin and must
3005
+ // still be able to rename or remove them from here.
3006
+ /* @__PURE__ */ jsx33("p", { className: "ba-muted", "data-testid": "passkey-add-elsewhere", children: t("passkeys.addElsewhere", { domain: blockedBy }) })
3007
+ ) : /* @__PURE__ */ jsx33(
2988
3008
  "button",
2989
3009
  {
2990
3010
  className: "ba-button",
@@ -2997,7 +3017,7 @@ function PasskeyManagerBody({
2997
3017
  }),
2998
3018
  children: /* @__PURE__ */ jsx33(Busy, { busy: pending, label: t("common.working"), children: t("passkeys.add") })
2999
3019
  }
3000
- )
3020
+ ))
3001
3021
  ] });
3002
3022
  }
3003
3023
 
@@ -3019,7 +3039,7 @@ function usePasskeyOffer() {
3019
3039
  const api = React25.useRef(listPasskeys);
3020
3040
  api.current = listPasskeys;
3021
3041
  const projectId = config?.environmentId ?? null;
3022
- const eligible = projectId !== null && isSignedIn && resolveProjectCapabilities(config).passkeyAdd && typeof window !== "undefined" && "PublicKeyCredential" in window && passkeyReachableForConfig(config, window.location.hostname);
3042
+ const eligible = projectId !== null && isSignedIn && resolveProjectCapabilities(config).passkeyAdd && typeof window !== "undefined" && "PublicKeyCredential" in window && passkeyReachableForConfig(config, currentPageHost());
3023
3043
  const userId = user?.id ?? null;
3024
3044
  const sessionId = sessionData?.session?.id ?? null;
3025
3045
  const subject = React25.useMemo(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authowl/react",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
4
4
  "description": "React provider, hooks, and drop-in components for AuthOwl, the multi-tenant auth SaaS.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -47,7 +47,7 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "dependencies": {
50
- "@authowl/core": "0.24.1"
50
+ "@authowl/core": "0.24.2"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=18",
@@ -56,7 +56,7 @@
56
56
  "devDependencies": {
57
57
  "@testing-library/react": "16.3.2",
58
58
  "@types/react": "^19.2.18",
59
- "@types/react-dom": "^19.2.4",
59
+ "@types/react-dom": "^19.2.5",
60
60
  "jsdom": "26.1.0",
61
61
  "qrcode-generator": "^2.0.4",
62
62
  "react": "^19.2.8",