cauthy-auth 0.11.0 → 0.12.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
@@ -212,6 +212,12 @@ type AuthField = 'email' | 'password' | 'newPassword' | 'name' | 'code';
212
212
 
213
213
  /** Whether this browser can use a passkey at all. */
214
214
  declare function canUsePasskeys(): boolean;
215
+ /**
216
+ * Whether this browser offers passkeys in the autofill of a field marked
217
+ * `autocomplete="username webauthn"`, so the person picks one without
218
+ * looking for a button.
219
+ */
220
+ declare function canAutofillPasskeys(): Promise<boolean>;
215
221
 
216
222
  /** What went wrong, in a form the screen can show. */
217
223
  interface AuthError {
@@ -426,6 +432,34 @@ interface AuthClient {
426
432
  }): Promise<AuthResult<'signed_in', {
427
433
  session: PublicSession;
428
434
  }> | AuthResult<'redirected'>>;
435
+ /**
436
+ * Offers the person's passkeys in the browser's autofill of the email
437
+ * field, which must carry `autocomplete="username webauthn"`, and signs in
438
+ * with the one they pick. It waits until they pick one or `signal`
439
+ * aborts: abort it before calling `passkey` or sending the form.
440
+ * `cancelled`: aborted, or this browser or page cannot (then there is
441
+ * nothing to offer, and the passkey button still works).
442
+ */
443
+ passkeyAutofill(options?: {
444
+ signal?: AbortSignal;
445
+ }): Promise<AuthResult<'signed_in', {
446
+ session: PublicSession;
447
+ }>>;
448
+ /**
449
+ * Google One Tap: Google's prompt over the page offers the Google account
450
+ * the browser already has, and signs in with it in one tap, as the Google
451
+ * sign-in does (the same account). Needs the workspace's Google sign-in,
452
+ * and the page's address among the Google client's authorized JavaScript
453
+ * origins. Waits until the person picks an account or closes the prompt
454
+ * (`cancelled`), or `signal` aborts. `one_tap_unavailable`: the workspace
455
+ * has no Google sign-in.
456
+ */
457
+ googleOneTap(options?: {
458
+ signal?: AbortSignal;
459
+ context?: 'signin' | 'signup' | 'use';
460
+ }): Promise<AuthResult<'signed_in', {
461
+ session: PublicSession;
462
+ }>>;
429
463
  /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
430
464
  passkeyUrl(options?: {
431
465
  returnTo?: string;
@@ -623,6 +657,21 @@ interface AuthFormProps {
623
657
  };
624
658
  }
625
659
  declare function SignIn(props: AuthFormProps): react.JSX.Element;
660
+ interface GoogleOneTapProps {
661
+ /** Where to go once signed in. Without it (and without `onSuccess`), the page stays and its session updates. */
662
+ returnTo?: string;
663
+ /** Called once signed in, instead of going to `returnTo`. */
664
+ onSuccess?: () => void;
665
+ /** The prompt's words: sign in (default), sign up, or use the application. */
666
+ context?: 'signin' | 'signup' | 'use';
667
+ }
668
+ /**
669
+ * Google One Tap: put it on any page, and a person who is not signed in sees
670
+ * Google's prompt with the Google account the browser has; one tap signs them
671
+ * in. It draws nothing of its own, and shows nothing to somebody signed in or
672
+ * where the workspace has no Google sign-in.
673
+ */
674
+ declare function GoogleOneTap(props: GoogleOneTapProps): null;
626
675
  declare function SignUp(props: AuthFormProps): react.JSX.Element;
627
676
  declare function ForgotPassword(props: AuthFormProps): react.JSX.Element;
628
677
  /** The page a "choose a new password" link opens: `pages.resetPassword`. */
@@ -685,4 +734,4 @@ declare function useSession(): SessionContext;
685
734
  */
686
735
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
687
736
 
688
- 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, 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, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
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 };
package/dist/react.d.ts CHANGED
@@ -212,6 +212,12 @@ type AuthField = 'email' | 'password' | 'newPassword' | 'name' | 'code';
212
212
 
213
213
  /** Whether this browser can use a passkey at all. */
214
214
  declare function canUsePasskeys(): boolean;
215
+ /**
216
+ * Whether this browser offers passkeys in the autofill of a field marked
217
+ * `autocomplete="username webauthn"`, so the person picks one without
218
+ * looking for a button.
219
+ */
220
+ declare function canAutofillPasskeys(): Promise<boolean>;
215
221
 
216
222
  /** What went wrong, in a form the screen can show. */
217
223
  interface AuthError {
@@ -426,6 +432,34 @@ interface AuthClient {
426
432
  }): Promise<AuthResult<'signed_in', {
427
433
  session: PublicSession;
428
434
  }> | AuthResult<'redirected'>>;
