cauthy-auth 0.10.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/README.md +3 -2
- package/dist/cli.js +299 -20
- package/dist/client.d.mts +88 -5
- package/dist/client.d.ts +88 -5
- package/dist/client.js +202 -11
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +200 -11
- package/dist/client.mjs.map +1 -1
- package/dist/index.js +128 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +128 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +110 -5
- package/dist/react.d.ts +110 -5
- package/dist/react.js +338 -20
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +335 -20
- package/dist/react.mjs.map +1 -1
- package/dist/svelte.d.mts +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.js +198 -11
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +198 -11
- package/dist/svelte.mjs.map +1 -1
- package/dist/vue.d.mts +1 -1
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +198 -11
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +198 -11
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.mjs
CHANGED
|
@@ -3,6 +3,82 @@
|
|
|
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
|
+
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
|
+
}
|
|
19
|
+
function bytes(text2) {
|
|
20
|
+
const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
|
|
21
|
+
const bin = atob(b64);
|
|
22
|
+
const out = new Uint8Array(bin.length);
|
|
23
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
24
|
+
return out.buffer;
|
|
25
|
+
}
|
|
26
|
+
function text(buf) {
|
|
27
|
+
if (!buf) return null;
|
|
28
|
+
const view = new Uint8Array(buf);
|
|
29
|
+
let bin = "";
|
|
30
|
+
for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
|
|
31
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
32
|
+
}
|
|
33
|
+
var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
|
|
34
|
+
async function getPasskey(options, how = {}) {
|
|
35
|
+
const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
|
|
36
|
+
const cred = await navigator.credentials.get({ publicKey, ...how });
|
|
37
|
+
if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
|
|
38
|
+
const r = cred.response;
|
|
39
|
+
return {
|
|
40
|
+
id: cred.id,
|
|
41
|
+
rawId: text(cred.rawId),
|
|
42
|
+
type: cred.type,
|
|
43
|
+
response: {
|
|
44
|
+
authenticatorData: text(r.authenticatorData),
|
|
45
|
+
clientDataJSON: text(r.clientDataJSON),
|
|
46
|
+
signature: text(r.signature),
|
|
47
|
+
userHandle: text(r.userHandle)
|
|
48
|
+
},
|
|
49
|
+
clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function makePasskey(options) {
|
|
53
|
+
const publicKey = {
|
|
54
|
+
...options,
|
|
55
|
+
challenge: bytes(options.challenge),
|
|
56
|
+
user: { ...options.user, id: bytes(options.user.id) },
|
|
57
|
+
excludeCredentials: withId(options.excludeCredentials)
|
|
58
|
+
};
|
|
59
|
+
const cred = await navigator.credentials.create({ publicKey });
|
|
60
|
+
if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
|
|
61
|
+
const r = cred.response;
|
|
62
|
+
return {
|
|
63
|
+
id: cred.id,
|
|
64
|
+
rawId: text(cred.rawId),
|
|
65
|
+
type: cred.type,
|
|
66
|
+
response: {
|
|
67
|
+
clientDataJSON: text(r.clientDataJSON),
|
|
68
|
+
attestationObject: text(r.attestationObject),
|
|
69
|
+
transports: typeof r.getTransports === "function" ? r.getTransports() : []
|
|
70
|
+
},
|
|
71
|
+
clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function ceremonyError(e) {
|
|
75
|
+
const name = e instanceof Error || e instanceof DOMException ? e.name : "";
|
|
76
|
+
if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
|
|
77
|
+
if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
|
|
78
|
+
if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
|
|
79
|
+
return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
|
|
80
|
+
}
|
|
81
|
+
|
|
6
82
|
// src/theme.ts
|
|
7
83
|
function themeVariables(theme) {
|
|
8
84
|
const vars = {};
|
|
@@ -101,6 +177,24 @@ function firstProblem(...checks) {
|
|
|
101
177
|
}
|
|
102
178
|
|
|
103
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;
|
|
104
198
|
var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
|
|
105
199
|
function createAuthClient(options = {}) {
|
|
106
200
|
const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
|
|
@@ -143,6 +237,28 @@ function createAuthClient(options = {}) {
|
|
|
143
237
|
if (o.locale) q.set("locale", o.locale);
|
|
144
238
|
return `${base}/login?${q}`;
|
|
145
239
|
};
|
|
240
|
+
async function methods() {
|
|
241
|
+
try {
|
|
242
|
+
const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
243
|
+
if (res.ok) {
|
|
244
|
+
const m = await res.json();
|
|
245
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
|
|
250
|
+
}
|
|
251
|
+
async function ceremony(begin, finish, form, run, more = {}) {
|
|
252
|
+
const begun = await post(begin, form, null);
|
|
253
|
+
if (!begun.ok) return begun;
|
|
254
|
+
let credential;
|
|
255
|
+
try {
|
|
256
|
+
credential = await run(begun.options);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
return { ok: false, error: ceremonyError(e) };
|
|
259
|
+
}
|
|
260
|
+
return post(finish, { credential, ...more }, null);
|
|
261
|
+
}
|
|
146
262
|
return {
|
|
147
263
|
signIn: {
|
|
148
264
|
email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
|
|
@@ -151,8 +267,76 @@ function createAuthClient(options = {}) {
|
|
|
151
267
|
magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
|
|
152
268
|
emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
|
|
153
269
|
passkeyUrl,
|
|
154
|
-
passkey(o) {
|
|
270
|
+
async passkey(o) {
|
|
271
|
+
const m = await methods();
|
|
272
|
+
if (m.passkeyHere && canUsePasskeys()) {
|
|
273
|
+
const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
|
|
274
|
+
if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
|
|
275
|
+
} else if (!m.passkeyOnCauthy) {
|
|
276
|
+
return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
|
|
277
|
+
}
|
|
155
278
|
window.location.assign(passkeyUrl(o));
|
|
279
|
+
return { ok: true, status: "redirected" };
|
|
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);
|
|
156
340
|
},
|
|
157
341
|
socialUrl,
|
|
158
342
|
social(provider, o) {
|
|
@@ -196,7 +380,16 @@ function createAuthClient(options = {}) {
|
|
|
196
380
|
const body = await read("/me/sessions");
|
|
197
381
|
return body && Array.isArray(body.sessions) ? body.sessions : [];
|
|
198
382
|
},
|
|
199
|
-
signOutEverywhere: () => post("/me/sessions/end", {}, null)
|
|
383
|
+
signOutEverywhere: () => post("/me/sessions/end", {}, null),
|
|
384
|
+
async passkeys() {
|
|
385
|
+
const body = await read("/me/passkeys");
|
|
386
|
+
return body && Array.isArray(body.passkeys) ? body.passkeys : [];
|
|
387
|
+
},
|
|
388
|
+
addPasskey(f = {}) {
|
|
389
|
+
if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
|
|
390
|
+
return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
|
|
391
|
+
},
|
|
392
|
+
removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
|
|
200
393
|
},
|
|
201
394
|
async getSession() {
|
|
202
395
|
try {
|
|
@@ -211,14 +404,7 @@ function createAuthClient(options = {}) {
|
|
|
211
404
|
looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
|
|
212
405
|
return looked;
|
|
213
406
|
},
|
|
214
|
-
|
|
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
|
-
}
|
|
407
|
+
methods
|
|
222
408
|
};
|
|
223
409
|
}
|
|
224
410
|
|
|
@@ -325,6 +511,13 @@ var TEXTS = {
|
|
|
325
511
|
keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
|
|
326
512
|
passkeys: "Passkeys",
|
|
327
513
|
managePasskeys: "Add or remove a passkey",
|
|
514
|
+
passkeysHelp: "Sign in with your fingerprint, your face or your device's lock, without a password.",
|
|
515
|
+
noPasskeys: "No passkey yet.",
|
|
516
|
+
passkeyName: "A name for this device (optional)",
|
|
517
|
+
addPasskey: "Add a passkey on this device",
|
|
518
|
+
removePasskey: "Remove",
|
|
519
|
+
passkeyAdded: "The passkey was added. Next time, sign in with it.",
|
|
520
|
+
passkeyUnnamed: "Passkey",
|
|
328
521
|
sessions: "Where you are signed in",
|
|
329
522
|
signOutEverywhere: "Sign out everywhere",
|
|
330
523
|
unknownDevice: "Unknown device",
|
|
@@ -355,7 +548,10 @@ var TEXTS = {
|
|
|
355
548
|
wrong_password: "The current password is not right.",
|
|
356
549
|
step_up_required: "Enter your current password.",
|
|
357
550
|
weak_password: "Choose a stronger password.",
|
|
358
|
-
unauthenticated: "Sign in first."
|
|
551
|
+
unauthenticated: "Sign in first.",
|
|
552
|
+
passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
|
|
553
|
+
passkey_exists: "This device already has a passkey for your account.",
|
|
554
|
+
passkey_unavailable: "Passkeys cannot be used here."
|
|
359
555
|
}
|
|
360
556
|
},
|
|
361
557
|
es: {
|
|
@@ -404,6 +600,13 @@ var TEXTS = {
|
|
|
404
600
|
keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
|
|
405
601
|
passkeys: "Llaves de acceso",
|
|
406
602
|
managePasskeys: "Agregar o quitar una llave de acceso",
|
|
603
|
+
passkeysHelp: "Entra con tu huella, tu rostro o el bloqueo de tu dispositivo, sin contrase\xF1a.",
|
|
604
|
+
noPasskeys: "Todav\xEDa no tienes llaves de acceso.",
|
|
605
|
+
passkeyName: "Un nombre para este dispositivo (opcional)",
|
|
606
|
+
addPasskey: "Agregar una llave en este dispositivo",
|
|
607
|
+
removePasskey: "Quitar",
|
|
608
|
+
passkeyAdded: "La llave de acceso qued\xF3 agregada. La pr\xF3xima vez, entra con ella.",
|
|
609
|
+
passkeyUnnamed: "Llave de acceso",
|
|
407
610
|
sessions: "D\xF3nde tienes la sesi\xF3n abierta",
|
|
408
611
|
signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
|
|
409
612
|
unknownDevice: "Dispositivo desconocido",
|
|
@@ -434,7 +637,10 @@ var TEXTS = {
|
|
|
434
637
|
wrong_password: "La contrase\xF1a actual no es correcta.",
|
|
435
638
|
step_up_required: "Escribe tu contrase\xF1a actual.",
|
|
436
639
|
weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
|
|
437
|
-
unauthenticated: "Inicia sesi\xF3n primero."
|
|
640
|
+
unauthenticated: "Inicia sesi\xF3n primero.",
|
|
641
|
+
passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
|
|
642
|
+
passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
|
|
643
|
+
passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
|
|
438
644
|
}
|
|
439
645
|
}
|
|
440
646
|
};
|
|
@@ -548,6 +754,8 @@ function SignIn(props) {
|
|
|
548
754
|
const [password, setPassword] = useState("");
|
|
549
755
|
const [code, setCode] = useState("");
|
|
550
756
|
const [step, setStep] = useState("password");
|
|
757
|
+
const autofill = useRef(null);
|
|
758
|
+
const [offer, setOffer] = useState(0);
|
|
551
759
|
useEffect(() => {
|
|
552
760
|
void auth.methods().then(setMethods);
|
|
553
761
|
const back = returnedError();
|
|
@@ -562,17 +770,46 @@ function SignIn(props) {
|
|
|
562
770
|
setStep("password");
|
|
563
771
|
});
|
|
564
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]);
|
|
565
786
|
const submit = async (e) => {
|
|
566
787
|
e.preventDefault();
|
|
788
|
+
autofill.current?.abort();
|
|
567
789
|
f.setBusy(true);
|
|
568
790
|
f.setError(null);
|
|
569
791
|
const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
|
|
570
792
|
f.setBusy(false);
|
|
571
|
-
if (!r.ok)
|
|
793
|
+
if (!r.ok) {
|
|
794
|
+
setOffer((n) => n + 1);
|
|
795
|
+
return f.setError(r.error);
|
|
796
|
+
}
|
|
572
797
|
if (r.status === "mfa_required") return setStep("code");
|
|
573
798
|
f.done();
|
|
574
799
|
};
|
|
800
|
+
const usePasskey = async () => {
|
|
801
|
+
autofill.current?.abort();
|
|
802
|
+
f.setBusy(true);
|
|
803
|
+
f.setError(null);
|
|
804
|
+
const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
|
|
805
|
+
if (r.ok && r.status === "redirected") return;
|
|
806
|
+
f.setBusy(false);
|
|
807
|
+
if (!r.ok) setOffer((n) => n + 1);
|
|
808
|
+
if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
|
|
809
|
+
f.done();
|
|
810
|
+
};
|
|
575
811
|
const sendLink = async () => {
|
|
812
|
+
autofill.current?.abort();
|
|
576
813
|
f.setBusy(true);
|
|
577
814
|
f.setError(null);
|
|
578
815
|
const r = await auth.signIn.magicLink({ email });
|
|
@@ -606,9 +843,9 @@ function SignIn(props) {
|
|
|
606
843
|
" ",
|
|
607
844
|
p.name
|
|
608
845
|
] }, p.id)),
|
|
609
|
-
methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () =>
|
|
846
|
+
methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
|
|
610
847
|
((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ jsx("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
|
|
611
|
-
/* @__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 }),
|
|
612
849
|
/* @__PURE__ */ jsx(
|
|
613
850
|
Field,
|
|
614
851
|
{
|
|
@@ -632,6 +869,21 @@ function SignIn(props) {
|
|
|
632
869
|
] })
|
|
633
870
|
] });
|
|
634
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
|
+
}
|
|
635
887
|
function SignUp(props) {
|
|
636
888
|
const auth = useAuth();
|
|
637
889
|
const f = useForm(props);
|
|
@@ -792,10 +1044,7 @@ function Account(props) {
|
|
|
792
1044
|
account.hasPassword && /* @__PURE__ */ jsx(PasswordPart, { props }),
|
|
793
1045
|
/* @__PURE__ */ jsx(LinkedPart, { props }),
|
|
794
1046
|
/* @__PURE__ */ jsx(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
|
|
795
|
-
/* @__PURE__ */
|
|
796
|
-
/* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.passkeys }),
|
|
797
|
-
/* @__PURE__ */ jsx("a", { className: cx("link"), href: accountUrl(), children: t.managePasskeys })
|
|
798
|
-
] }),
|
|
1047
|
+
/* @__PURE__ */ jsx(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
|
|
799
1048
|
/* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
|
|
800
1049
|
/* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.sessions }),
|
|
801
1050
|
/* @__PURE__ */ jsx("ul", { className: cx("sessions"), children: sessions.map((s, i) => /* @__PURE__ */ jsxs("li", { className: cx("session"), children: [
|
|
@@ -809,6 +1058,60 @@ function Account(props) {
|
|
|
809
1058
|
] })
|
|
810
1059
|
] });
|
|
811
1060
|
}
|
|
1061
|
+
function PasskeysPart({ props, account, accountUrl, when }) {
|
|
1062
|
+
const auth = useAuth();
|
|
1063
|
+
const { f, done, setDone, run, general } = useSection(props);
|
|
1064
|
+
const [here, setHere] = useState(null);
|
|
1065
|
+
const [list, setList] = useState([]);
|
|
1066
|
+
const [name, setName] = useState("");
|
|
1067
|
+
const [password, setPassword] = useState("");
|
|
1068
|
+
useEffect(() => {
|
|
1069
|
+
void auth.methods().then((m) => {
|
|
1070
|
+
const can2 = m.passkeyHere && canUsePasskeys();
|
|
1071
|
+
setHere(can2);
|
|
1072
|
+
if (can2) void auth.account.passkeys().then(setList);
|
|
1073
|
+
});
|
|
1074
|
+
}, []);
|
|
1075
|
+
if (here === null) return null;
|
|
1076
|
+
if (!here) {
|
|
1077
|
+
return /* @__PURE__ */ jsxs("section", { className: f.cx("section"), children: [
|
|
1078
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.passkeys }),
|
|
1079
|
+
/* @__PURE__ */ jsx("a", { className: f.cx("link"), href: accountUrl, children: f.t.managePasskeys })
|
|
1080
|
+
] });
|
|
1081
|
+
}
|
|
1082
|
+
const add = async (e) => {
|
|
1083
|
+
e.preventDefault();
|
|
1084
|
+
const r = await run(() => auth.account.addPasskey({ name, password }));
|
|
1085
|
+
if (!r.ok) {
|
|
1086
|
+
if (r.error.code === "cancelled") f.setError(null);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
setList([...list, r.passkey]);
|
|
1090
|
+
setName("");
|
|
1091
|
+
setPassword("");
|
|
1092
|
+
setDone(f.t.passkeyAdded);
|
|
1093
|
+
};
|
|
1094
|
+
const remove = async (id) => {
|
|
1095
|
+
const r = await run(() => auth.account.removePasskey(id));
|
|
1096
|
+
if (r.ok) setList(list.filter((p) => p.id !== id));
|
|
1097
|
+
};
|
|
1098
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
|
|
1099
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.passkeys }),
|
|
1100
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.passkeysHelp }),
|
|
1101
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
1102
|
+
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: [
|
|
1103
|
+
p.name || f.t.passkeyUnnamed,
|
|
1104
|
+
" \xB7 ",
|
|
1105
|
+
when(p.createdAt),
|
|
1106
|
+
" ",
|
|
1107
|
+
/* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void remove(p.id), children: f.t.removePasskey })
|
|
1108
|
+
] }, p.id)) }),
|
|
1109
|
+
/* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.passkeyName, value: name, autoComplete: "off", onChange: setName }),
|
|
1110
|
+
account.hasPassword && /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
|
|
1111
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.addPasskey }),
|
|
1112
|
+
done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
|
|
1113
|
+
] });
|
|
1114
|
+
}
|
|
812
1115
|
function ProfilePart({ props, account, onSaved }) {
|
|
813
1116
|
const auth = useAuth();
|
|
814
1117
|
const { f, done, setDone, run, general } = useSection(props);
|
|
@@ -1072,7 +1375,16 @@ function useAuth() {
|
|
|
1072
1375
|
};
|
|
1073
1376
|
return {
|
|
1074
1377
|
...client,
|
|
1075
|
-
signIn: {
|
|
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
|
+
},
|
|
1076
1388
|
signUp: { email: (f) => then(client.signUp.email(f)) },
|
|
1077
1389
|
signOut: () => then(client.signOut()),
|
|
1078
1390
|
account: {
|
|
@@ -1092,11 +1404,14 @@ export {
|
|
|
1092
1404
|
Account,
|
|
1093
1405
|
CauthyProvider,
|
|
1094
1406
|
ForgotPassword,
|
|
1407
|
+
GoogleOneTap,
|
|
1095
1408
|
ResetPassword,
|
|
1096
1409
|
SignIn,
|
|
1097
1410
|
SignUp,
|
|
1098
1411
|
VerifyEmail,
|
|
1099
1412
|
can,
|
|
1413
|
+
canAutofillPasskeys,
|
|
1414
|
+
canUsePasskeys,
|
|
1100
1415
|
createAuthClient,
|
|
1101
1416
|
definePolicy,
|
|
1102
1417
|
useAuth,
|