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.mjs CHANGED
@@ -7,6 +7,15 @@ import { createContext, useCallback, useContext, useEffect as useEffect2, useMem
7
7
  function canUsePasskeys() {
8
8
  return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
9
9
  }
10
+ async function canAutofillPasskeys() {
11
+ if (!canUsePasskeys()) return false;
12
+ const pkc = window.PublicKeyCredential;
13
+ try {
14
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
10
19
  function bytes(text2) {
11
20
  const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
12
21
  const bin = atob(b64);
@@ -22,9 +31,9 @@ function text(buf) {
22
31
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
23
32
  }
24
33
  var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
25
- async function getPasskey(options) {
34
+ async function getPasskey(options, how = {}) {
26
35
  const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
27
- const cred = await navigator.credentials.get({ publicKey });
36
+ const cred = await navigator.credentials.get({ publicKey, ...how });
28
37
  if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
29
38
  const r = cred.response;
30
39
  return {
@@ -168,6 +177,24 @@ function firstProblem(...checks) {
168
177
  }
169
178
 
170
179
  // src/client.ts
180
+ var googleScript = null;
181
+ function loadGoogleIdentity() {
182
+ const ready = () => window.google?.accounts?.id;
183
+ if (ready()) return Promise.resolve(ready());
184
+ googleScript ??= new Promise((resolve, reject) => {
185
+ const script = document.createElement("script");
186
+ script.src = "https://accounts.google.com/gsi/client";
187
+ script.async = true;
188
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
189
+ script.onerror = () => {
190
+ googleScript = null;
191
+ reject(new Error("google identity services did not load"));
192
+ };
193
+ document.head.appendChild(script);
194
+ });
195
+ return googleScript;
196
+ }
197
+ var PASSKEY_SECONDS = 300;
171
198
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
172
199
  function createAuthClient(options = {}) {
173
200
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -251,6 +278,66 @@ function createAuthClient(options = {}) {
251
278
  window.location.assign(passkeyUrl(o));
252
279
  return { ok: true, status: "redirected" };
253
280
  },
281
+ async passkeyAutofill(o = {}) {
282
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
283
+ const m = await methods();
284
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
285
+ for (; ; ) {
286
+ if (o.signal?.aborted) return cancelled;
287
+ const begun = await post("/sign-in/passkey/begin", {}, null);
288
+ if (!begun.ok) return begun;
289
+ const renew = new AbortController();
290
+ const stop = () => renew.abort();
291
+ o.signal?.addEventListener("abort", stop, { once: true });
292
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
293
+ let credential;
294
+ try {
295
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
296
+ } catch (e) {
297
+ if (o.signal?.aborted) return cancelled;
298
+ if (renew.signal.aborted) continue;
299
+ return { ok: false, error: ceremonyError(e) };
300
+ } finally {
301
+ clearTimeout(timer);
302
+ o.signal?.removeEventListener("abort", stop);
303
+ }
304
+ return post("/sign-in/passkey/finish", { credential }, null);
305
+ }
306
+ },
307
+ async googleOneTap(o = {}) {
308
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
309
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
310
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
311
+ if (!begun.ok) return begun;
312
+ let gis;
313
+ try {
314
+ gis = await loadGoogleIdentity();
315
+ } catch {
316
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
317
+ }
318
+ const credential = await new Promise((done) => {
319
+ o.signal?.addEventListener("abort", () => {
320
+ gis.cancel();
321
+ done(null);
322
+ }, { once: true });
323
+ gis.initialize({
324
+ client_id: begun.clientId,
325
+ nonce: begun.nonce,
326
+ context: o.context ?? "signin",
327
+ callback: (r) => done(r.credential ?? null),
328
+ auto_select: false,
329
+ cancel_on_tap_outside: true,
330
+ itp_support: true,
331
+ use_fedcm_for_prompt: true
332
+ });
333
+ gis.prompt((moment) => {
334
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
335
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
336
+ });
337
+ });
338
+ if (credential === null) return cancelled;
339
+ return post("/sign-in/google-one-tap", { credential }, null);
340
+ },
254
341
  socialUrl,
255
342
  social(provider, o) {
256
343
  window.location.assign(socialUrl(provider, o));
@@ -667,6 +754,8 @@ function SignIn(props) {
667
754
  const [password, setPassword] = useState("");
668
755
  const [code, setCode] = useState("");
669
756
  const [step, setStep] = useState("password");
757
+ const autofill = useRef(null);
758
+ const [offer, setOffer] = useState(0);
670
759
  useEffect(() => {
671
760
  void auth.methods().then(setMethods);
672
761
  const back = returnedError();
@@ -681,26 +770,46 @@ function SignIn(props) {
681
770
  setStep("password");
682
771
  });
683
772
  });
773
+ useEffect(() => {
774
+ if (step !== "password" || !methods?.passkeyHere) return;
775
+ const ctl = new AbortController();
776
+ autofill.current = ctl;
777
+ void auth.signIn.passkeyAutofill({ signal: ctl.signal }).then((r) => {
778
+ if (ctl.signal.aborted) return;
779
+ if (r.ok) return f.done();
780
+ if (r.error.code === "cancelled") return;
781
+ f.setError(r.error);
782
+ if (r.error.code === "passkey_failed" || r.error.code === "step_expired") setOffer((n) => n + 1);
783
+ });
784
+ return () => ctl.abort();
785
+ }, [step, methods, offer]);
684
786
  const submit = async (e) => {
685
787
  e.preventDefault();
788
+ autofill.current?.abort();
686
789
  f.setBusy(true);
687
790
  f.setError(null);
688
791
  const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
689
792
  f.setBusy(false);
690
- if (!r.ok) return f.setError(r.error);
793
+ if (!r.ok) {
794
+ setOffer((n) => n + 1);
795
+ return f.setError(r.error);
796
+ }
691
797
  if (r.status === "mfa_required") return setStep("code");
692
798
  f.done();
693
799
  };
694
800
  const usePasskey = async () => {
801
+ autofill.current?.abort();
695
802
  f.setBusy(true);
696
803
  f.setError(null);
697
804
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
698
805
  if (r.ok && r.status === "redirected") return;
699
806
  f.setBusy(false);
807
+ if (!r.ok) setOffer((n) => n + 1);
700
808
  if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
701
809
  f.done();
702
810
  };
703
811
  const sendLink = async () => {
812
+ autofill.current?.abort();
704
813
  f.setBusy(true);
705
814
  f.setError(null);
706
815
  const r = await auth.signIn.magicLink({ email });
@@ -736,7 +845,7 @@ function SignIn(props) {
736
845
  ] }, p.id)),
737
846
  methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
738
847
  ((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ jsx("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
739
- /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
848
+ /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: methods?.passkeyHere ? "username webauthn" : "username", required: true, onChange: setEmail }),
740
849
  /* @__PURE__ */ jsx(
741
850
  Field,
742
851
  {
@@ -760,6 +869,21 @@ function SignIn(props) {
760
869
  ] })
761
870
  ] });
762
871
  }
872
+ function GoogleOneTap(props) {
873
+ const auth = useAuth();
874
+ const unauthenticated = auth.status === "unauthenticated";
875
+ useEffect(() => {
876
+ if (!unauthenticated) return;
877
+ const ctl = new AbortController();
878
+ void auth.signIn.googleOneTap({ signal: ctl.signal, context: props.context }).then((r) => {
879
+ if (!r.ok || ctl.signal.aborted) return;
880
+ if (props.onSuccess) props.onSuccess();
881
+ else if (props.returnTo) window.location.assign(props.returnTo);
882
+ });
883
+ return () => ctl.abort();
884
+ }, [unauthenticated]);
885
+ return null;
886
+ }
763
887
  function SignUp(props) {
764
888
  const auth = useAuth();
765
889
  const f = useForm(props);
@@ -1251,7 +1375,16 @@ function useAuth() {
1251
1375
  };
1252
1376
  return {
1253
1377
  ...client,
1254
- signIn: { ...client.signIn, email: (f) => then(client.signIn.email(f)), mfa: (f) => then(client.signIn.mfa(f)), magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)), emailCode: (f) => then(client.signIn.emailCode(f)) },
1378
+ signIn: {
1379
+ ...client.signIn,
1380
+ email: (f) => then(client.signIn.email(f)),
1381
+ mfa: (f) => then(client.signIn.mfa(f)),
1382
+ magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
1383
+ emailCode: (f) => then(client.signIn.emailCode(f)),
1384
+ passkey: (o) => then(client.signIn.passkey(o)),
1385
+ passkeyAutofill: (o) => then(client.signIn.passkeyAutofill(o)),
1386
+ googleOneTap: (o) => then(client.signIn.googleOneTap(o))
1387
+ },
1255
1388
  signUp: { email: (f) => then(client.signUp.email(f)) },
1256
1389
  signOut: () => then(client.signOut()),
1257
1390
  account: {
@@ -1271,11 +1404,13 @@ export {
1271
1404
  Account,
1272
1405
  CauthyProvider,
1273
1406
  ForgotPassword,
1407
+ GoogleOneTap,
1274
1408
  ResetPassword,
1275
1409
  SignIn,
1276
1410
  SignUp,
1277
1411
  VerifyEmail,
1278
1412
  can,
1413
+ canAutofillPasskeys,
1279
1414
  canUsePasskeys,
1280
1415
  createAuthClient,
1281
1416
  definePolicy,