cauthy-auth 0.12.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.d.mts CHANGED
@@ -258,6 +258,13 @@ interface AuthMethods {
258
258
  * page that cannot, or a browser that refuses to here, sends the person.
259
259
  */
260
260
  passkeyOnCauthy: boolean;
261
+ /**
262
+ * The workspace's Cloudflare Turnstile site key, when it asks for the
263
+ * challenge: signing in, creating an account, and asking for a sign-in link
264
+ * or a new password then take its answer (`captchaToken`). The ready-made
265
+ * forms show it themselves.
266
+ */
267
+ captchaSiteKey?: string;
261
268
  }
262
269
  /** A passkey of the signed-in account. Moments are UTC. */
263
270
  interface Passkey {
@@ -299,6 +306,17 @@ interface LinkedIdentity {
299
306
  /** When it was linked, in UTC (ISO 8601). */
300
307
  linkedAt: string;
301
308
  }
309
+ /** How an account takes its second step. */
310
+ interface SecondStep {
311
+ totp: boolean;
312
+ /** The phone codes are sent to, masked (`+57 ••• ••• 4567`). */
313
+ phone: {
314
+ number: string;
315
+ channel: string;
316
+ } | null;
317
+ /** The ways the workspace sends codes to phones. */
318
+ phoneChannels: string[];
319
+ }
302
320
  interface AccountClient {
303
321
  /** The account, or null when nobody is signed in. */
304
322
  get(): Promise<Account$1 | null>;
@@ -337,6 +355,30 @@ interface AccountClient {
337
355
  }): Promise<AuthResult<'renewed', {
338
356
  recoveryCodes: string[];
339
357
  }>>;
358
+ /**
359
+ * How the account takes its second step: an authenticator app, a phone
360
+ * (masked), and the ways the workspace sends codes to phones (`sms`,
361
+ * `whatsapp`; none when it has no Twilio account for it).
362
+ */
363
+ factors(): Promise<SecondStep | null>;
364
+ /** Adds a phone as the second step: a code is sent to it, for `verifyPhone`. */
365
+ addPhone(form: {
366
+ phone: string;
367
+ channel: 'sms' | 'whatsapp';
368
+ password: string;
369
+ }): Promise<AuthResult<'sent', {
370
+ sentTo: string;
371
+ channel: string;
372
+ }>>;
373
+ /** Proves the phone with the code it received. `recoveryCodes` come with a first second step, shown this once. */
374
+ verifyPhone(form: {
375
+ code: string;
376
+ }): Promise<AuthResult<'enabled', {
377
+ recoveryCodes: string[];
378
+ }>>;
379
+ removePhone(form: {
380
+ password: string;
381
+ }): Promise<AuthResult<'removed'>>;
340
382
  };
341
383
  /**
342
384
  * Asks to sign in with another address. Nothing changes yet: the new address
@@ -393,6 +435,7 @@ interface AuthClient {
393
435
  email(form: {
394
436
  email: string;
395
437
  password: string;
438
+ captchaToken?: string;
396
439
  }): Promise<AuthResult<'signed_in', {
397
440
  session: PublicSession;
398
441
  }> | AuthResult<'mfa_required'>>;
@@ -402,9 +445,23 @@ interface AuthClient {
402
445
  }): Promise<AuthResult<'signed_in', {
403
446
  session: PublicSession;
404
447
  }>>;
448
+ /** While the second step is asked for: whether the account has an authenticator app, and the phone (masked) a code can be sent to. */
449
+ mfaMethods(): Promise<AuthResult<'ok', {
450
+ totp: boolean;
451
+ phone: {
452
+ number: string;
453
+ channel: string;
454
+ } | null;
455
+ }>>;
456
+ /** Sends a code to the account's phone, by SMS or WhatsApp; `mfa` takes it. A few per sign-in. */
457
+ mfaSendCode(): Promise<AuthResult<'sent', {
458
+ sentTo: string;
459
+ channel: string;
460
+ }>>;
405
461
  /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
406
462
  magicLink(form: {
407
463
  email: string;
464
+ captchaToken?: string;
408
465
  }): Promise<AuthResult<'sent'>>;
