cauthy-auth 0.11.0 → 0.13.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/react.mjs CHANGED
@@ -7,6 +7,15 @@ import { createContext, useCallback, useContext, useEffect as useEffect2, useMem
7
7
  function canUsePasskeys() {
8
8
  return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
9
9
  }
10
+ async function canAutofillPasskeys() {
11
+ if (!canUsePasskeys()) return false;
12
+ const pkc = window.PublicKeyCredential;
13
+ try {
14
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
10
19
  function bytes(text2) {
11
20
  const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
12
21
  const bin = atob(b64);
@@ -22,9 +31,9 @@ function text(buf) {
22
31
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
23
32
  }
24
33
  var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
25
- async function getPasskey(options) {
34
+ async function getPasskey(options, how = {}) {
26
35
  const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
27
- const cred = await navigator.credentials.get({ publicKey });
36
+ const cred = await navigator.credentials.get({ publicKey, ...how });
28
37
  if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
29
38
  const r = cred.response;
30
39
  return {
@@ -168,6 +177,24 @@ function firstProblem(...checks) {
168
177
  }
169
178
 
170
179
  // src/client.ts
180
+ var googleScript = null;
181
+ function loadGoogleIdentity() {
182
+ const ready = () => window.google?.accounts?.id;
183
+ if (ready()) return Promise.resolve(ready());
184
+ googleScript ??= new Promise((resolve, reject) => {
185
+ const script = document.createElement("script");
186
+ script.src = "https://accounts.google.com/gsi/client";
187
+ script.async = true;
188
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
189
+ script.onerror = () => {
190
+ googleScript = null;
191
+ reject(new Error("google identity services did not load"));
192
+ };
193
+ document.head.appendChild(script);
194
+ });
195
+ return googleScript;
196
+ }
197
+ var PASSKEY_SECONDS = 300;
171
198
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
172
199
  function createAuthClient(options = {}) {
173
200
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -176,13 +203,13 @@ function createAuthClient(options = {}) {
176
203
  const said = typeof options.locale === "function" ? options.locale() : options.locale;
177
204
  return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
178
205
  };
179
- async function post(path, form, bad) {
206
+ async function post(path, form, bad, captchaToken) {
180
207
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
181
208
  try {
182
209
  const res = await doFetch(`${base}${path}`, {
183
210
  method: "POST",
184
211
  credentials: "same-origin",
185
- headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
212
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {}, ...captchaToken ? { "x-cauthy-captcha": captchaToken } : {} },
186
213
  body: JSON.stringify(form)
187
214
  });
188
215
  const body = await res.json().catch(() => null);
@@ -234,9 +261,11 @@ function createAuthClient(options = {}) {
234
261
  }
235
262
  return {
236
263
  signIn: {
237
- email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
264
+ email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password)), f.captchaToken),
238
265
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
239
- magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email)),
266
+ mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
267
+ mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
268
+ magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
240
269
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
241
270
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
242
271
  passkeyUrl,
@@ -251,15 +280,75 @@ function createAuthClient(options = {}) {
251
280
  window.location.assign(passkeyUrl(o));
252
281
  return { ok: true, status: "redirected" };
253
282
  },
283
+ async passkeyAutofill(o = {}) {
284
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
285
+ const m = await methods();
286
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
287
+ for (; ; ) {
288
+ if (o.signal?.aborted) return cancelled;
289
+ const begun = await post("/sign-in/passkey/begin", {}, null);
290
+ if (!begun.ok) return begun;
291
+ const renew = new AbortController();
292
+ const stop = () => renew.abort();
293
+ o.signal?.addEventListener("abort", stop, { once: true });
294
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
295
+ let credential;
296
+ try {
297
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
298
+ } catch (e) {
299
+ if (o.signal?.aborted) return cancelled;
300
+ if (renew.signal.aborted) continue;
301
+ return { ok: false, error: ceremonyError(e) };
302
+ } finally {
303
+ clearTimeout(timer);
304
+ o.signal?.removeEventListener("abort", stop);
305
+ }
306
+ return post("/sign-in/passkey/finish", { credential }, null);
307
+ }
308
+ },
309
+ async googleOneTap(o = {}) {
310
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
311
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
312
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
313
+ if (!begun.ok) return begun;
314
+ let gis;
315
+ try {
316
+ gis = await loadGoogleIdentity();
317
+ } catch {
318
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
319
+ }
320
+ const credential = await new Promise((done) => {
321
+ o.signal?.addEventListener("abort", () => {
322
+ gis.cancel();
323
+ done(null);
324
+ }, { once: true });
325
+ gis.initialize({
326
+ client_id: begun.clientId,
327
+ nonce: begun.nonce,
328
+ context: o.context ?? "signin",
329
+ callback: (r) => done(r.credential ?? null),
330
+ auto_select: false,
331
+ cancel_on_tap_outside: true,
332
+ itp_support: true,
333
+ use_fedcm_for_prompt: true
334
+ });
335
+ gis.prompt((moment) => {
336
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
337
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
338
+ });
339
+ });
340
+ if (credential === null) return cancelled;
341
+ return post("/sign-in/google-one-tap", { credential }, null);
342
+ },
254
343
  socialUrl,
255
344
  social(provider, o) {
256
345
  window.location.assign(socialUrl(provider, o));
257
346
  }
258
347
  },
259
348
  signUp: {
260
- email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)))
349
+ email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)), f.captchaToken)
261
350
  },
