cauthy-auth 0.12.0 → 0.14.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
@@ -240,13 +240,13 @@ function createAuthClient(options = {}) {
240
240
  const said = typeof options.locale === "function" ? options.locale() : options.locale;
241
241
  return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
242
242
  };
243
- async function post(path, form, bad) {
243
+ async function post(path, form, bad, captchaToken) {
244
244
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
245
245
  try {
246
246
  const res = await doFetch(`${base}${path}`, {
247
247
  method: "POST",
248
248
  credentials: "same-origin",
249
- 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 } : {} },
250
250
  body: JSON.stringify(form)
251
251
  });
252
252
  const body = await res.json().catch(() => null);
@@ -298,9 +298,11 @@ function createAuthClient(options = {}) {
298
298
  }
299
299
  return {
300
300
  signIn: {
301
- 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),
302
302
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
303
- 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),
304
306
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
305
307
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
306
308
  passkeyUrl,
@@ -381,9 +383,9 @@ function createAuthClient(options = {}) {
381
383
  }
382
384
  },
383
385
  signUp: {
384
- 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)
385
387
  },
386
- 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),
387
389
  resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
388
390
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
389
391
  signOut: () => post("/sign-out", {}, null),
@@ -401,7 +403,14 @@ function createAuthClient(options = {}) {
401
403
  setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
402
404
  verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
403
405
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
404
- 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)
405
414
  },
406
415
  changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
