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.js
CHANGED
|
@@ -24,11 +24,14 @@ __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,
|
|
34
|
+
canUsePasskeys: () => canUsePasskeys,
|
|
32
35
|
createAuthClient: () => createAuthClient,
|
|
33
36
|
definePolicy: () => definePolicy,
|
|
34
37
|
useAuth: () => useAuth,
|
|
@@ -37,6 +40,82 @@ __export(react_exports, {
|
|
|
37
40
|
module.exports = __toCommonJS(react_exports);
|
|
38
41
|
var import_react3 = require("react");
|
|
39
42
|
|
|
43
|
+
// src/passkey.ts
|
|
44
|
+
function canUsePasskeys() {
|
|
45
|
+
return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
|
|
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
|
+
}
|
|
56
|
+
function bytes(text2) {
|
|
57
|
+
const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
|
|
58
|
+
const bin = atob(b64);
|
|
59
|
+
const out = new Uint8Array(bin.length);
|
|
60
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
61
|
+
return out.buffer;
|
|
62
|
+
}
|
|
63
|
+
function text(buf) {
|
|
64
|
+
if (!buf) return null;
|
|
65
|
+
const view = new Uint8Array(buf);
|
|
66
|
+
let bin = "";
|
|
67
|
+
for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
|
|
68
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
69
|
+
}
|
|
70
|
+
var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
|
|
71
|
+
async function getPasskey(options, how = {}) {
|
|
72
|
+
const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
|
|
73
|
+
const cred = await navigator.credentials.get({ publicKey, ...how });
|
|
74
|
+
if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
|
|
75
|
+
const r = cred.response;
|
|
76
|
+
return {
|
|
77
|
+
id: cred.id,
|
|
78
|
+
rawId: text(cred.rawId),
|
|
79
|
+
type: cred.type,
|
|
80
|
+
response: {
|
|
81
|
+
authenticatorData: text(r.authenticatorData),
|
|
82
|
+
clientDataJSON: text(r.clientDataJSON),
|
|
83
|
+
signature: text(r.signature),
|
|
84
|
+
userHandle: text(r.userHandle)
|
|
85
|
+
},
|
|
86
|
+
clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function makePasskey(options) {
|
|
90
|
+
const publicKey = {
|
|
91
|
+
...options,
|
|
92
|
+
challenge: bytes(options.challenge),
|
|
93
|
+
user: { ...options.user, id: bytes(options.user.id) },
|
|
94
|
+
excludeCredentials: withId(options.excludeCredentials)
|
|
95
|
+
};
|
|
96
|
+
const cred = await navigator.credentials.create({ publicKey });
|
|
97
|
+
if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
|
|
98
|
+
const r = cred.response;
|
|
99
|
+
return {
|
|
100
|
+
id: cred.id,
|
|
101
|
+
rawId: text(cred.rawId),
|
|
102
|
+
type: cred.type,
|
|
103
|
+
response: {
|
|
104
|
+
clientDataJSON: text(r.clientDataJSON),
|
|
105
|
+
attestationObject: text(r.attestationObject),
|
|
106
|
+
transports: typeof r.getTransports === "function" ? r.getTransports() : []
|
|
107
|
+
},
|
|
108
|
+
clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function ceremonyError(e) {
|
|
112
|
+
const name = e instanceof Error || e instanceof DOMException ? e.name : "";
|
|
113
|
+
if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
|
|
114
|
+
if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
|
|
115
|
+
if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
|
|
116
|
+
return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
|
|
117
|
+
}
|
|
118
|
+
|
|
40
119
|
// src/theme.ts
|
|
41
120
|
function themeVariables(theme) {
|
|
42
121
|
const vars = {};
|
|
@@ -135,6 +214,24 @@ function firstProblem(...checks) {
|
|
|
135
214
|
}
|
|
136
215
|
|
|
137
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;
|
|
138
235
|
var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
|
|
139
236
|
function createAuthClient(options = {}) {
|
|
140
237
|
const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
|
|
@@ -177,6 +274,28 @@ function createAuthClient(options = {}) {
|
|
|
177
274
|
if (o.locale) q.set("locale", o.locale);
|
|
178
275
|
return `${base}/login?${q}`;
|
|
179
276
|
};
|
|
277
|
+
async function methods() {
|
|
278
|
+
try {
|
|
279
|
+
const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
280
|
+
if (res.ok) {
|
|
281
|
+
const m = await res.json();
|
|
282
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
|
|
287
|
+
}
|
|
288
|
+
async function ceremony(begin, finish, form, run, more = {}) {
|
|
289
|
+
const begun = await post(begin, form, null);
|
|
290
|
+
if (!begun.ok) return begun;
|
|
291
|
+
let credential;
|
|
292
|
+
try {
|
|
293
|
+
credential = await run(begun.options);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return { ok: false, error: ceremonyError(e) };
|
|
296
|
+
}
|
|
297
|
+
return post(finish, { credential, ...more }, null);
|
|
298
|
+
}
|
|
180
299
|
return {
|
|
181
300
|
signIn: {
|
|
182
301
|
email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
|
|
@@ -185,8 +304,76 @@ function createAuthClient(options = {}) {
|
|
|
185
304
|
magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
|
|
186
305
|
emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
|
|
187
306
|
passkeyUrl,
|
|
188
|
-
passkey(o) {
|
|
307
|
+
async passkey(o) {
|
|
308
|
+
const m = await methods();
|
|
309
|
+
if (m.passkeyHere && canUsePasskeys()) {
|
|
310
|
+
const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
|
|
311
|
+
if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
|
|
312
|
+
} else if (!m.passkeyOnCauthy) {
|
|
313
|
+
return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
|
|
314
|
+
}
|
|
189
315
|
window.location.assign(passkeyUrl(o));
|
|
316
|
+
return { ok: true, status: "redirected" };
|
|
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);
|
|
190
377
|
},
|
|
191
378
|
socialUrl,
|
|
192
379
|
social(provider, o) {
|
|
@@ -230,7 +417,16 @@ function createAuthClient(options = {}) {
|
|
|
230
417
|
const body = await read("/me/sessions");
|
|
231
418
|
return body && Array.isArray(body.sessions) ? body.sessions : [];
|
|
232
419
|
},
|
|
233
|
-
signOutEverywhere: () => post("/me/sessions/end", {}, null)
|
|
420
|
+
signOutEverywhere: () => post("/me/sessions/end", {}, null),
|
|
421
|
+
async passkeys() {
|
|
422
|
+
const body = await read("/me/passkeys");
|
|
423
|
+
return body && Array.isArray(body.passkeys) ? body.passkeys : [];
|
|
424
|
+
},
|
|
425
|
+
addPasskey(f = {}) {
|
|
426
|
+
if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
|
|
427
|
+
return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
|
|
428
|
+
},
|
|
429
|
+
removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
|
|
234
430
|
},
|
|
235
431
|
async getSession() {
|
|
236
432
|
try {
|
|
@@ -245,14 +441,7 @@ function createAuthClient(options = {}) {
|
|
|
245
441
|
looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
|
|
246
442
|
return looked;
|
|
247
443
|
},
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
251
|
-
if (res.ok) return await res.json();
|
|
252
|
-
} catch {
|
|
253
|
-
}
|
|
254
|
-
return { providers: [], signUp: true, magicLink: false, passkey: false };
|
|
255
|
-
}
|
|
444
|
+
methods
|
|
256
445
|
};
|
|
257
446
|
}
|
|
258
447
|
|
|
@@ -359,6 +548,13 @@ var TEXTS = {
|
|
|
359
548
|
keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
|
|
360
549
|
passkeys: "Passkeys",
|
|
361
550
|
managePasskeys: "Add or remove a passkey",
|
|
551
|
+
passkeysHelp: "Sign in with your fingerprint, your face or your device's lock, without a password.",
|
|
552
|
+
noPasskeys: "No passkey yet.",
|
|
553
|
+
passkeyName: "A name for this device (optional)",
|
|
554
|
+
addPasskey: "Add a passkey on this device",
|
|
555
|
+
removePasskey: "Remove",
|
|
556
|
+
passkeyAdded: "The passkey was added. Next time, sign in with it.",
|
|
557
|
+
passkeyUnnamed: "Passkey",
|
|
362
558
|
sessions: "Where you are signed in",
|
|
363
559
|
signOutEverywhere: "Sign out everywhere",
|
|
364
560
|
unknownDevice: "Unknown device",
|
|
@@ -389,7 +585,10 @@ var TEXTS = {
|
|
|
389
585
|
wrong_password: "The current password is not right.",
|
|
390
586
|
step_up_required: "Enter your current password.",
|
|
391
587
|
weak_password: "Choose a stronger password.",
|
|
392
|
-
unauthenticated: "Sign in first."
|
|
588
|
+
unauthenticated: "Sign in first.",
|
|
589
|
+
passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
|
|
590
|
+
passkey_exists: "This device already has a passkey for your account.",
|
|
591
|
+
passkey_unavailable: "Passkeys cannot be used here."
|
|
393
592
|
}
|
|
394
593
|
},
|
|
395
594
|
es: {
|
|
@@ -438,6 +637,13 @@ var TEXTS = {
|
|
|
438
637
|
keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
|
|
439
638
|
passkeys: "Llaves de acceso",
|
|
440
639
|
managePasskeys: "Agregar o quitar una llave de acceso",
|
|
640
|
+
passkeysHelp: "Entra con tu huella, tu rostro o el bloqueo de tu dispositivo, sin contrase\xF1a.",
|
|
641
|
+
noPasskeys: "Todav\xEDa no tienes llaves de acceso.",
|
|
642
|
+
passkeyName: "Un nombre para este dispositivo (opcional)",
|
|
643
|
+
addPasskey: "Agregar una llave en este dispositivo",
|
|
644
|
+
removePasskey: "Quitar",
|
|
645
|
+
passkeyAdded: "La llave de acceso qued\xF3 agregada. La pr\xF3xima vez, entra con ella.",
|
|
646
|
+
passkeyUnnamed: "Llave de acceso",
|
|
441
647
|
sessions: "D\xF3nde tienes la sesi\xF3n abierta",
|
|
442
648
|
signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
|
|
443
649
|
unknownDevice: "Dispositivo desconocido",
|
|
@@ -468,7 +674,10 @@ var TEXTS = {
|
|
|
468
674
|
wrong_password: "La contrase\xF1a actual no es correcta.",
|
|
469
675
|
step_up_required: "Escribe tu contrase\xF1a actual.",
|
|
470
676
|
weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
|
|
471
|
-
unauthenticated: "Inicia sesi\xF3n primero."
|
|
677
|
+
unauthenticated: "Inicia sesi\xF3n primero.",
|
|
678
|
+
passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
|
|
679
|
+
passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
|
|
680
|
+
passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
|
|
472
681
|
}
|
|
473
682
|
}
|
|
474
683
|
};
|
|
@@ -582,6 +791,8 @@ function SignIn(props) {
|
|
|
582
791
|
const [password, setPassword] = (0, import_react.useState)("");
|
|
583
792
|
const [code, setCode] = (0, import_react.useState)("");
|
|
584
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);
|
|
585
796
|
(0, import_react.useEffect)(() => {
|
|
586
797
|
void auth.methods().then(setMethods);
|
|
587
798
|
const back = returnedError();
|
|
@@ -596,17 +807,46 @@ function SignIn(props) {
|
|
|
596
807
|
setStep("password");
|
|
597
808
|
});
|
|
598
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]);
|
|
599
823
|
const submit = async (e) => {
|
|
600
824
|
e.preventDefault();
|
|
825
|
+
autofill.current?.abort();
|
|
601
826
|
f.setBusy(true);
|
|
602
827
|
f.setError(null);
|
|
603
828
|
const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
|
|
604
829
|
f.setBusy(false);
|
|
605
|
-
if (!r.ok)
|
|
830
|
+
if (!r.ok) {
|
|
831
|
+
setOffer((n) => n + 1);
|
|
832
|
+
return f.setError(r.error);
|
|
833
|
+
}
|
|
606
834
|
if (r.status === "mfa_required") return setStep("code");
|
|
607
835
|
f.done();
|
|
608
836
|
};
|
|
837
|
+
const usePasskey = async () => {
|
|
838
|
+
autofill.current?.abort();
|
|
839
|
+
f.setBusy(true);
|
|
840
|
+
f.setError(null);
|
|
841
|
+
const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
|
|
842
|
+
if (r.ok && r.status === "redirected") return;
|
|
843
|
+
f.setBusy(false);
|
|
844
|
+
if (!r.ok) setOffer((n) => n + 1);
|
|
845
|
+
if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
|
|
846
|
+
f.done();
|
|
847
|
+
};
|
|
609
848
|
const sendLink = async () => {
|
|
849
|
+
autofill.current?.abort();
|
|
610
850
|
f.setBusy(true);
|
|
611
851
|
f.setError(null);
|
|
612
852
|
const r = await auth.signIn.magicLink({ email });
|
|
@@ -640,9 +880,9 @@ function SignIn(props) {
|
|
|
640
880
|
" ",
|
|
641
881
|
p.name
|
|
642
882
|
] }, p.id)),
|
|
643
|
-
methods?.passkey && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () =>
|
|
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 }),
|
|
644
884
|
((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
|
|
645
|
-
/* @__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 }),
|
|
646
886
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
647
887
|
Field,
|
|
648
888
|
{
|
|
@@ -666,6 +906,21 @@ function SignIn(props) {
|
|
|
666
906
|
] })
|
|
667
907
|
] });
|
|
668
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
|
+
}
|
|
669
924
|
function SignUp(props) {
|
|
670
925
|
const auth = useAuth();
|
|
671
926
|
const f = useForm(props);
|
|
@@ -826,10 +1081,7 @@ function Account(props) {
|
|
|
826
1081
|
account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
|
|
827
1082
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
|
|
828
1083
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
|
|
829
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
830
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.passkeys }),
|
|
831
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: cx("link"), href: accountUrl(), children: t.managePasskeys })
|
|
832
|
-
] }),
|
|
1084
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
|
|
833
1085
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
|
|
834
1086
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
|
|
835
1087
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: cx("sessions"), children: sessions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: cx("session"), children: [
|
|
@@ -843,6 +1095,60 @@ function Account(props) {
|
|
|
843
1095
|
] })
|
|
844
1096
|
] });
|
|
845
1097
|
}
|
|
1098
|
+
function PasskeysPart({ props, account, accountUrl, when }) {
|
|
1099
|
+
const auth = useAuth();
|
|
1100
|
+
const { f, done, setDone, run, general } = useSection(props);
|
|
1101
|
+
const [here, setHere] = (0, import_react.useState)(null);
|
|
1102
|
+
const [list, setList] = (0, import_react.useState)([]);
|
|
1103
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
1104
|
+
const [password, setPassword] = (0, import_react.useState)("");
|
|
1105
|
+
(0, import_react.useEffect)(() => {
|
|
1106
|
+
void auth.methods().then((m) => {
|
|
1107
|
+
const can2 = m.passkeyHere && canUsePasskeys();
|
|
1108
|
+
setHere(can2);
|
|
1109
|
+
if (can2) void auth.account.passkeys().then(setList);
|
|
1110
|
+
});
|
|
1111
|
+
}, []);
|
|
1112
|
+
if (here === null) return null;
|
|
1113
|
+
if (!here) {
|
|
1114
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
|
|
1115
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.passkeys }),
|
|
1116
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: f.cx("link"), href: accountUrl, children: f.t.managePasskeys })
|
|
1117
|
+
] });
|
|
1118
|
+
}
|
|
1119
|
+
const add = async (e) => {
|
|
1120
|
+
e.preventDefault();
|
|
1121
|
+
const r = await run(() => auth.account.addPasskey({ name, password }));
|
|
1122
|
+
if (!r.ok) {
|
|
1123
|
+
if (r.error.code === "cancelled") f.setError(null);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
setList([...list, r.passkey]);
|
|
1127
|
+
setName("");
|
|
1128
|
+
setPassword("");
|
|
1129
|
+
setDone(f.t.passkeyAdded);
|
|
1130
|
+
};
|
|
1131
|
+
const remove = async (id) => {
|
|
1132
|
+
const r = await run(() => auth.account.removePasskey(id));
|
|
1133
|
+
if (r.ok) setList(list.filter((p) => p.id !== id));
|
|
1134
|
+
};
|
|
1135
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
|
|
1136
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.passkeys }),
|
|
1137
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.passkeysHelp }),
|
|
1138
|
+
general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
1139
|
+
list.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.noPasskeys }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: f.cx("sessions"), children: list.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: f.cx("session"), children: [
|
|
1140
|
+
p.name || f.t.passkeyUnnamed,
|
|
1141
|
+
" \xB7 ",
|
|
1142
|
+
when(p.createdAt),
|
|
1143
|
+
" ",
|
|
1144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void remove(p.id), children: f.t.removePasskey })
|
|
1145
|
+
] }, p.id)) }),
|
|
1146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.passkeyName, value: name, autoComplete: "off", onChange: setName }),
|
|
1147
|
+
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 }),
|
|
1148
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.addPasskey }),
|
|
1149
|
+
done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: done })
|
|
1150
|
+
] });
|
|
1151
|
+
}
|
|
846
1152
|
function ProfilePart({ props, account, onSaved }) {
|
|
847
1153
|
const auth = useAuth();
|
|
848
1154
|
const { f, done, setDone, run, general } = useSection(props);
|
|
@@ -1106,7 +1412,16 @@ function useAuth() {
|
|
|
1106
1412
|
};
|
|
1107
1413
|
return {
|
|
1108
1414
|
...client,
|
|
1109
|
-
signIn: {
|
|
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
|
+
},
|
|
1110
1425
|
signUp: { email: (f) => then(client.signUp.email(f)) },
|
|
1111
1426
|
signOut: () => then(client.signOut()),
|
|
1112
1427
|
account: {
|
|
@@ -1127,11 +1442,14 @@ function useAuth() {
|
|
|
1127
1442
|
Account,
|
|
1128
1443
|
CauthyProvider,
|
|
1129
1444
|
ForgotPassword,
|
|
1445
|
+
GoogleOneTap,
|
|
1130
1446
|
ResetPassword,
|
|
1131
1447
|
SignIn,
|
|
1132
1448
|
SignUp,
|
|
1133
1449
|
VerifyEmail,
|
|
1134
1450
|
can,
|
|
1451
|
+
canAutofillPasskeys,
|
|
1452
|
+
canUsePasskeys,
|
|
1135
1453
|
createAuthClient,
|
|
1136
1454
|
definePolicy,
|
|
1137
1455
|
useAuth,
|