435
+ /**
436
+ * Offers the person's passkeys in the browser's autofill of the email
437
+ * field, which must carry `autocomplete="username webauthn"`, and signs in
438
+ * with the one they pick. It waits until they pick one or `signal`
439
+ * aborts: abort it before calling `passkey` or sending the form.
440
+ * `cancelled`: aborted, or this browser or page cannot (then there is
441
+ * nothing to offer, and the passkey button still works).
442
+ */
443
+ passkeyAutofill(options?: {
444
+ signal?: AbortSignal;
445
+ }): Promise<AuthResult<'signed_in', {
446
+ session: PublicSession;
447
+ }>>;
448
+ /**
449
+ * Google One Tap: Google's prompt over the page offers the Google account
450
+ * the browser already has, and signs in with it in one tap, as the Google
451
+ * sign-in does (the same account). Needs the workspace's Google sign-in,
452
+ * and the page's address among the Google client's authorized JavaScript
453
+ * origins. Waits until the person picks an account or closes the prompt
454
+ * (`cancelled`), or `signal` aborts. `one_tap_unavailable`: the workspace
455
+ * has no Google sign-in.
456
+ */
457
+ googleOneTap(options?: {
458
+ signal?: AbortSignal;
459
+ context?: 'signin' | 'signup' | 'use';
460
+ }): Promise<AuthResult<'signed_in', {
461
+ session: PublicSession;
462
+ }>>;
429
463
  /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
430
464
  passkeyUrl(options?: {
431
465
  returnTo?: string;
@@ -623,6 +657,21 @@ interface AuthFormProps {
623
657
  };
624
658
  }
625
659
  declare function SignIn(props: AuthFormProps): react.JSX.Element;
660
+ interface GoogleOneTapProps {
661
+ /** Where to go once signed in. Without it (and without `onSuccess`), the page stays and its session updates. */
662
+ returnTo?: string;
663
+ /** Called once signed in, instead of going to `returnTo`. */
664
+ onSuccess?: () => void;
665
+ /** The prompt's words: sign in (default), sign up, or use the application. */
666
+ context?: 'signin' | 'signup' | 'use';
667
+ }
668
+ /**
669
+ * Google One Tap: put it on any page, and a person who is not signed in sees
670
+ * Google's prompt with the Google account the browser has; one tap signs them
671
+ * in. It draws nothing of its own, and shows nothing to somebody signed in or
672
+ * where the workspace has no Google sign-in.
673
+ */
674
+ declare function GoogleOneTap(props: GoogleOneTapProps): null;
626
675
  declare function SignUp(props: AuthFormProps): react.JSX.Element;
627
676
  declare function ForgotPassword(props: AuthFormProps): react.JSX.Element;
628
677
  /** The page a "choose a new password" link opens: `pages.resetPassword`. */
@@ -685,4 +734,4 @@ declare function useSession(): SessionContext;
685
734
  */
686
735
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
687
736
 
688
- 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, 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, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
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 };
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(/\/+$/, "");
@@ -286,6 +315,66 @@ function createAuthClient(options = {}) {
286
315
  window.location.assign(passkeyUrl(o));
287
316
  return { ok: true, status: "redirected" };
288
317
  },
318
+ async passkeyAutofill(o = {}) {
319
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
320
+ const m = await methods();
321
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
322
+ for (; ; ) {
323
+ if (o.signal?.aborted) return cancelled;
324
+ const begun = await post("/sign-in/passkey/begin", {}, null);
325
+ if (!begun.ok) return begun;
326
+ const renew = new AbortController();
327
+ const stop = () => renew.abort();
328
+ o.signal?.addEventListener("abort", stop, { once: true });
329
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
330
+ let credential;
331
+ try {
332
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
333
+ } catch (e) {
334
+ if (o.signal?.aborted) return cancelled;
335
+ if (renew.signal.aborted) continue;
336
+ return { ok: false, error: ceremonyError(e) };
337
+ } finally {
338
+ clearTimeout(timer);
339
+ o.signal?.removeEventListener("abort", stop);
340
+ }
341
+ return post("/sign-in/passkey/finish", { credential }, null);
342
+ }
343
+ },
344
+ async googleOneTap(o = {}) {
345
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
346
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
347
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
348
+ if (!begun.ok) return begun;
349
+ let gis;
350
+ try {
351
+ gis = await loadGoogleIdentity();
352
+ } catch {
353
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
354
+ }
355
+ const credential = await new Promise((done) => {
356
+ o.signal?.addEventListener("abort", () => {
357
+ gis.cancel();
358
+ done(null);
359
+ }, { once: true });
360
+ gis.initialize({
361
+ client_id: begun.clientId,
362
+ nonce: begun.nonce,
363
+ context: o.context ?? "signin",
364
+ callback: (r) => done(r.credential ?? null),
365
+ auto_select: false,
366
+ cancel_on_tap_outside: true,
367
+ itp_support: true,
368
+ use_fedcm_for_prompt: true
369
+ });
370
+ gis.prompt((moment) => {
371
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
372
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
373
+ });
374
+ });
375
+ if (credential === null) return cancelled;
376
+ return post("/sign-in/google-one-tap", { credential }, null);
377
+ },
289
378
  socialUrl,