262
- forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
351
+ forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
263
352
  resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
264
353
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
265
354
  signOut: () => post("/sign-out", {}, null),
@@ -277,7 +366,14 @@ function createAuthClient(options = {}) {
277
366
  setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
278
367
  verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
279
368
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
280
- recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
369
+ recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null),
370
+ async factors() {
371
+ const body = await read("/me/mfa/factors");
372
+ return body && typeof body.totp === "boolean" ? body : null;
373
+ },
374
+ addPhone: (f) => post("/me/mfa/phone", { phone: f.phone, channel: f.channel, password: f.password }, null),
375
+ verifyPhone: (f) => post("/me/mfa/phone/verify", { code: f.code }, checkCode(f.code)),
376
+ removePhone: (f) => post("/me/mfa/phone/remove", { password: f.password }, null)
281
377
  },
282
378
  changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
283
379
  async identities() {
@@ -395,6 +491,18 @@ var TEXTS = {
395
491
  linkSent: "If that address has an account, a link to choose a new password is on its way.",
396
492
  codeHelp: "Enter the code from your authenticator app, or a recovery code.",
397
493
  verify: "Verify",
494
+ codeHelpPhone: "We send a code to your phone. Ask for it, then type it here, or use a recovery code.",
495
+ sendCodeSms: "Send a code by SMS to {to}",
496
+ sendCodeWhatsapp: "Send a code by WhatsApp to {to}",
497
+ codeSent: "We sent a code to {to}. Type it here.",
498
+ phone: "Phone, with its country code",
499
+ phoneHelp: "Get your codes by SMS or WhatsApp instead of an app.",
500
+ phoneOn: "Codes go to {to}.",
501
+ phoneOff: "No phone yet.",
502
+ addPhone: "Add phone",
503
+ removePhone: "Remove phone",
504
+ bySms: "SMS",
505
+ byWhatsapp: "WhatsApp",
398
506
  checkEmail: "Almost there: open the link we sent to confirm your address, then sign in.",
399
507
  backToSignIn: "Back to sign in",
400
508
  emailMeALink: "Email me a sign-in link",
@@ -462,6 +570,13 @@ var TEXTS = {
462
570
  step_up_required: "Enter your current password.",
463
571
  weak_password: "Choose a stronger password.",
464
572
  unauthenticated: "Sign in first.",
573
+ phone_refused: "The code could not be sent to that number.",
574
+ too_many_codes: "Enough codes were sent. Use the last one, or sign in again.",
575
+ phone_unavailable: "Codes cannot be sent that way here.",
576
+ phone_already_set: "Remove the phone you have first.",
577
+ phone_codes_unavailable: "Codes cannot be sent right now. Try again in a moment.",
578
+ captcha_required: "Complete the check that you are not a bot, then try again.",
579
+ captcha_failed: "The check that you are not a bot did not pass. Try it again.",
465
580
  passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
466
581
  passkey_exists: "This device already has a passkey for your account.",
467
582
  passkey_unavailable: "Passkeys cannot be used here."
@@ -484,6 +599,18 @@ var TEXTS = {
484
599
  linkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para elegir una contrase\xF1a nueva.",
485
600
  codeHelp: "Escribe el c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n, o un c\xF3digo de recuperaci\xF3n.",
486
601
  verify: "Verificar",
602
+ codeHelpPhone: "Te enviamos un c\xF3digo a tu tel\xE9fono. P\xEDdelo y escr\xEDbelo aqu\xED, o usa un c\xF3digo de recuperaci\xF3n.",
603
+ sendCodeSms: "Enviar un c\xF3digo por SMS a {to}",
604
+ sendCodeWhatsapp: "Enviar un c\xF3digo por WhatsApp a {to}",
605
+ codeSent: "Te enviamos un c\xF3digo a {to}. Escr\xEDbelo aqu\xED.",
606
+ phone: "Tel\xE9fono, con su c\xF3digo de pa\xEDs",
607
+ phoneHelp: "Recibe tus c\xF3digos por SMS o WhatsApp en vez de una app.",
608
+ phoneOn: "Los c\xF3digos van a {to}.",
609
+ phoneOff: "Todav\xEDa no tienes un tel\xE9fono.",
610
+ addPhone: "Agregar tel\xE9fono",
611
+ removePhone: "Quitar tel\xE9fono",
612
+ bySms: "SMS",
613
+ byWhatsapp: "WhatsApp",
487
614
  checkEmail: "Ya casi: abre el enlace que te enviamos para confirmar tu correo y luego entra.",
488
615
  backToSignIn: "Volver a entrar",
489
616
  emailMeALink: "Env\xEDame un enlace para entrar",
@@ -551,6 +678,13 @@ var TEXTS = {
551
678
  step_up_required: "Escribe tu contrase\xF1a actual.",
552
679
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
553
680
  unauthenticated: "Inicia sesi\xF3n primero.",
681
+ phone_refused: "No se pudo enviar el c\xF3digo a ese n\xFAmero.",
682
+ too_many_codes: "Ya se enviaron suficientes c\xF3digos. Usa el \xFAltimo o vuelve a entrar.",
683
+ phone_unavailable: "Aqu\xED no se pueden enviar c\xF3digos de esa forma.",
684
+ phone_already_set: "Primero quita el tel\xE9fono que tienes.",
685
+ phone_codes_unavailable: "Ahora no se pueden enviar c\xF3digos. Int\xE9ntalo en un momento.",
686
+ captcha_required: "Completa la verificaci\xF3n de que no eres un robot y vuelve a intentarlo.",
687
+ captcha_failed: "La verificaci\xF3n de que no eres un robot no pas\xF3. Int\xE9ntalo de nuevo.",
554
688
  passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
555
689
  passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
556
690
  passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
@@ -595,6 +729,63 @@ function useRoot(props) {
595
729
  if (props.unstyled) return {};
596
730
  return { "data-cauthy": "", style: vars };
597
731
  }
732
+ var turnstileScript = null;
733
+ function loadTurnstile() {
734
+ const ready = () => window.turnstile;
735
+ if (ready()) return Promise.resolve(ready());
736
+ turnstileScript ??= new Promise((resolve, reject) => {
737
+ const script = document.createElement("script");
738
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
739
+ script.async = true;
740
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("turnstile did not load"));
741
+ script.onerror = () => {
742
+ turnstileScript = null;
743
+ reject(new Error("turnstile did not load"));
744
+ };
745
+ document.head.appendChild(script);
746
+ });
747
+ return turnstileScript;
748
+ }
749
+ function useChallenge(siteKey, locale) {
750
+ const box = useRef(null);
751
+ const widget = useRef(null);
752
+ const [token, setToken] = useState("");
753
+ useEffect(() => {
754
+ if (!siteKey || !box.current) return;
755
+ let gone = false;
756
+ void loadTurnstile().then((api) => {
757
+ if (gone || !box.current) return;
758
+ const id = api.render(box.current, {
759
+ sitekey: siteKey,
760
+ language: locale ?? "auto",
761
+ size: "flexible",
762
+ callback: (t) => setToken(t),
763
+ "expired-callback": () => setToken(""),
764
+ "error-callback": () => setToken("")
765
+ });
766
+ widget.current = { api, id };
767
+ }).catch(() => {
768
+ });
769
+ return () => {
770
+ gone = true;
771
+ if (widget.current) widget.current.api.remove(widget.current.id);
772
+ widget.current = null;
773
+ };
774
+ }, [siteKey, locale]);
775
+ const renew = () => {
776
+ setToken("");
777
+ if (widget.current) widget.current.api.reset(widget.current.id);
778
+ };
779
+ const node = siteKey ? /* @__PURE__ */ jsx("div", { ref: box, className: "cauthy-challenge" }) : null;
780
+ return { node, token: token || void 0, renew };
781
+ }
782
+ function useMethods(auth) {
783
+ const [methods, setMethods] = useState(null);
784
+ useEffect(() => {
785
+ void auth.methods().then(setMethods);
786
+ }, []);
787
+ return methods;
788
+ }
598
789
  function useForm(props) {
599
790
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
600
791
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
@@ -667,6 +858,25 @@ function SignIn(props) {
667
858
  const [password, setPassword] = useState("");
668
859
  const [code, setCode] = useState("");
669
860
  const [step, setStep] = useState("password");
861
+ const challenge = useChallenge(methods?.captchaSiteKey, props.locale);
862
+ const [phoneWay, setPhoneWay] = useState(null);
863
+ const [phoneSent, setPhoneSent] = useState("");
864
+ useEffect(() => {
865
+ if (step !== "code") return;
866
+ void auth.signIn.mfaMethods().then((r) => {
867
+ if (r.ok) setPhoneWay({ totp: r.totp, phone: r.phone });
868
+ });
869
+ }, [step]);
870
+ const sendCode = async () => {
871
+ f.setBusy(true);
872
+ f.setError(null);
873
+ const r = await auth.signIn.mfaSendCode();
874
+ f.setBusy(false);
875
+ if (!r.ok) return f.setError(r.error);
876
+ setPhoneSent(r.sentTo);
877
+ };
878
+ const autofill = useRef(null);
879
+ const [offer, setOffer] = useState(0);
670
880
  useEffect(() => {
671
881
  void auth.methods().then(setMethods);
672
882
  const back = returnedError();
@@ -681,30 +891,52 @@ function SignIn(props) {
681
891
  setStep("password");
682
892
  });
683
893
  });
894
+ useEffect(() => {
895
+ if (step !== "password" || !methods?.passkeyHere) return;
896
+ const ctl = new AbortController();
897
+ autofill.current = ctl;
898
+ void auth.signIn.passkeyAutofill({ signal: ctl.signal }).then((r) => {
899
+ if (ctl.signal.aborted) return;
900
+ if (r.ok) return f.done();
901
+ if (r.error.code === "cancelled") return;
902
+ f.setError(r.error);
903
+ if (r.error.code === "passkey_failed" || r.error.code === "step_expired") setOffer((n) => n + 1);
904
+ });
905
+ return () => ctl.abort();
906
+ }, [step, methods, offer]);
684
907
  const submit = async (e) => {
685
908
  e.preventDefault();
909
+ autofill.current?.abort();
686
910
  f.setBusy(true);
687
911
  f.setError(null);
688
- const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
912
+ const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password, captchaToken: challenge.token });
689
913
  f.setBusy(false);
690
- if (!r.ok) return f.setError(r.error);
914
+ if (step === "password") challenge.renew();
915
+ if (!r.ok) {
916
+ setOffer((n) => n + 1);
917
+ return f.setError(r.error);
918
+ }
691
919
  if (r.status === "mfa_required") return setStep("code");
692
920
  f.done();
693
921
  };
694
922
  const usePasskey = async () => {
923
+ autofill.current?.abort();
695
924
  f.setBusy(true);
696
925
  f.setError(null);
697
926
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
698
927
  if (r.ok && r.status === "redirected") return;
699
928
  f.setBusy(false);
929
+ if (!r.ok) setOffer((n) => n + 1);
700
930
  if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
701
931
  f.done();
702
932
  };
703
933
  const sendLink = async () => {
934
+ autofill.current?.abort();
704
935
  f.setBusy(true);
705
936
  f.setError(null);
706
- const r = await auth.signIn.magicLink({ email });
937
+ const r = await auth.signIn.magicLink({ email, captchaToken: challenge.token });
707
938
  f.setBusy(false);
939
+ challenge.renew();
708
940
  if (!r.ok) return f.setError(r.error);
709
941
  setStep("sent");
710
942
  };
@@ -720,9 +952,11 @@ function SignIn(props) {
720
952
  ] });
721
953
  }
