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.js CHANGED
@@ -24,11 +24,13 @@ __export(react_exports, {
24
24
  Account: () => Account,
25
25
  CauthyProvider: () => CauthyProvider,
26
26
  ForgotPassword: () => ForgotPassword,
27
+ GoogleOneTap: () => GoogleOneTap,
27
28
  ResetPassword: () => ResetPassword,
28
29
  SignIn: () => SignIn,
29
30
  SignUp: () => SignUp,
30
31
  VerifyEmail: () => VerifyEmail,
31
32
  can: () => can,
33
+ canAutofillPasskeys: () => canAutofillPasskeys,
32
34
  canUsePasskeys: () => canUsePasskeys,
33
35
  createAuthClient: () => createAuthClient,
34
36
  definePolicy: () => definePolicy,
@@ -42,6 +44,15 @@ var import_react3 = require("react");
42
44
  function canUsePasskeys() {
43
45
  return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
44
46
  }
47
+ async function canAutofillPasskeys() {
48
+ if (!canUsePasskeys()) return false;
49
+ const pkc = window.PublicKeyCredential;
50
+ try {
51
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
45
56
  function bytes(text2) {
46
57
  const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
47
58
  const bin = atob(b64);
@@ -57,9 +68,9 @@ function text(buf) {
57
68
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
58
69
  }
59
70
  var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
60
- async function getPasskey(options) {
71
+ async function getPasskey(options, how = {}) {
61
72
  const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
62
- const cred = await navigator.credentials.get({ publicKey });
73
+ const cred = await navigator.credentials.get({ publicKey, ...how });
63
74
  if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
64
75
  const r = cred.response;
65
76
  return {
@@ -203,6 +214,24 @@ function firstProblem(...checks) {
203
214
  }
204
215
 
205
216
  // src/client.ts
217
+ var googleScript = null;
218
+ function loadGoogleIdentity() {
219
+ const ready = () => window.google?.accounts?.id;
220
+ if (ready()) return Promise.resolve(ready());
221
+ googleScript ??= new Promise((resolve, reject) => {
222
+ const script = document.createElement("script");
223
+ script.src = "https://accounts.google.com/gsi/client";
224
+ script.async = true;
225
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
226
+ script.onerror = () => {
227
+ googleScript = null;
228
+ reject(new Error("google identity services did not load"));
229
+ };
230
+ document.head.appendChild(script);
231
+ });
232
+ return googleScript;
233
+ }
234
+ var PASSKEY_SECONDS = 300;
206
235
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
207
236
  function createAuthClient(options = {}) {
208
237
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -211,13 +240,13 @@ function createAuthClient(options = {}) {
211
240
  const said = typeof options.locale === "function" ? options.locale() : options.locale;
212
241
  return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
213
242
  };
214
- async function post(path, form, bad) {
243
+ async function post(path, form, bad, captchaToken) {
215
244
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
216
245
  try {
217
246
  const res = await doFetch(`${base}${path}`, {
218
247
  method: "POST",
219
248
  credentials: "same-origin",
220
- headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
249
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {}, ...captchaToken ? { "x-cauthy-captcha": captchaToken } : {} },
221
250
  body: JSON.stringify(form)
222
251
  });
223
252
  const body = await res.json().catch(() => null);
@@ -269,9 +298,11 @@ function createAuthClient(options = {}) {
269
298
  }
270
299
  return {
271
300
  signIn: {
272
- email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
301
+ email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password)), f.captchaToken),
273
302
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
274
- magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email)),
303
+ mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
304
+ mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
305
+ magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
275
306
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
276
307
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
277
308
  passkeyUrl,
@@ -286,15 +317,75 @@ function createAuthClient(options = {}) {
286
317
  window.location.assign(passkeyUrl(o));
287
318
  return { ok: true, status: "redirected" };
288
319
  },
320
+ async passkeyAutofill(o = {}) {
321
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
322
+ const m = await methods();
323
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
324
+ for (; ; ) {
325
+ if (o.signal?.aborted) return cancelled;
326
+ const begun = await post("/sign-in/passkey/begin", {}, null);
327
+ if (!begun.ok) return begun;
328
+ const renew = new AbortController();
329
+ const stop = () => renew.abort();
330
+ o.signal?.addEventListener("abort", stop, { once: true });
331
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
332
+ let credential;
333
+ try {
334
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
335
+ } catch (e) {
336
+ if (o.signal?.aborted) return cancelled;
337
+ if (renew.signal.aborted) continue;
338
+ return { ok: false, error: ceremonyError(e) };
339
+ } finally {
340
+ clearTimeout(timer);
341
+ o.signal?.removeEventListener("abort", stop);
342
+ }
343
+ return post("/sign-in/passkey/finish", { credential }, null);
344
+ }
345
+ },
346
+ async googleOneTap(o = {}) {
347
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
348
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
349
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
350
+ if (!begun.ok) return begun;
351
+ let gis;
352
+ try {
353
+ gis = await loadGoogleIdentity();
354
+ } catch {
355
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
356
+ }
357
+ const credential = await new Promise((done) => {
358
+ o.signal?.addEventListener("abort", () => {
359
+ gis.cancel();
360
+ done(null);
361
+ }, { once: true });
362
+ gis.initialize({
363
+ client_id: begun.clientId,
364
+ nonce: begun.nonce,
365
+ context: o.context ?? "signin",
366
+ callback: (r) => done(r.credential ?? null),
367
+ auto_select: false,
368
+ cancel_on_tap_outside: true,
369
+ itp_support: true,
370
+ use_fedcm_for_prompt: true
371
+ });
372
+ gis.prompt((moment) => {
373
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
374
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
375
+ });
376
+ });
377
+ if (credential === null) return cancelled;
378
+ return post("/sign-in/google-one-tap", { credential }, null);
379
+ },
289
380
  socialUrl,
290
381
  social(provider, o) {
291
382
  window.location.assign(socialUrl(provider, o));
292
383
  }
293
384
  },
294
385
  signUp: {
295
- 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)))
386
+ 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)
296
387
  },
297
- forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
388
+ forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
298
389
  resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
299
390
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
300
391
  signOut: () => post("/sign-out", {}, null),
@@ -312,7 +403,14 @@ function createAuthClient(options = {}) {
312
403
  setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
313
404
  verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
314
405
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
315
- recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
406
+ recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null),
407
+ async factors() {
408
+ const body = await read("/me/mfa/factors");
409
+ return body && typeof body.totp === "boolean" ? body : null;
410
+ },
411
+ addPhone: (f) => post("/me/mfa/phone", { phone: f.phone, channel: f.channel, password: f.password }, null),
412
+ verifyPhone: (f) => post("/me/mfa/phone/verify", { code: f.code }, checkCode(f.code)),
413
+ removePhone: (f) => post("/me/mfa/phone/remove", { password: f.password }, null)
316
414
  },
317
415
  changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
318
416
  async identities() {
@@ -430,6 +528,18 @@ var TEXTS = {
430
528
  linkSent: "If that address has an account, a link to choose a new password is on its way.",
431
529
  codeHelp: "Enter the code from your authenticator app, or a recovery code.",
432
530
  verify: "Verify",
531
+ codeHelpPhone: "We send a code to your phone. Ask for it, then type it here, or use a recovery code.",
532
+ sendCodeSms: "Send a code by SMS to {to}",
533
+ sendCodeWhatsapp: "Send a code by WhatsApp to {to}",
534
+ codeSent: "We sent a code to {to}. Type it here.",
535
+ phone: "Phone, with its country code",
536
+ phoneHelp: "Get your codes by SMS or WhatsApp instead of an app.",
537
+ phoneOn: "Codes go to {to}.",
538
+ phoneOff: "No phone yet.",
539
+ addPhone: "Add phone",
540
+ removePhone: "Remove phone",
541
+ bySms: "SMS",
542
+ byWhatsapp: "WhatsApp",
433
543
  checkEmail: "Almost there: open the link we sent to confirm your address, then sign in.",
434
544
  backToSignIn: "Back to sign in",
435
545
  emailMeALink: "Email me a sign-in link",
@@ -497,6 +607,13 @@ var TEXTS = {
497
607
  step_up_required: "Enter your current password.",
498
608
  weak_password: "Choose a stronger password.",
499
609
  unauthenticated: "Sign in first.",
610
+ phone_refused: "The code could not be sent to that number.",
611
+ too_many_codes: "Enough codes were sent. Use the last one, or sign in again.",
612
+ phone_unavailable: "Codes cannot be sent that way here.",
613
+ phone_already_set: "Remove the phone you have first.",
614
+ phone_codes_unavailable: "Codes cannot be sent right now. Try again in a moment.",
615
+ captcha_required: "Complete the check that you are not a bot, then try again.",
616
+ captcha_failed: "The check that you are not a bot did not pass. Try it again.",
500
617
  passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
501
618
  passkey_exists: "This device already has a passkey for your account.",
502
619
  passkey_unavailable: "Passkeys cannot be used here."
@@ -519,6 +636,18 @@ var TEXTS = {
519
636
  linkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para elegir una contrase\xF1a nueva.",
520
637
  codeHelp: "Escribe el c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n, o un c\xF3digo de recuperaci\xF3n.",
521
638
  verify: "Verificar",
639
+ 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.",
640
+ sendCodeSms: "Enviar un c\xF3digo por SMS a {to}",
641
+ sendCodeWhatsapp: "Enviar un c\xF3digo por WhatsApp a {to}",
642
+ codeSent: "Te enviamos un c\xF3digo a {to}. Escr\xEDbelo aqu\xED.",
643
+ phone: "Tel\xE9fono, con su c\xF3digo de pa\xEDs",
644
+ phoneHelp: "Recibe tus c\xF3digos por SMS o WhatsApp en vez de una app.",
645
+ phoneOn: "Los c\xF3digos van a {to}.",
646
+ phoneOff: "Todav\xEDa no tienes un tel\xE9fono.",
647
+ addPhone: "Agregar tel\xE9fono",
648
+ removePhone: "Quitar tel\xE9fono",
649
+ bySms: "SMS",
650
+ byWhatsapp: "WhatsApp",
522
651
  checkEmail: "Ya casi: abre el enlace que te enviamos para confirmar tu correo y luego entra.",
523
652
  backToSignIn: "Volver a entrar",
524
653
  emailMeALink: "Env\xEDame un enlace para entrar",
@@ -586,6 +715,13 @@ var TEXTS = {
586
715
  step_up_required: "Escribe tu contrase\xF1a actual.",
587
716
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
588
717
  unauthenticated: "Inicia sesi\xF3n primero.",
718
+ phone_refused: "No se pudo enviar el c\xF3digo a ese n\xFAmero.",
719
+ too_many_codes: "Ya se enviaron suficientes c\xF3digos. Usa el \xFAltimo o vuelve a entrar.",
720
+ phone_unavailable: "Aqu\xED no se pueden enviar c\xF3digos de esa forma.",
721
+ phone_already_set: "Primero quita el tel\xE9fono que tienes.",
722
+ phone_codes_unavailable: "Ahora no se pueden enviar c\xF3digos. Int\xE9ntalo en un momento.",
723
+ captcha_required: "Completa la verificaci\xF3n de que no eres un robot y vuelve a intentarlo.",
724
+ captcha_failed: "La verificaci\xF3n de que no eres un robot no pas\xF3. Int\xE9ntalo de nuevo.",
589
725
  passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
590
726
  passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
591
727
  passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
@@ -630,6 +766,63 @@ function useRoot(props) {
630
766
  if (props.unstyled) return {};
631
767
  return { "data-cauthy": "", style: vars };
632
768
  }
769
+ var turnstileScript = null;
770
+ function loadTurnstile() {
771
+ const ready = () => window.turnstile;
772
+ if (ready()) return Promise.resolve(ready());
773
+ turnstileScript ??= new Promise((resolve, reject) => {
774
+ const script = document.createElement("script");
775
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
776
+ script.async = true;
777
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("turnstile did not load"));
778
+ script.onerror = () => {
779
+ turnstileScript = null;
780
+ reject(new Error("turnstile did not load"));
781
+ };
782
+ document.head.appendChild(script);
783
+ });
784
+ return turnstileScript;
785
+ }
786
+ function useChallenge(siteKey, locale) {
787
+ const box = (0, import_react.useRef)(null);
788
+ const widget = (0, import_react.useRef)(null);
789
+ const [token, setToken] = (0, import_react.useState)("");
790
+ (0, import_react.useEffect)(() => {
791
+ if (!siteKey || !box.current) return;
792
+ let gone = false;
793
+ void loadTurnstile().then((api) => {
794
+ if (gone || !box.current) return;
795
+ const id = api.render(box.current, {
796
+ sitekey: siteKey,
797
+ language: locale ?? "auto",
798
+ size: "flexible",
799
+ callback: (t) => setToken(t),
800
+ "expired-callback": () => setToken(""),
801
+ "error-callback": () => setToken("")
802
+ });
803
+ widget.current = { api, id };
804
+ }).catch(() => {
805
+ });
806
+ return () => {
807
+ gone = true;
808
+ if (widget.current) widget.current.api.remove(widget.current.id);
809
+ widget.current = null;
810
+ };
811
+ }, [siteKey, locale]);
812
+ const renew = () => {
813
+ setToken("");
814
+ if (widget.current) widget.current.api.reset(widget.current.id);
815
+ };
816
+ const node = siteKey ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: box, className: "cauthy-challenge" }) : null;
817
+ return { node, token: token || void 0, renew };
818
+ }
819
+ function useMethods(auth) {
820
+ const [methods, setMethods] = (0, import_react.useState)(null);
821
+ (0, import_react.useEffect)(() => {
822
+ void auth.methods().then(setMethods);
823
+ }, []);
824
+ return methods;
825
+ }
633
826
  function useForm(props) {
634
827
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
635
828
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
@@ -702,6 +895,25 @@ function SignIn(props) {
702
895
  const [password, setPassword] = (0, import_react.useState)("");
703
896
  const [code, setCode] = (0, import_react.useState)("");
704
897
  const [step, setStep] = (0, import_react.useState)("password");
898
+ const challenge = useChallenge(methods?.captchaSiteKey, props.locale);
899
+ const [phoneWay, setPhoneWay] = (0, import_react.useState)(null);
900
+ const [phoneSent, setPhoneSent] = (0, import_react.useState)("");
901
+ (0, import_react.useEffect)(() => {
902
+ if (step !== "code") return;
903
+ void auth.signIn.mfaMethods().then((r) => {
904
+ if (r.ok) setPhoneWay({ totp: r.totp, phone: r.phone });
905
+ });
906
+ }, [step]);
907
+ const sendCode = async () => {
908
+ f.setBusy(true);
909
+ f.setError(null);
910
+ const r = await auth.signIn.mfaSendCode();
911
+ f.setBusy(false);
912
+ if (!r.ok) return f.setError(r.error);
913
+ setPhoneSent(r.sentTo);
914
+ };
915
+ const autofill = (0, import_react.useRef)(null);
916
+ const [offer, setOffer] = (0, import_react.useState)(0);
705
917
  (0, import_react.useEffect)(() => {
706
918
  void auth.methods().then(setMethods);
707
919
  const back = returnedError();
@@ -716,30 +928,52 @@ function SignIn(props) {
716
928
  setStep("password");
717
929
  });
718
930
  });
931
+ (0, import_react.useEffect)(() => {
932
+ if (step !== "password" || !methods?.passkeyHere) return;
933
+ const ctl = new AbortController();
934
+ autofill.current = ctl;
935
+ void auth.signIn.passkeyAutofill({ signal: ctl.signal }).then((r) => {
936
+ if (ctl.signal.aborted) return;
937
+ if (r.ok) return f.done();
938
+ if (r.error.code === "cancelled") return;
939
+ f.setError(r.error);
940
+ if (r.error.code === "passkey_failed" || r.error.code === "step_expired") setOffer((n) => n + 1);
941
+ });
942
+ return () => ctl.abort();
943
+ }, [step, methods, offer]);
719
944
  const submit = async (e) => {
720
945
  e.preventDefault();
946
+ autofill.current?.abort();
721
947
  f.setBusy(true);
722
948
  f.setError(null);
723
- const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
949
+ 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 });
724
950
  f.setBusy(false);
725
- if (!r.ok) return f.setError(r.error);
951
+ if (step === "password") challenge.renew();
952
+ if (!r.ok) {
953
+ setOffer((n) => n + 1);
954
+ return f.setError(r.error);
955
+ }
726
956
  if (r.status === "mfa_required") return setStep("code");
727
957
  f.done();
728
958
  };
729
959
  const usePasskey = async () => {
960
+ autofill.current?.abort();
730
961
  f.setBusy(true);
731
962
  f.setError(null);
732
963
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
733
964
  if (r.ok && r.status === "redirected") return;
734
965
  f.setBusy(false);
966
+ if (!r.ok) setOffer((n) => n + 1);
735
967
  if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
736
968
  f.done();
737
969
  };
738
970
  const sendLink = async () => {
971
+ autofill.current?.abort();
739
972
  f.setBusy(true);
740
973
  f.setError(null);
741
- const r = await auth.signIn.magicLink({ email });
974
+ const r = await auth.signIn.magicLink({ email, captchaToken: challenge.token });
742
975
  f.setBusy(false);
976
+ challenge.renew();
743
977
  if (!r.ok) return f.setError(r.error);
744
978
  setStep("sent");
745
979
  };
@@ -755,9 +989,11 @@ function SignIn(props) {
755
989
  ] });
756
990
  }