290
379
  social(provider, o) {
291
380
  window.location.assign(socialUrl(provider, o));
@@ -702,6 +791,8 @@ function SignIn(props) {
702
791
  const [password, setPassword] = (0, import_react.useState)("");
703
792
  const [code, setCode] = (0, import_react.useState)("");
704
793
  const [step, setStep] = (0, import_react.useState)("password");
794
+ const autofill = (0, import_react.useRef)(null);
795
+ const [offer, setOffer] = (0, import_react.useState)(0);
705
796
  (0, import_react.useEffect)(() => {
706
797
  void auth.methods().then(setMethods);
707
798
  const back = returnedError();
@@ -716,26 +807,46 @@ function SignIn(props) {
716
807
  setStep("password");
717
808
  });
718
809
  });
810
+ (0, import_react.useEffect)(() => {
811
+ if (step !== "password" || !methods?.passkeyHere) return;
812
+ const ctl = new AbortController();
813
+ autofill.current = ctl;
814
+ void auth.signIn.passkeyAutofill({ signal: ctl.signal }).then((r) => {
815
+ if (ctl.signal.aborted) return;
816
+ if (r.ok) return f.done();
817
+ if (r.error.code === "cancelled") return;
818
+ f.setError(r.error);
819
+ if (r.error.code === "passkey_failed" || r.error.code === "step_expired") setOffer((n) => n + 1);
820
+ });
821
+ return () => ctl.abort();
822
+ }, [step, methods, offer]);
719
823
  const submit = async (e) => {
720
824
  e.preventDefault();
825
+ autofill.current?.abort();
721
826
  f.setBusy(true);
722
827
  f.setError(null);
723
828
  const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
724
829
  f.setBusy(false);
725
- if (!r.ok) return f.setError(r.error);
830
+ if (!r.ok) {
831
+ setOffer((n) => n + 1);
832
+ return f.setError(r.error);
833
+ }
726
834
  if (r.status === "mfa_required") return setStep("code");
727
835
  f.done();
728
836
  };
729
837
  const usePasskey = async () => {
838
+ autofill.current?.abort();
730
839
  f.setBusy(true);
731
840
  f.setError(null);
732
841
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
733
842
  if (r.ok && r.status === "redirected") return;
734
843
  f.setBusy(false);
844
+ if (!r.ok) setOffer((n) => n + 1);
735
845
  if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
736
846
  f.done();
737
847
  };
738
848
  const sendLink = async () => {
849
+ autofill.current?.abort();
739
850
  f.setBusy(true);
740
851
  f.setError(null);
741
852
  const r = await auth.signIn.magicLink({ email });
@@ -771,7 +882,7 @@ function SignIn(props) {
771
882
  ] }, p.id)),
772
883
  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
884
  ((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 }),
885
+ /* @__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
886
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
776
887
  Field,
777
888
  {
@@ -795,6 +906,21 @@ function SignIn(props) {
795
906
  ] })
796
907
  ] });
797
908
  }
909
+ function GoogleOneTap(props) {
910
+ const auth = useAuth();
911
+ const unauthenticated = auth.status === "unauthenticated";
912
+ (0, import_react.useEffect)(() => {
913
+ if (!unauthenticated) return;
914
+ const ctl = new AbortController();
915
+ void auth.signIn.googleOneTap({ signal: ctl.signal, context: props.context }).then((r) => {
916
+ if (!r.ok || ctl.signal.aborted) return;
917
+ if (props.onSuccess) props.onSuccess();
918
+ else if (props.returnTo) window.location.assign(props.returnTo);
919
+ });
920
+ return () => ctl.abort();
921
+ }, [unauthenticated]);
922
+ return null;
923
+ }
798
924
  function SignUp(props) {
799
925
  const auth = useAuth();
800
926
  const f = useForm(props);
@@ -1286,7 +1412,16 @@ function useAuth() {
1286
1412
  };
1287
1413
  return {
1288
1414
  ...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)) },
1415
+ signIn: {
1416
+ ...client.signIn,
1417
+ email: (f) => then(client.signIn.email(f)),
1418
+ mfa: (f) => then(client.signIn.mfa(f)),
1419
+ magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
1420
+ emailCode: (f) => then(client.signIn.emailCode(f)),
1421
+ passkey: (o) => then(client.signIn.passkey(o)),
1422
+ passkeyAutofill: (o) => then(client.signIn.passkeyAutofill(o)),
1423
+ googleOneTap: (o) => then(client.signIn.googleOneTap(o))
1424
+ },
1290
1425
  signUp: { email: (f) => then(client.signUp.email(f)) },
1291
1426
  signOut: () => then(client.signOut()),
1292
1427
  account: {
@@ -1307,11 +1442,13 @@ function useAuth() {
1307
1442
  Account,
1308
1443
  CauthyProvider,
1309
1444
  ForgotPassword,
1445
+ GoogleOneTap,
1310
1446
  ResetPassword,
1311
1447
  SignIn,
1312
1448
  SignUp,
1313
1449
  VerifyEmail,
1314
1450
  can,
1451
+ canAutofillPasskeys,
1315
1452
  canUsePasskeys,
1316
1453
  createAuthClient,
1317
1454
  definePolicy,