722
954
  if (step === "code") {
955
+ const to = phoneWay?.phone?.number ?? "";
723
956
  return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
724
- /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.codeHelp }),
957
+ /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: phoneSent ? f.t.codeSent.replace("{to}", phoneSent) : phoneWay?.phone && !phoneWay.totp ? f.t.codeHelpPhone : f.t.codeHelp }),
725
958
  general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
959
+ phoneWay?.phone && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void sendCode(), children: (phoneWay.phone.channel === "whatsapp" ? f.t.sendCodeWhatsapp : f.t.sendCodeSms).replace("{to}", to) }),
726
960
  /* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
727
961
  /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
728
962
  ] });
@@ -736,7 +970,7 @@ function SignIn(props) {
736
970
  ] }, p.id)),
737
971
  methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
738
972
  ((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ jsx("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
739
- /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
973
+ /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: methods?.passkeyHere ? "username webauthn" : "username", required: true, onChange: setEmail }),
740
974
  /* @__PURE__ */ jsx(
741
975
  Field,
742
976
  {
@@ -751,6 +985,7 @@ function SignIn(props) {
751
985
  aside: props.links?.forgotPassword ? /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.forgotPassword, children: f.t.forgotPassword }) : void 0
752
986
  }
753
987
  ),
988
+ challenge.node,
754
989
  /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signIn }),
