cauthy-auth 0.10.0 → 0.11.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
@@ -3,6 +3,73 @@
3
3
  // src/react.tsx
4
4
  import { createContext, useCallback, useContext, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
5
5
 
6
+ // src/passkey.ts
7
+ function canUsePasskeys() {
8
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
9
+ }
10
+ function bytes(text2) {
11
+ const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
12
+ const bin = atob(b64);
13
+ const out = new Uint8Array(bin.length);
14
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
15
+ return out.buffer;
16
+ }
17
+ function text(buf) {
18
+ if (!buf) return null;
19
+ const view = new Uint8Array(buf);
20
+ let bin = "";
21
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
22
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
23
+ }
24
+ var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
25
+ async function getPasskey(options) {
26
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
27
+ const cred = await navigator.credentials.get({ publicKey });
28
+ if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
29
+ const r = cred.response;
30
+ return {
31
+ id: cred.id,
32
+ rawId: text(cred.rawId),
33
+ type: cred.type,
34
+ response: {
35
+ authenticatorData: text(r.authenticatorData),
36
+ clientDataJSON: text(r.clientDataJSON),
37
+ signature: text(r.signature),
38
+ userHandle: text(r.userHandle)
39
+ },
40
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
41
+ };
42
+ }
43
+ async function makePasskey(options) {
44
+ const publicKey = {
45
+ ...options,
46
+ challenge: bytes(options.challenge),
47
+ user: { ...options.user, id: bytes(options.user.id) },
48
+ excludeCredentials: withId(options.excludeCredentials)
49
+ };
50
+ const cred = await navigator.credentials.create({ publicKey });
51
+ if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
52
+ const r = cred.response;
53
+ return {
54
+ id: cred.id,
55
+ rawId: text(cred.rawId),
56
+ type: cred.type,
57
+ response: {
58
+ clientDataJSON: text(r.clientDataJSON),
59
+ attestationObject: text(r.attestationObject),
60
+ transports: typeof r.getTransports === "function" ? r.getTransports() : []
61
+ },
62
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
63
+ };
64
+ }
65
+ function ceremonyError(e) {
66
+ const name = e instanceof Error || e instanceof DOMException ? e.name : "";
67
+ if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
68
+ if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
69
+ if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
70
+ return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
71
+ }
72
+
6
73
  // src/theme.ts
7
74
  function themeVariables(theme) {
8
75
  const vars = {};
@@ -143,6 +210,28 @@ function createAuthClient(options = {}) {
143
210
  if (o.locale) q.set("locale", o.locale);
144
211
  return `${base}/login?${q}`;
145
212
  };
213
+ async function methods() {
214
+ try {
215
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
216
+ if (res.ok) {
217
+ const m = await res.json();
218
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
219
+ }
220
+ } catch {
221
+ }
222
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
223
+ }
224
+ async function ceremony(begin, finish, form, run, more = {}) {
225
+ const begun = await post(begin, form, null);
226
+ if (!begun.ok) return begun;
227
+ let credential;
228
+ try {
229
+ credential = await run(begun.options);
230
+ } catch (e) {
231
+ return { ok: false, error: ceremonyError(e) };
232
+ }
233
+ return post(finish, { credential, ...more }, null);
234
+ }
146
235
  return {
147
236
  signIn: {
148
237
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -151,8 +240,16 @@ function createAuthClient(options = {}) {
151
240
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
152
241
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
153
242
  passkeyUrl,
154
- passkey(o) {
243
+ async passkey(o) {
244
+ const m = await methods();
245
+ if (m.passkeyHere && canUsePasskeys()) {
246
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
247
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
248
+ } else if (!m.passkeyOnCauthy) {
249
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
250
+ }
155
251
  window.location.assign(passkeyUrl(o));
252
+ return { ok: true, status: "redirected" };
156
253
  },
157
254
  socialUrl,
158
255
  social(provider, o) {
@@ -196,7 +293,16 @@ function createAuthClient(options = {}) {
196
293
  const body = await read("/me/sessions");
197
294
  return body && Array.isArray(body.sessions) ? body.sessions : [];
198
295
  },
199
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
296
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
297
+ async passkeys() {
298
+ const body = await read("/me/passkeys");
299
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
300
+ },
301
+ addPasskey(f = {}) {
302
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
303
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
304
+ },
305
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
200
306
  },
201
307
  async getSession() {
202
308
  try {
@@ -211,14 +317,7 @@ function createAuthClient(options = {}) {
211
317
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
212
318
  return looked;
213
319
  },
214
- async methods() {
215
- try {
216
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
217
- if (res.ok) return await res.json();
218
- } catch {
219
- }
220
- return { providers: [], signUp: true, magicLink: false, passkey: false };
221
- }
320
+ methods
222
321
  };
223
322
  }
224
323
 
@@ -325,6 +424,13 @@ var TEXTS = {
325
424
  keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
326
425
  passkeys: "Passkeys",
327
426
  managePasskeys: "Add or remove a passkey",
427
+ passkeysHelp: "Sign in with your fingerprint, your face or your device's lock, without a password.",
428
+ noPasskeys: "No passkey yet.",
429
+ passkeyName: "A name for this device (optional)",
430
+ addPasskey: "Add a passkey on this device",
431
+ removePasskey: "Remove",
432
+ passkeyAdded: "The passkey was added. Next time, sign in with it.",
433
+ passkeyUnnamed: "Passkey",
328
434
  sessions: "Where you are signed in",
329
435
  signOutEverywhere: "Sign out everywhere",
330
436
  unknownDevice: "Unknown device",
@@ -355,7 +461,10 @@ var TEXTS = {
355
461
  wrong_password: "The current password is not right.",
356
462
  step_up_required: "Enter your current password.",
357
463
  weak_password: "Choose a stronger password.",
358
- unauthenticated: "Sign in first."
464
+ unauthenticated: "Sign in first.",
465
+ passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
466
+ passkey_exists: "This device already has a passkey for your account.",
467
+ passkey_unavailable: "Passkeys cannot be used here."
359
468
  }
360
469
  },
361
470
  es: {
@@ -404,6 +513,13 @@ var TEXTS = {
404
513
  keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
405
514
  passkeys: "Llaves de acceso",
406
515
  managePasskeys: "Agregar o quitar una llave de acceso",
516
+ passkeysHelp: "Entra con tu huella, tu rostro o el bloqueo de tu dispositivo, sin contrase\xF1a.",
517
+ noPasskeys: "Todav\xEDa no tienes llaves de acceso.",
518
+ passkeyName: "Un nombre para este dispositivo (opcional)",
519
+ addPasskey: "Agregar una llave en este dispositivo",
520
+ removePasskey: "Quitar",
521
+ passkeyAdded: "La llave de acceso qued\xF3 agregada. La pr\xF3xima vez, entra con ella.",
522
+ passkeyUnnamed: "Llave de acceso",
407
523
  sessions: "D\xF3nde tienes la sesi\xF3n abierta",
408
524
  signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
409
525
  unknownDevice: "Dispositivo desconocido",
@@ -434,7 +550,10 @@ var TEXTS = {
434
550
  wrong_password: "La contrase\xF1a actual no es correcta.",
435
551
  step_up_required: "Escribe tu contrase\xF1a actual.",
436
552
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
437
- unauthenticated: "Inicia sesi\xF3n primero."
553
+ unauthenticated: "Inicia sesi\xF3n primero.",
554
+ passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
555
+ passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
556
+ passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
438
557
  }
439
558
  }
440
559
  };
@@ -572,6 +691,15 @@ function SignIn(props) {
572
691
  if (r.status === "mfa_required") return setStep("code");
573
692
  f.done();
574
693
  };
694
+ const usePasskey = async () => {
695
+ f.setBusy(true);
696
+ f.setError(null);
697
+ const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
698
+ if (r.ok && r.status === "redirected") return;
699
+ f.setBusy(false);
700
+ if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
701
+ f.done();
702
+ };
575
703
  const sendLink = async () => {
576
704
  f.setBusy(true);
577
705
  f.setError(null);
@@ -606,7 +734,7 @@ function SignIn(props) {
606
734
  " ",
607
735
  p.name
608
736
  ] }, p.id)),
609
- methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () => auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale }), children: f.t.usePasskey }),
737
+ methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
610
738
  ((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ jsx("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
611
739
  /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
612
740
  /* @__PURE__ */ jsx(
@@ -792,10 +920,7 @@ function Account(props) {
792
920
  account.hasPassword && /* @__PURE__ */ jsx(PasswordPart, { props }),
793
921
  /* @__PURE__ */ jsx(LinkedPart, { props }),
794
922
  /* @__PURE__ */ jsx(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
795
- /* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
796
- /* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.passkeys }),
797
- /* @__PURE__ */ jsx("a", { className: cx("link"), href: accountUrl(), children: t.managePasskeys })
798
- ] }),
923
+ /* @__PURE__ */ jsx(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
799
924
  /* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
800
925
  /* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.sessions }),
801
926
  /* @__PURE__ */ jsx("ul", { className: cx("sessions"), children: sessions.map((s, i) => /* @__PURE__ */ jsxs("li", { className: cx("session"), children: [
@@ -809,6 +934,60 @@ function Account(props) {
809
934
  ] })
810
935
  ] });
811
936
  }
937
+ function PasskeysPart({ props, account, accountUrl, when }) {
938
+ const auth = useAuth();
939
+ const { f, done, setDone, run, general } = useSection(props);
940
+ const [here, setHere] = useState(null);
941
+ const [list, setList] = useState([]);
942
+ const [name, setName] = useState("");
943
+ const [password, setPassword] = useState("");
944
+ useEffect(() => {
945
+ void auth.methods().then((m) => {
946
+ const can2 = m.passkeyHere && canUsePasskeys();
947
+ setHere(can2);
948
+ if (can2) void auth.account.passkeys().then(setList);
949
+ });
950
+ }, []);
951
+ if (here === null) return null;
952
+ if (!here) {
953
+ return /* @__PURE__ */ jsxs("section", { className: f.cx("section"), children: [
954
+ /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.passkeys }),
955
+ /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: accountUrl, children: f.t.managePasskeys })
956
+ ] });
957
+ }
958
+ const add = async (e) => {
959
+ e.preventDefault();
960
+ const r = await run(() => auth.account.addPasskey({ name, password }));
961
+ if (!r.ok) {
962
+ if (r.error.code === "cancelled") f.setError(null);
963
+ return;
964
+ }
965
+ setList([...list, r.passkey]);
966
+ setName("");
967
+ setPassword("");
968
+ setDone(f.t.passkeyAdded);
969
+ };
970
+ const remove = async (id) => {
971
+ const r = await run(() => auth.account.removePasskey(id));
972
+ if (r.ok) setList(list.filter((p) => p.id !== id));
973
+ };
974
+ return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
975
+ /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.passkeys }),
976
+ /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.passkeysHelp }),
977
+ general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
978
+ list.length === 0 ? /* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.noPasskeys }) : /* @__PURE__ */ jsx("ul", { className: f.cx("sessions"), children: list.map((p) => /* @__PURE__ */ jsxs("li", { className: f.cx("session"), children: [
979
+ p.name || f.t.passkeyUnnamed,
980
+ " \xB7 ",
981
+ when(p.createdAt),
982
+ " ",
983
+ /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void remove(p.id), children: f.t.removePasskey })
984
+ ] }, p.id)) }),
985
+ /* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.passkeyName, value: name, autoComplete: "off", onChange: setName }),
986
+ account.hasPassword && /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
987
+ /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.addPasskey }),
988
+ done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
989
+ ] });
990
+ }
812
991
  function ProfilePart({ props, account, onSaved }) {
813
992
  const auth = useAuth();
814
993
  const { f, done, setDone, run, general } = useSection(props);
@@ -1097,6 +1276,7 @@ export {
1097
1276
  SignUp,
1098
1277
  VerifyEmail,
1099
1278
  can,
1279
+ canUsePasskeys,
1100
1280
  createAuthClient,
1101
1281
  definePolicy,
1102
1282
  useAuth,