757
991
  if (step === "code") {
992
+ const to = phoneWay?.phone?.number ?? "";
758
993
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.codeHelp }),
994
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: phoneSent ? f.t.codeSent.replace("{to}", phoneSent) : phoneWay?.phone && !phoneWay.totp ? f.t.codeHelpPhone : f.t.codeHelp }),
760
995
  general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
996
+ phoneWay?.phone && /* @__PURE__ */ (0, import_jsx_runtime.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) }),
761
997
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
762
998
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
763
999
  ] });
@@ -771,7 +1007,7 @@ function SignIn(props) {
771
1007
  ] }, p.id)),
772
1008
  methods?.passkey && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
773
1009
  ((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
774
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: methods?.passkeyHere ? "username webauthn" : "username", required: true, onChange: setEmail }),
775
1011
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
776
1012
  Field,
777
1013
  {
@@ -786,6 +1022,7 @@ function SignIn(props) {
786
1022
  aside: props.links?.forgotPassword ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: f.cx("link"), href: props.links.forgotPassword, children: f.t.forgotPassword }) : void 0
787
1023
  }
788
1024
  ),
1025
+ challenge.node,
789
1026
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signIn }),
790
1027
  methods?.magicLink && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void sendLink(), children: f.t.emailMeALink }),