755
990
  methods?.magicLink && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void sendLink(), children: f.t.emailMeALink }),
756
991
  methods?.signUp !== false && props.links?.signUp && /* @__PURE__ */ jsxs("p", { className: f.cx("footer"), children: [
@@ -760,6 +995,21 @@ function SignIn(props) {
760
995
  ] })
761
996
  ] });
762
997
  }
998
+ function GoogleOneTap(props) {
999
+ const auth = useAuth();
1000
+ const unauthenticated = auth.status === "unauthenticated";
1001
+ useEffect(() => {
1002
+ if (!unauthenticated) return;
1003
+ const ctl = new AbortController();
1004
+ void auth.signIn.googleOneTap({ signal: ctl.signal, context: props.context }).then((r) => {
1005
+ if (!r.ok || ctl.signal.aborted) return;
1006
+ if (props.onSuccess) props.onSuccess();
1007
+ else if (props.returnTo) window.location.assign(props.returnTo);
1008
+ });
1009
+ return () => ctl.abort();
1010
+ }, [unauthenticated]);
1011
+ return null;
1012
+ }
763
1013
  function SignUp(props) {
764
1014
  const auth = useAuth();
765
1015
  const f = useForm(props);
@@ -767,12 +1017,14 @@ function SignUp(props) {
767
1017
  const [email, setEmail] = useState("");
768
1018
  const [password, setPassword] = useState("");
769
1019
  const [sent, setSent] = useState(false);
1020
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
770
1021
  const submit = async (e) => {
771
1022
  e.preventDefault();
772
1023
  f.setBusy(true);
773
1024
  f.setError(null);
774
- const r = await auth.signUp.email({ name, email, password });
1025
+ const r = await auth.signUp.email({ name, email, password, captchaToken: challenge.token });
775
1026
  f.setBusy(false);
1027
+ challenge.renew();
776
1028
  if (!r.ok) return f.setError(r.error);
777
1029
  if (r.status === "verification_required") return setSent(true);
778
1030
  f.done();
@@ -789,6 +1041,7 @@ function SignUp(props) {
789
1041
  /* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
790
1042
  /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
791
1043
  /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.password, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
1044
+ challenge.node,
792
1045
  /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signUp }),
793
1046
  props.links?.signIn && /* @__PURE__ */ jsxs("p", { className: f.cx("footer"), children: [
794
1047
  f.t.haveAccount,
@@ -802,12 +1055,14 @@ function ForgotPassword(props) {
802
1055
  const f = useForm(props);
803
1056
  const [email, setEmail] = useState("");
804
1057
  const [sent, setSent] = useState(false);
1058
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
805
1059
  const submit = async (e) => {
806
1060
  e.preventDefault();
807
1061
  f.setBusy(true);
808
1062
  f.setError(null);
809
- const r = await auth.forgotPassword({ email });
1063
+ const r = await auth.forgotPassword({ email, captchaToken: challenge.token });
810
1064
  f.setBusy(false);
1065
+ challenge.renew();
811
1066
  if (!r.ok) return f.setError(r.error);
812
1067
  setSent(true);
813
1068
  };
@@ -821,6 +1076,7 @@ function ForgotPassword(props) {
821
1076
  return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
822
1077
  general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
823
1078
  /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
1079
+ challenge.node,
824
1080
  /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.sendLink }),
825
1081
  props.links?.signIn && /* @__PURE__ */ jsx("p", { className: f.cx("footer"), children: /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn }) })
826
1082
  ] });
@@ -920,6 +1176,7 @@ function Account(props) {
920
1176
  account.hasPassword && /* @__PURE__ */ jsx(PasswordPart, { props }),
921
1177
  /* @__PURE__ */ jsx(LinkedPart, { props }),
922
1178
  /* @__PURE__ */ jsx(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
1179
+ /* @__PURE__ */ jsx(PhonePart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
923
1180
  /* @__PURE__ */ jsx(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
924
1181
  /* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
925
1182
  /* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.sessions }),
@@ -1149,6 +1406,77 @@ function SecondStepPart({ props, account, onChanged }) {
1149
1406
  account.mfaEnabled && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void renew(), children: f.t.newRecoveryCodes })
1150
1407
  ] });
1151
1408
  }
1409
+ function PhonePart({ props, account, onChanged }) {
1410
+ const auth = useAuth();
1411
+ const { f, run, general } = useSection(props);
1412
+ const [ways, setWays] = useState(null);
1413
+ const [number, setNumber] = useState("");
1414
+ const [channel, setChannel] = useState("sms");
1415
+ const [password, setPassword] = useState("");
1416
+ const [code, setCode] = useState("");
1417
+ const [sentTo, setSentTo] = useState("");
1418
+ const [codes, setCodes] = useState(null);
1419
+ const load = () => auth.account.mfa.factors().then((w) => {
1420
+ setWays(w);
1421
+ if (w && w.phoneChannels.length > 0 && !w.phoneChannels.includes(channel)) setChannel(w.phoneChannels[0]);
1422
+ });
1423
+ useEffect(() => {
1424
+ void load();
1425
+ }, []);
1426
+ if (!ways || ways.phoneChannels.length === 0) return null;
1427
+ const add = async (e) => {
1428
+ e.preventDefault();
1429
+ const r = await run(() => auth.account.mfa.addPhone({ phone: number, channel, password }));
1430
+ if (r.ok) setSentTo(r.sentTo);
1431
+ };
1432
+ const confirm = async (e) => {
1433
+ e.preventDefault();
1434
+ const r = await run(() => auth.account.mfa.verifyPhone({ code }));
1435
+ if (!r.ok) return;
1436
+ setSentTo("");
1437
+ setCode("");
1438
+ setPassword("");
1439
+ if (r.recoveryCodes.length > 0) setCodes(r.recoveryCodes);
1440
+ onChanged(true);
1441
+ await load();
1442
+ };
1443
+ const remove = async (e) => {
1444
+ e.preventDefault();
1445
+ const r = await run(() => auth.account.mfa.removePhone({ password }));
1446
+ if (!r.ok) return;
1447
+ setPassword("");
1448
+ await load();
1449
+ const left = await auth.account.mfa.factors();
1450
+ onChanged(Boolean(left?.totp));
1451
+ };
1452
+ const passwordField = account.hasPassword && /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword });
1453
+ if (sentTo) {
1454
+ return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: confirm, noValidate: true, children: [
1455
+ /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.phone }),
1456
+ /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.codeSent.replace("{to}", sentTo) }),
1457
+ general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1458
+ /* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
1459
+ /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
1460
+ ] });
1461
+ }
1462
+ return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: ways.phone ? remove : add, noValidate: true, children: [
1463
+ /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.phone }),
1464
+ /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: ways.phone ? f.t.phoneOn.replace("{to}", ways.phone.number) : `${f.t.phoneOff} ${f.t.phoneHelp}` }),
1465
+ codes && /* @__PURE__ */ jsxs(Fragment, { children: [
1466
+ /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.keepCodes }),
1467
+ /* @__PURE__ */ jsx("ul", { className: f.cx("codes"), children: codes.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { children: c }) }, c)) })
1468
+ ] }),
1469
+ general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1470
+ !ways.phone && /* @__PURE__ */ jsx(Field, { f, name: "phone", type: "tel", label: f.t.phone, value: number, autoComplete: "tel", required: true, onChange: setNumber }),
1471
+ !ways.phone && ways.phoneChannels.length > 1 && /* @__PURE__ */ jsx("div", { className: f.cx("field"), role: "radiogroup", children: ways.phoneChannels.map((c) => /* @__PURE__ */ jsxs("label", { className: f.cx("label"), children: [
1472
+ /* @__PURE__ */ jsx("input", { type: "radio", name: "channel", value: c, checked: channel === c, onChange: () => setChannel(c) }),
1473
+ " ",
1474
+ c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
1475
+ ] }, c)) }),
1476
+ passwordField,
1477
+ /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: ways.phone ? f.t.removePhone : f.t.addPhone })
1478
+ ] });
1479
+ }
1152
1480
  function SignOutEverywhere({ props }) {
1153
1481
  const auth = useAuth();
1154
1482
  const { f, run, general } = useSection(props);
@@ -1251,7 +1579,16 @@ function useAuth() {
1251
1579
  };
1252
1580
  return {
1253
1581
  ...client,
1254
- signIn: { ...client.signIn, email: (f) => then(client.signIn.email(f)), mfa: (f) => then(client.signIn.mfa(f)), magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)), emailCode: (f) => then(client.signIn.emailCode(f)) },
1582
+ signIn: {
1583
+ ...client.signIn,
1584
+ email: (f) => then(client.signIn.email(f)),
1585
+ mfa: (f) => then(client.signIn.mfa(f)),
1586
+ magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
1587
+ emailCode: (f) => then(client.signIn.emailCode(f)),
1588
+ passkey: (o) => then(client.signIn.passkey(o)),
1589
+ passkeyAutofill: (o) => then(client.signIn.passkeyAutofill(o)),
1590
+ googleOneTap: (o) => then(client.signIn.googleOneTap(o))
1591
+ },
1255
1592
  signUp: { email: (f) => then(client.signUp.email(f)) },
1256
1593
  signOut: () => then(client.signOut()),
1257
1594
  account: {
@@ -1271,11 +1608,13 @@ export {
1271
1608
  Account,
1272
1609
  CauthyProvider,
1273
1610
  ForgotPassword,
1611
+ GoogleOneTap,
1274
1612
  ResetPassword,
1275
1613
  SignIn,
1276
1614
  SignUp,
1277
1615
  VerifyEmail,
1278
1616
  can,
1617
+ canAutofillPasskeys,
1279
1618
  canUsePasskeys,
1280
1619
  createAuthClient,
1281
1620
  definePolicy,