409
466
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
410
467
  magicLinkVerify(form: {
@@ -480,6 +537,7 @@ interface AuthClient {
480
537
  email: string;
481
538
  password: string;
482
539
  name?: string;
540
+ captchaToken?: string;
483
541
  }): Promise<AuthResult<'signed_in', {
484
542
  session: PublicSession;
485
543
  }> | AuthResult<'verification_required', {
@@ -489,6 +547,7 @@ interface AuthClient {
489
547
  /** Mails a link to choose a new password. The answer is the same whether or not the address has an account. */
490
548
  forgotPassword(form: {
491
549
  email: string;
550
+ captchaToken?: string;
492
551
  }): Promise<AuthResult<'sent'>>;
493
552
  /** Sets the new password, with the token the mailed link opened the page with (`token`). */
494
553
  resetPassword(form: {
@@ -542,6 +601,20 @@ interface AuthTexts {
542
601
  sendLink: string;
543
602
  linkSent: string;
544
603
  codeHelp: string;
604
+ /** The second step when it is a phone only. */
605
+ codeHelpPhone: string;
606
+ /** "{to}" is the phone, masked. */
607
+ sendCodeSms: string;
608
+ sendCodeWhatsapp: string;
609
+ codeSent: string;
610
+ phone: string;
611
+ phoneHelp: string;
612
+ phoneOn: string;
613
+ phoneOff: string;
614
+ addPhone: string;
615
+ removePhone: string;
616
+ bySms: string;
617
+ byWhatsapp: string;
545
618
  verify: string;
546
619
  checkEmail: string;
547
620
  backToSignIn: string;
@@ -734,4 +807,4 @@ declare function useSession(): SessionContext;
734
807
  */
735
808
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
736
809
 
737
- export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, GoogleOneTap, type GoogleOneTapProps, type JsonValue, type LinkedIdentity, type Passkey, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, canAutofillPasskeys, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
810
+ export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, GoogleOneTap, type GoogleOneTapProps, type JsonValue, type LinkedIdentity, type Passkey, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SecondStep, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, canAutofillPasskeys, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
package/dist/react.d.ts CHANGED
@@ -258,6 +258,13 @@ interface AuthMethods {
258
258
  * page that cannot, or a browser that refuses to here, sends the person.
259
259
  */
260
260
  passkeyOnCauthy: boolean;
261
+ /**
262
+ * The workspace's Cloudflare Turnstile site key, when it asks for the
263
+ * challenge: signing in, creating an account, and asking for a sign-in link
264
+ * or a new password then take its answer (`captchaToken`). The ready-made
265
+ * forms show it themselves.
266
+ */
267
+ captchaSiteKey?: string;
261
268
  }
262
269
  /** A passkey of the signed-in account. Moments are UTC. */
263
270
  interface Passkey {
@@ -299,6 +306,17 @@ interface LinkedIdentity {
299
306
  /** When it was linked, in UTC (ISO 8601). */
300
307
  linkedAt: string;
301
308
  }
309
+ /** How an account takes its second step. */
310
+ interface SecondStep {
311
+ totp: boolean;
312
+ /** The phone codes are sent to, masked (`+57 ••• ••• 4567`). */
313
+ phone: {
314
+ number: string;
315
+ channel: string;
316
+ } | null;
317
+ /** The ways the workspace sends codes to phones. */
318
+ phoneChannels: string[];
319
+ }
302
320
  interface AccountClient {
303
321
  /** The account, or null when nobody is signed in. */
304
322
  get(): Promise<Account$1 | null>;
@@ -337,6 +355,30 @@ interface AccountClient {
337
355
  }): Promise<AuthResult<'renewed', {
338
356
  recoveryCodes: string[];
339
357
  }>>;
358
+ /**
359
+ * How the account takes its second step: an authenticator app, a phone
360
+ * (masked), and the ways the workspace sends codes to phones (`sms`,
361
+ * `whatsapp`; none when it has no Twilio account for it).
362
+ */
363
+ factors(): Promise<SecondStep | null>;
364
+ /** Adds a phone as the second step: a code is sent to it, for `verifyPhone`. */
365
+ addPhone(form: {
366
+ phone: string;
367
+ channel: 'sms' | 'whatsapp';
368
+ password: string;
369
+ }): Promise<AuthResult<'sent', {
370
+ sentTo: string;
371
+ channel: string;
372
+ }>>;
373
+ /** Proves the phone with the code it received. `recoveryCodes` come with a first second step, shown this once. */
374
+ verifyPhone(form: {
375
+ code: string;
376
+ }): Promise<AuthResult<'enabled', {
377
+ recoveryCodes: string[];
378
+ }>>;
379
+ removePhone(form: {
380
+ password: string;
381
+ }): Promise<AuthResult<'removed'>>;
340
382
  };
341
383
  /**
342
384
  * Asks to sign in with another address. Nothing changes yet: the new address
@@ -393,6 +435,7 @@ interface AuthClient {
393
435
  email(form: {
394
436
  email: string;
395
437
  password: string;
438
+ captchaToken?: string;
396
439
  }): Promise<AuthResult<'signed_in', {
397
440
  session: PublicSession;
398
441
  }> | AuthResult<'mfa_required'>>;
@@ -402,9 +445,23 @@ interface AuthClient {
402
445
  }): Promise<AuthResult<'signed_in', {
403
446
  session: PublicSession;
404
447
  }>>;
448
+ /** While the second step is asked for: whether the account has an authenticator app, and the phone (masked) a code can be sent to. */
449
+ mfaMethods(): Promise<AuthResult<'ok', {
450
+ totp: boolean;
451
+ phone: {
452
+ number: string;
453
+ channel: string;
454
+ } | null;
455
+ }>>;
456
+ /** Sends a code to the account's phone, by SMS or WhatsApp; `mfa` takes it. A few per sign-in. */
457
+ mfaSendCode(): Promise<AuthResult<'sent', {
458
+ sentTo: string;
459
+ channel: string;
460
+ }>>;
405
461
  /** Mails a link that signs the person in. The answer is the same whether or not the address has an account. */
406
462
  magicLink(form: {
407
463
  email: string;
464
+ captchaToken?: string;
408
465
  }): Promise<AuthResult<'sent'>>;
409
466
  /** Signs in with the token the mailed link opened the page with (`ml_token`). */
410
467
  magicLinkVerify(form: {
@@ -480,6 +537,7 @@ interface AuthClient {
480
537
  email: string;
481
538
  password: string;
482
539
  name?: string;
540
+ captchaToken?: string;
483
541
  }): Promise<AuthResult<'signed_in', {
484
542
  session: PublicSession;
485
543
  }> | AuthResult<'verification_required', {
@@ -489,6 +547,7 @@ interface AuthClient {
489
547
  /** Mails a link to choose a new password. The answer is the same whether or not the address has an account. */
490
548
  forgotPassword(form: {
491
549
  email: string;
550
+ captchaToken?: string;
492
551
  }): Promise<AuthResult<'sent'>>;
493
552
  /** Sets the new password, with the token the mailed link opened the page with (`token`). */
494
553
  resetPassword(form: {
@@ -542,6 +601,20 @@ interface AuthTexts {
542
601
  sendLink: string;
543
602
  linkSent: string;
544
603
  codeHelp: string;
604
+ /** The second step when it is a phone only. */
605
+ codeHelpPhone: string;
606
+ /** "{to}" is the phone, masked. */
607
+ sendCodeSms: string;
608
+ sendCodeWhatsapp: string;
609
+ codeSent: string;
610
+ phone: string;
611
+ phoneHelp: string;
612
+ phoneOn: string;
613
+ phoneOff: string;
614
+ addPhone: string;
615
+ removePhone: string;
616
+ bySms: string;
617
+ byWhatsapp: string;
545
618
  verify: string;
546
619
  checkEmail: string;
547
620
  backToSignIn: string;
@@ -734,4 +807,4 @@ declare function useSession(): SessionContext;
734
807
  */
735
808
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
736
809
 
737
- export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, GoogleOneTap, type GoogleOneTapProps, type JsonValue, type LinkedIdentity, type Passkey, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, canAutofillPasskeys, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
810
+ export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, GoogleOneTap, type GoogleOneTapProps, type JsonValue, type LinkedIdentity, type Passkey, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SecondStep, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, canAutofillPasskeys, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
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() {
@@ -519,6 +528,18 @@ var TEXTS = {
519
528
  linkSent: "If that address has an account, a link to choose a new password is on its way.",
520
529
  codeHelp: "Enter the code from your authenticator app, or a recovery code.",
521
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",
522
543
  checkEmail: "Almost there: open the link we sent to confirm your address, then sign in.",
523
544
  backToSignIn: "Back to sign in",
524
545
  emailMeALink: "Email me a sign-in link",
@@ -586,6 +607,13 @@ var TEXTS = {
586
607
  step_up_required: "Enter your current password.",
587
608
  weak_password: "Choose a stronger password.",
588
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.",
589
617
  passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
590
618
  passkey_exists: "This device already has a passkey for your account.",
591
619
  passkey_unavailable: "Passkeys cannot be used here."
@@ -608,6 +636,18 @@ var TEXTS = {
608
636
  linkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para elegir una contrase\xF1a nueva.",
609
637
  codeHelp: "Escribe el c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n, o un c\xF3digo de recuperaci\xF3n.",
610
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",
611
651
  checkEmail: "Ya casi: abre el enlace que te enviamos para confirmar tu correo y luego entra.",
612
652
  backToSignIn: "Volver a entrar",
613
653
  emailMeALink: "Env\xEDame un enlace para entrar",
@@ -675,6 +715,13 @@ var TEXTS = {
675
715
  step_up_required: "Escribe tu contrase\xF1a actual.",
676
716
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
677
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.",
678
725
  passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
679
726
  passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
680
727
  passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
@@ -719,6 +766,63 @@ function useRoot(props) {
719
766
  if (props.unstyled) return {};
720
767
  return { "data-cauthy": "", style: vars };
721
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
+ }
722
826
  function useForm(props) {
723
827
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
724
828
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
@@ -791,6 +895,23 @@ function SignIn(props) {
791
895
  const [password, setPassword] = (0, import_react.useState)("");
792
896
  const [code, setCode] = (0, import_react.useState)("");
793
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
+ };
794
915
  const autofill = (0, import_react.useRef)(null);
795
916
  const [offer, setOffer] = (0, import_react.useState)(0);
796
917
  (0, import_react.useEffect)(() => {
@@ -825,8 +946,9 @@ function SignIn(props) {
825
946
  autofill.current?.abort();
826
947
  f.setBusy(true);
827
948
  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 });
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 });
829
950
  f.setBusy(false);
951
+ if (step === "password") challenge.renew();
830
952
  if (!r.ok) {
831
953
  setOffer((n) => n + 1);
832
954
  return f.setError(r.error);
@@ -849,8 +971,9 @@ function SignIn(props) {
849
971
  autofill.current?.abort();
850
972
  f.setBusy(true);
851
973
  f.setError(null);
852
- const r = await auth.signIn.magicLink({ email });
974
+ const r = await auth.signIn.magicLink({ email, captchaToken: challenge.token });
853
975
  f.setBusy(false);
976
+ challenge.renew();
854
977
  if (!r.ok) return f.setError(r.error);
855
978
  setStep("sent");
856
979
  };
@@ -866,9 +989,11 @@ function SignIn(props) {
866
989
  ] });
867
990
  }
868
991
  if (step === "code") {
992
+ const to = phoneWay?.phone?.number ?? "";
869
993
  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 }),
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 }),
871
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) }),
872
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 }),
873
998
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
874
999
  ] });
@@ -897,6 +1022,7 @@ function SignIn(props) {
897
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
898
1023
  }
899
1024
  ),
1025
+ challenge.node,
900
1026
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signIn }),
901
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 }),
902
1028
  methods?.signUp !== false && props.links?.signUp && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: f.cx("footer"), children: [
@@ -928,12 +1054,14 @@ function SignUp(props) {
928
1054
  const [email, setEmail] = (0, import_react.useState)("");
929
1055
  const [password, setPassword] = (0, import_react.useState)("");
930
1056
  const [sent, setSent] = (0, import_react.useState)(false);
1057
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
931
1058
  const submit = async (e) => {
932
1059
  e.preventDefault();
933
1060
  f.setBusy(true);
934
1061
  f.setError(null);
935
- const r = await auth.signUp.email({ name, email, password });
1062
+ const r = await auth.signUp.email({ name, email, password, captchaToken: challenge.token });
936
1063
  f.setBusy(false);
1064
+ challenge.renew();
937
1065
  if (!r.ok) return f.setError(r.error);
938
1066
  if (r.status === "verification_required") return setSent(true);
939
1067
  f.done();
@@ -950,6 +1078,7 @@ function SignUp(props) {
950
1078
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
951
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 }),
952
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,
953
1082
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signUp }),
954
1083
  props.links?.signIn && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: f.cx("footer"), children: [
955
1084
  f.t.haveAccount,
@@ -963,12 +1092,14 @@ function ForgotPassword(props) {
963
1092
  const f = useForm(props);
964
1093
  const [email, setEmail] = (0, import_react.useState)("");
965
1094
  const [sent, setSent] = (0, import_react.useState)(false);
1095
+ const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
966
1096
  const submit = async (e) => {
967
1097
  e.preventDefault();
968
1098
  f.setBusy(true);
969
1099
  f.setError(null);
970
- const r = await auth.forgotPassword({ email });
1100
+ const r = await auth.forgotPassword({ email, captchaToken: challenge.token });
971
1101
  f.setBusy(false);
1102
+ challenge.renew();
972
1103
  if (!r.ok) return f.setError(r.error);
973
1104
  setSent(true);
974
1105
  };
@@ -982,6 +1113,7 @@ function ForgotPassword(props) {
982
1113
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
983
1114
  general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
984
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,
985
1117
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.sendLink }),
986
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 }) })
987
1119
  ] });
@@ -1081,6 +1213,7 @@ function Account(props) {
1081
1213
  account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
1082
1214
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
1083
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 }) }),
1084
1217
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
1085
1218
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
1086
1219
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
@@ -1310,6 +1443,77 @@ function SecondStepPart({ props, account, onChanged }) {
1310
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 })
1311
1444
  ] });
1312
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
+ }
1313
1517
  function SignOutEverywhere({ props }) {
1314
1518
  const auth = useAuth();
1315
1519
  const { f, run, general } = useSection(props);