791
1028
  methods?.signUp !== false && props.links?.signUp && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: f.cx("footer"), children: [
@@ -795,6 +1032,21 @@ function SignIn(props) {
795
1032
  ] })
796
1033
  ] });
797
1034
  }
1035
+ function GoogleOneTap(props) {
1036
+ const auth = useAuth();
1037
+ const unauthenticated = auth.status === "unauthenticated";
1038
+ (0, import_react.useEffect)(() => {
1039
+ if (!unauthenticated) return;
1040
+ const ctl = new AbortController();
1041
+ void auth.signIn.googleOneTap({ signal: ctl.signal, context: props.context }).then((r) => {
1042
+ if (!r.ok || ctl.signal.aborted) return;
1043
+ if (props.onSuccess) props.onSuccess();
1044
+ else if (props.returnTo) window.location.assign(props.returnTo);
1045
+ });
1046
+ return () => ctl.abort();
1047
+ }, [unauthenticated]);
1048
+ return null;
1049
+ }
798
1050
  function SignUp(props) {
799
1051
  const auth = useAuth();
800
1052
  const f = useForm(props);
@@ -802,12 +1054,14 @@ function SignUp(props) {
802
1054
  const [email, setEmail] = (0, import_react.useState)("");
803
1055
  const [password, setPassword] = (0, import_react.useState)("");
804
1056
  const [sent, setSent] = (0, import_react.useState)(false);
1057
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
805
1058
  const submit = async (e) => {
806
1059
  e.preventDefault();
807
1060
  f.setBusy(true);
808
1061
  f.setError(null);
809
- const r = await auth.signUp.email({ name, email, password });
1062
+ const r = await auth.signUp.email({ name, email, password, captchaToken: challenge.token });
810
1063
  f.setBusy(false);
1064
+ challenge.renew();
811
1065
  if (!r.ok) return f.setError(r.error);
812
1066
  if (r.status === "verification_required") return setSent(true);
813
1067
  f.done();
@@ -824,6 +1078,7 @@ function SignUp(props) {
824
1078
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
825
1079
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
826
1080
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "password", type: "password", label: f.t.password, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
1081
+ challenge.node,
827
1082
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signUp }),
828
1083
  props.links?.signIn && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: f.cx("footer"), children: [
829
1084
  f.t.haveAccount,
@@ -837,12 +1092,14 @@ function ForgotPassword(props) {
837
1092
  const f = useForm(props);
838
1093
  const [email, setEmail] = (0, import_react.useState)("");
839
1094
  const [sent, setSent] = (0, import_react.useState)(false);
1095
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
840
1096
  const submit = async (e) => {
841
1097
  e.preventDefault();
842
1098
  f.setBusy(true);
843
1099
  f.setError(null);
844
- const r = await auth.forgotPassword({ email });
1100
+ const r = await auth.forgotPassword({ email, captchaToken: challenge.token });
845
1101
  f.setBusy(false);
1102
+ challenge.renew();
846
1103
  if (!r.ok) return f.setError(r.error);
847
1104
  setSent(true);
848
1105
  };
@@ -856,6 +1113,7 @@ function ForgotPassword(props) {
856
1113
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
857
1114
  general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
858
1115
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
1116
+ challenge.node,
859
1117
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.sendLink }),
860
1118
  props.links?.signIn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("footer"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn }) })