407
416
  async identities() {
@@ -418,6 +427,11 @@ function createAuthClient(options = {}) {
418
427
  return body && Array.isArray(body.sessions) ? body.sessions : [];
419
428
  },
420
429
  signOutEverywhere: () => post("/me/sessions/end", {}, null),
430
+ async agents() {
431
+ const body = await read("/me/agents");
432
+ return body && Array.isArray(body.agents) ? body.agents : [];
433
+ },
434
+ disconnectAgent: (id) => post("/me/agents/disconnect", { id }, null),
421
435
  async passkeys() {
422
436
  const body = await read("/me/passkeys");
423
437
  return body && Array.isArray(body.passkeys) ? body.passkeys : [];
@@ -519,6 +533,18 @@ var TEXTS = {
519
533
  linkSent: "If that address has an account, a link to choose a new password is on its way.",
520
534
  codeHelp: "Enter the code from your authenticator app, or a recovery code.",
521
535
  verify: "Verify",
536
+ codeHelpPhone: "We send a code to your phone. Ask for it, then type it here, or use a recovery code.",
537
+ sendCodeSms: "Send a code by SMS to {to}",
538
+ sendCodeWhatsapp: "Send a code by WhatsApp to {to}",
539
+ codeSent: "We sent a code to {to}. Type it here.",
540
+ phone: "Phone, with its country code",
541
+ phoneHelp: "Get your codes by SMS or WhatsApp instead of an app.",
542
+ phoneOn: "Codes go to {to}.",
543
+ phoneOff: "No phone yet.",
544
+ addPhone: "Add phone",
545
+ removePhone: "Remove phone",
546
+ bySms: "SMS",
547
+ byWhatsapp: "WhatsApp",
522
548
  checkEmail: "Almost there: open the link we sent to confirm your address, then sign in.",
523
549
  backToSignIn: "Back to sign in",
524
550
  emailMeALink: "Email me a sign-in link",
@@ -557,6 +583,10 @@ var TEXTS = {
557
583
  passkeyUnnamed: "Passkey",
558
584
  sessions: "Where you are signed in",
559
585
  signOutEverywhere: "Sign out everywhere",
586
+ agents: "AI agents",
587
+ agentsHelp: "Agents you allowed to act for you. Disconnect one and it loses its access within minutes.",
588
+ disconnect: "Disconnect",
589
+ anAgent: "An AI agent",
560
590
  unknownDevice: "Unknown device",
561
591
  emailAddress: "Email address",
562
592
  newEmail: "New email address",
@@ -586,6 +616,13 @@ var TEXTS = {
586
616
  step_up_required: "Enter your current password.",
587
617
  weak_password: "Choose a stronger password.",
588
618
  unauthenticated: "Sign in first.",
619
+ phone_refused: "The code could not be sent to that number.",
620
+ too_many_codes: "Enough codes were sent. Use the last one, or sign in again.",
621
+ phone_unavailable: "Codes cannot be sent that way here.",
622
+ phone_already_set: "Remove the phone you have first.",
623
+ phone_codes_unavailable: "Codes cannot be sent right now. Try again in a moment.",
624
+ captcha_required: "Complete the check that you are not a bot, then try again.",
625
+ captcha_failed: "The check that you are not a bot did not pass. Try it again.",
589
626
  passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
590
627
  passkey_exists: "This device already has a passkey for your account.",
591
628
  passkey_unavailable: "Passkeys cannot be used here."
@@ -608,6 +645,18 @@ var TEXTS = {
608
645
  linkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para elegir una contrase\xF1a nueva.",
609
646
  codeHelp: "Escribe el c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n, o un c\xF3digo de recuperaci\xF3n.",
610
647
  verify: "Verificar",
648
+ 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.",
649
+ sendCodeSms: "Enviar un c\xF3digo por SMS a {to}",
650
+ sendCodeWhatsapp: "Enviar un c\xF3digo por WhatsApp a {to}",
651
+ codeSent: "Te enviamos un c\xF3digo a {to}. Escr\xEDbelo aqu\xED.",
652
+ phone: "Tel\xE9fono, con su c\xF3digo de pa\xEDs",
653
+ phoneHelp: "Recibe tus c\xF3digos por SMS o WhatsApp en vez de una app.",
654
+ phoneOn: "Los c\xF3digos van a {to}.",
655
+ phoneOff: "Todav\xEDa no tienes un tel\xE9fono.",
656
+ addPhone: "Agregar tel\xE9fono",
657
+ removePhone: "Quitar tel\xE9fono",
658
+ bySms: "SMS",
659
+ byWhatsapp: "WhatsApp",
611
660
  checkEmail: "Ya casi: abre el enlace que te enviamos para confirmar tu correo y luego entra.",
612
661
  backToSignIn: "Volver a entrar",
613
662
  emailMeALink: "Env\xEDame un enlace para entrar",
@@ -646,6 +695,10 @@ var TEXTS = {
646
695
  passkeyUnnamed: "Llave de acceso",
647
696
  sessions: "D\xF3nde tienes la sesi\xF3n abierta",
648
697
  signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
698
+ agents: "Agentes de IA",
699
+ agentsHelp: "Los agentes a los que permitiste actuar por ti. Si desconectas uno, pierde el acceso en pocos minutos.",
700
+ disconnect: "Desconectar",
701
+ anAgent: "Un agente de IA",
649
702
  unknownDevice: "Dispositivo desconocido",
650
703
  emailAddress: "Correo",
651
704
  newEmail: "Correo nuevo",
@@ -675,6 +728,13 @@ var TEXTS = {
675
728
  step_up_required: "Escribe tu contrase\xF1a actual.",
676
729
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
677
730
  unauthenticated: "Inicia sesi\xF3n primero.",
731
+ phone_refused: "No se pudo enviar el c\xF3digo a ese n\xFAmero.",
732
+ too_many_codes: "Ya se enviaron suficientes c\xF3digos. Usa el \xFAltimo o vuelve a entrar.",
733
+ phone_unavailable: "Aqu\xED no se pueden enviar c\xF3digos de esa forma.",
734
+ phone_already_set: "Primero quita el tel\xE9fono que tienes.",
735
+ phone_codes_unavailable: "Ahora no se pueden enviar c\xF3digos. Int\xE9ntalo en un momento.",
736
+ captcha_required: "Completa la verificaci\xF3n de que no eres un robot y vuelve a intentarlo.",
737
+ captcha_failed: "La verificaci\xF3n de que no eres un robot no pas\xF3. Int\xE9ntalo de nuevo.",
678
738
  passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
679
739
  passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
680
740
  passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
@@ -719,6 +779,63 @@ function useRoot(props) {
719
779
  if (props.unstyled) return {};
720
780
  return { "data-cauthy": "", style: vars };
721
781
  }
782
+ var turnstileScript = null;
783
+ function loadTurnstile() {
784
+ const ready = () => window.turnstile;
785
+ if (ready()) return Promise.resolve(ready());
786
+ turnstileScript ??= new Promise((resolve, reject) => {
787
+ const script = document.createElement("script");
788
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
789
+ script.async = true;
790
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("turnstile did not load"));
791
+ script.onerror = () => {
792
+ turnstileScript = null;
793
+ reject(new Error("turnstile did not load"));
794
+ };
795
+ document.head.appendChild(script);
796
+ });
797
+ return turnstileScript;
798
+ }
799
+ function useChallenge(siteKey, locale) {
800
+ const box = (0, import_react.useRef)(null);
801
+ const widget = (0, import_react.useRef)(null);
802
+ const [token, setToken] = (0, import_react.useState)("");
803
+ (0, import_react.useEffect)(() => {
804
+ if (!siteKey || !box.current) return;
805
+ let gone = false;
806
+ void loadTurnstile().then((api) => {
807
+ if (gone || !box.current) return;
808
+ const id = api.render(box.current, {
809
+ sitekey: siteKey,
810
+ language: locale ?? "auto",
811
+ size: "flexible",
812
+ callback: (t) => setToken(t),
813
+ "expired-callback": () => setToken(""),
814
+ "error-callback": () => setToken("")
815
+ });
816
+ widget.current = { api, id };
817
+ }).catch(() => {
818
+ });
819
+ return () => {
820
+ gone = true;
821
+ if (widget.current) widget.current.api.remove(widget.current.id);
822
+ widget.current = null;
823
+ };
824
+ }, [siteKey, locale]);
825
+ const renew = () => {
826
+ setToken("");
827
+ if (widget.current) widget.current.api.reset(widget.current.id);
828
+ };
829
+ const node = siteKey ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: box, className: "cauthy-challenge" }) : null;
830
+ return { node, token: token || void 0, renew };
831
+ }
832
+ function useMethods(auth) {
833
+ const [methods, setMethods] = (0, import_react.useState)(null);
834
+ (0, import_react.useEffect)(() => {
835
+ void auth.methods().then(setMethods);
836
+ }, []);
837
+ return methods;
838
+ }
722
839
  function useForm(props) {
723
840
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
724
841
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
@@ -791,6 +908,23 @@ function SignIn(props) {
791
908
  const [password, setPassword] = (0, import_react.useState)("");
792
909
  const [code, setCode] = (0, import_react.useState)("");
793
910
  const [step, setStep] = (0, import_react.useState)("password");
911
+ const challenge = useChallenge(methods?.captchaSiteKey, props.locale);
912
+ const [phoneWay, setPhoneWay] = (0, import_react.useState)(null);
913
+ const [phoneSent, setPhoneSent] = (0, import_react.useState)("");
914
+ (0, import_react.useEffect)(() => {
915
+ if (step !== "code") return;
916
+ void auth.signIn.mfaMethods().then((r) => {
917
+ if (r.ok) setPhoneWay({ totp: r.totp, phone: r.phone });
918
+ });
919
+ }, [step]);
920
+ const sendCode = async () => {
921
+ f.setBusy(true);
922
+ f.setError(null);
923
+ const r = await auth.signIn.mfaSendCode();
924
+ f.setBusy(false);
925
+ if (!r.ok) return f.setError(r.error);
926
+ setPhoneSent(r.sentTo);
927
+ };
794
928
  const autofill = (0, import_react.useRef)(null);
795
929
  const [offer, setOffer] = (0, import_react.useState)(0);
796
930
  (0, import_react.useEffect)(() => {
@@ -825,8 +959,9 @@ function SignIn(props) {
825
959
  autofill.current?.abort();
826
960
  f.setBusy(true);
827
961
  f.setError(null);
828
- const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
962
+ 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 });
829
963
  f.setBusy(false);
964
+ if (step === "password") challenge.renew();
830
965
  if (!r.ok) {
831
966
  setOffer((n) => n + 1);
832
967
  return f.setError(r.error);
@@ -849,8 +984,9 @@ function SignIn(props) {
849
984
  autofill.current?.abort();
850
985
  f.setBusy(true);
851
986
  f.setError(null);
852
- const r = await auth.signIn.magicLink({ email });
987
+ const r = await auth.signIn.magicLink({ email, captchaToken: challenge.token });
853
988
  f.setBusy(false);
989
+ challenge.renew();
854
990
  if (!r.ok) return f.setError(r.error);
855
991
  setStep("sent");
856
992
  };
@@ -866,9 +1002,11 @@ function SignIn(props) {
866
1002
  ] });
867
1003
  }
868
1004
  if (step === "code") {
1005
+ const to = phoneWay?.phone?.number ?? "";
869
1006
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
870
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.codeHelp }),
1007
+ /* @__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 }),
871
1008
  general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1009
+ 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) }),
872
1010
  /* @__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 }),
873
1011
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
874
1012
  ] });
@@ -897,6 +1035,7 @@ function SignIn(props) {
897
1035
  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
898
1036
  }
899
1037
  ),
1038
+ challenge.node,
900
1039
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signIn }),
901
1040
  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 }),
902
1041
  methods?.signUp !== false && props.links?.signUp && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: f.cx("footer"), children: [
@@ -928,12 +1067,14 @@ function SignUp(props) {
928
1067
  const [email, setEmail] = (0, import_react.useState)("");
929
1068
  const [password, setPassword] = (0, import_react.useState)("");
930
1069
  const [sent, setSent] = (0, import_react.useState)(false);
1070
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
931
1071
  const submit = async (e) => {
932
1072
  e.preventDefault();
933
1073
  f.setBusy(true);
934
1074
  f.setError(null);
935
- const r = await auth.signUp.email({ name, email, password });
1075
+ const r = await auth.signUp.email({ name, email, password, captchaToken: challenge.token });
936
1076
  f.setBusy(false);
1077
+ challenge.renew();
937
1078
  if (!r.ok) return f.setError(r.error);
938
1079
  if (r.status === "verification_required") return setSent(true);
939
1080
  f.done();
@@ -950,6 +1091,7 @@ function SignUp(props) {
950
1091
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
951
1092
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
952
1093
  /* @__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 }),
1094
+ challenge.node,
953
1095
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signUp }),
954
1096
  props.links?.signIn && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: f.cx("footer"), children: [
955
1097
  f.t.haveAccount,
@@ -963,12 +1105,14 @@ function ForgotPassword(props) {
963
1105
  const f = useForm(props);
964
1106
  const [email, setEmail] = (0, import_react.useState)("");
965
1107
  const [sent, setSent] = (0, import_react.useState)(false);
1108
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
966
1109
  const submit = async (e) => {
967
1110
  e.preventDefault();
968
1111
  f.setBusy(true);
969
1112
  f.setError(null);
970
- const r = await auth.forgotPassword({ email });
1113
+ const r = await auth.forgotPassword({ email, captchaToken: challenge.token });
971
1114
  f.setBusy(false);
1115
+ challenge.renew();
972
1116
  if (!r.ok) return f.setError(r.error);
973
1117
  setSent(true);
974
1118
  };
@@ -982,6 +1126,7 @@ function ForgotPassword(props) {
982
1126
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
983
1127
  general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
984
1128
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
1129
+ challenge.node,
985
1130
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.sendLink }),
986
1131
  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 }) })
987
1132
  ] });
@@ -1081,7 +1226,9 @@ function Account(props) {
1081
1226
  account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
1082
1227
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
1083
1228
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
1229
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PhonePart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
1084
1230
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
1231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentsPart, { props, when }),
1085
1232
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
1086
1233
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
1087
1234
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: cx("sessions"), children: sessions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: cx("session"), children: [
@@ -1310,6 +1457,106 @@ function SecondStepPart({ props, account, onChanged }) {
1310
1457
  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 })
1311
1458
  ] });
1312
1459
  }
1460
+ function PhonePart({ props, account, onChanged }) {
1461
+ const auth = useAuth();
1462
+ const { f, run, general } = useSection(props);
1463
+ const [ways, setWays] = (0, import_react.useState)(null);
1464
+ const [number, setNumber] = (0, import_react.useState)("");
1465
+ const [channel, setChannel] = (0, import_react.useState)("sms");
1466
+ const [password, setPassword] = (0, import_react.useState)("");
1467
+ const [code, setCode] = (0, import_react.useState)("");
1468
+ const [sentTo, setSentTo] = (0, import_react.useState)("");
1469
+ const [codes, setCodes] = (0, import_react.useState)(null);
1470
+ const load = () => auth.account.mfa.factors().then((w) => {
1471
+ setWays(w);
1472
+ if (w && w.phoneChannels.length > 0 && !w.phoneChannels.includes(channel)) setChannel(w.phoneChannels[0]);
1473
+ });
1474
+ (0, import_react.useEffect)(() => {
1475
+ void load();
1476
+ }, []);
1477
+ if (!ways || ways.phoneChannels.length === 0) return null;
1478
+ const add = async (e) => {
1479
+ e.preventDefault();
1480
+ const r = await run(() => auth.account.mfa.addPhone({ phone: number, channel, password }));
1481
+ if (r.ok) setSentTo(r.sentTo);
1482
+ };
1483
+ const confirm = async (e) => {
1484
+ e.preventDefault();
1485
+ const r = await run(() => auth.account.mfa.verifyPhone({ code }));
1486
+ if (!r.ok) return;
1487
+ setSentTo("");
1488
+ setCode("");
1489
+ setPassword("");
1490
+ if (r.recoveryCodes.length > 0) setCodes(r.recoveryCodes);
1491
+ onChanged(true);
1492
+ await load();
1493
+ };
1494
+ const remove = async (e) => {
1495
+ e.preventDefault();
1496
+ const r = await run(() => auth.account.mfa.removePhone({ password }));
1497
+ if (!r.ok) return;
1498
+ setPassword("");
1499
+ await load();
1500
+ const left = await auth.account.mfa.factors();
1501
+ onChanged(Boolean(left?.totp));
1502
+ };
1503
+ 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 });
1504
+ if (sentTo) {
1505
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: confirm, noValidate: true, children: [
1506
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.phone }),
1507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.codeSent.replace("{to}", sentTo) }),
1508
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1509
+ /* @__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 }),
1510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
1511
+ ] });
1512
+ }
1513
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: ways.phone ? remove : add, noValidate: true, children: [
1514
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.phone }),
1515
+ /* @__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}` }),
1516
+ codes && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1517
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.keepCodes }),
1518
+ /* @__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)) })
1519
+ ] }),
1520
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1521
+ !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 }),
1522
+ !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: [
1523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "radio", name: "channel", value: c, checked: channel === c, onChange: () => setChannel(c) }),
1524
+ " ",
1525
+ c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
1526
+ ] }, c)) }),
1527
+ passwordField,
1528
+ /* @__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 })
1529
+ ] });
1530
+ }
1531
+ function AgentsPart({ props, when }) {
1532
+ const auth = useAuth();
1533
+ const { f, run, general } = useSection(props);
1534
+ const [list, setList] = (0, import_react.useState)([]);
1535
+ (0, import_react.useEffect)(() => {
1536
+ void auth.account.agents().then(setList);
1537
+ }, []);
1538
+ if (list.length === 0) return null;
1539
+ const disconnect = async (id) => {
1540
+ const r = await run(() => auth.account.disconnectAgent(id));
1541
+ if (r.ok) setList(list.filter((a) => a.id !== id));
1542
+ };
1543
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
1544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.agents }),
1545
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.agentsHelp }),
1546
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1547
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: f.cx("sessions"), children: list.map((a) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: f.cx("session"), children: [
1548
+ a.name || f.t.anAgent,
1549
+ a.redirectHost ? ` (${a.redirectHost})` : "",
1550
+ " \u2192 ",
1551
+ a.resource.name,
1552
+ a.scopes.length > 0 ? ` \xB7 ${a.scopes.map((s) => s.description || s.name).join(", ")}` : "",
1553
+ " \xB7 ",
1554
+ when(a.lastUsedAt),
1555
+ " ",
1556
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void disconnect(a.id), children: f.t.disconnect })
1557
+ ] }, a.id)) })
1558
+ ] });
1559
+ }
1313
1560
  function SignOutEverywhere({ props }) {
1314
1561
  const auth = useAuth();
1315
1562
  const { f, run, general } = useSection(props);