861
1119
  ] });
@@ -955,6 +1213,7 @@ function Account(props) {
955
1213
  account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
956
1214
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
957
1215
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
1216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PhonePart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
958
1217
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
959
1218
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
960
1219
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
@@ -1184,6 +1443,77 @@ function SecondStepPart({ props, account, onChanged }) {
1184
1443
  account.mfaEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void renew(), children: f.t.newRecoveryCodes })
1185
1444
  ] });
1186
1445
  }
1446
+ function PhonePart({ props, account, onChanged }) {
1447
+ const auth = useAuth();
1448
+ const { f, run, general } = useSection(props);
1449
+ const [ways, setWays] = (0, import_react.useState)(null);
1450
+ const [number, setNumber] = (0, import_react.useState)("");
1451
+ const [channel, setChannel] = (0, import_react.useState)("sms");
1452
+ const [password, setPassword] = (0, import_react.useState)("");
1453
+ const [code, setCode] = (0, import_react.useState)("");
1454
+ const [sentTo, setSentTo] = (0, import_react.useState)("");
1455
+ const [codes, setCodes] = (0, import_react.useState)(null);
1456
+ const load = () => auth.account.mfa.factors().then((w) => {
1457
+ setWays(w);
1458
+ if (w && w.phoneChannels.length > 0 && !w.phoneChannels.includes(channel)) setChannel(w.phoneChannels[0]);
1459
+ });
1460
+ (0, import_react.useEffect)(() => {
1461
+ void load();
1462
+ }, []);
1463
+ if (!ways || ways.phoneChannels.length === 0) return null;
1464
+ const add = async (e) => {
1465
+ e.preventDefault();
1466
+ const r = await run(() => auth.account.mfa.addPhone({ phone: number, channel, password }));
1467
+ if (r.ok) setSentTo(r.sentTo);
1468
+ };
1469
+ const confirm = async (e) => {
1470
+ e.preventDefault();
1471
+ const r = await run(() => auth.account.mfa.verifyPhone({ code }));
1472
+ if (!r.ok) return;
1473
+ setSentTo("");
1474
+ setCode("");
1475
+ setPassword("");
1476
+ if (r.recoveryCodes.length > 0) setCodes(r.recoveryCodes);
1477
+ onChanged(true);
1478
+ await load();
1479
+ };
1480
+ const remove = async (e) => {
1481
+ e.preventDefault();
1482
+ const r = await run(() => auth.account.mfa.removePhone({ password }));
1483
+ if (!r.ok) return;
1484
+ setPassword("");
1485
+ await load();
1486
+ const left = await auth.account.mfa.factors();
1487
+ onChanged(Boolean(left?.totp));
1488
+ };
1489
+ const passwordField = account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword });
1490
+ if (sentTo) {
1491
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: confirm, noValidate: true, children: [
1492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.phone }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.codeSent.replace("{to}", sentTo) }),
1494
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
1497
+ ] });
1498
+ }
1499
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: ways.phone ? remove : add, noValidate: true, children: [
1500
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.phone }),
1501
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: ways.phone ? f.t.phoneOn.replace("{to}", ways.phone.number) : `${f.t.phoneOff} ${f.t.phoneHelp}` }),
1502
+ codes && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.keepCodes }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: f.cx("codes"), children: codes.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: c }) }, c)) })
1505
+ ] }),
1506
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1507
+ !ways.phone && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "phone", type: "tel", label: f.t.phone, value: number, autoComplete: "tel", required: true, onChange: setNumber }),
1508
+ !ways.phone && ways.phoneChannels.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: f.cx("field"), role: "radiogroup", children: ways.phoneChannels.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: f.cx("label"), children: [
1509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "radio", name: "channel", value: c, checked: channel === c, onChange: () => setChannel(c) }),
1510
+ " ",
1511
+ c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
1512
+ ] }, c)) }),
1513
+ passwordField,
1514
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: ways.phone ? f.t.removePhone : f.t.addPhone })
1515
+ ] });
1516
+ }
1187
1517
  function SignOutEverywhere({ props }) {
1188
1518
  const auth = useAuth();
1189
1519
  const { f, run, general } = useSection(props);
@@ -1286,7 +1616,16 @@ function useAuth() {
1286
1616
  };
1287
1617
  return {
1288
1618
  ...client,
1289
- 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)) },
1619
+ signIn: {
1620
+ ...client.signIn,
1621
+ email: (f) => then(client.signIn.email(f)),
1622
+ mfa: (f) => then(client.signIn.mfa(f)),
1623
+ magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
1624
+ emailCode: (f) => then(client.signIn.emailCode(f)),
1625
+ passkey: (o) => then(client.signIn.passkey(o)),
1626
+ passkeyAutofill: (o) => then(client.signIn.passkeyAutofill(o)),
1627
+ googleOneTap: (o) => then(client.signIn.googleOneTap(o))
1628
+ },
1290
1629
  signUp: { email: (f) => then(client.signUp.email(f)) },
1291
1630
  signOut: () => then(client.signOut()),
1292
1631
  account: {
@@ -1307,11 +1646,13 @@ function useAuth() {
1307
1646
  Account,
1308
1647
  CauthyProvider,
1309
1648
  ForgotPassword,
1649
+ GoogleOneTap,
1310
1650
  ResetPassword,
1311
1651
  SignIn,
1312
1652
  SignUp,
1313
1653
  VerifyEmail,
1314
1654
  can,
1655
+ canAutofillPasskeys,
1315
1656
  canUsePasskeys,
1316
1657
  createAuthClient,
1317
1658
  definePolicy,