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.d.mts CHANGED
@@ -210,6 +210,9 @@ interface PublicSession {
210
210
 
211
211
  type AuthField = 'email' | 'password' | 'newPassword' | 'name' | 'code';
212
212
 
213
+ /** Whether this browser can use a passkey at all. */
214
+ declare function canUsePasskeys(): boolean;
215
+
213
216
  /** What went wrong, in a form the screen can show. */
214
217
  interface AuthError {
215
218
  /** Stable, for translating: `invalid_credentials`, `invalid_email`, `too_short`, `email_taken`, `unavailable`… */
@@ -235,8 +238,29 @@ interface AuthMethods {
235
238
  /** Whether people may create an account themselves. */
236
239
  signUp: boolean;
237
240
  magicLink: boolean;
238
- /** Whether to offer a passkey. It is asked for on Cauthy's page: the person goes there for that step and comes back signed in. */
241
+ /** Whether to offer a passkey. */
239
242
  passkey: boolean;
243
+ /**
244
+ * Whether the passkey is used on this page itself. False: `signIn.passkey`
245
+ * sends the person to Cauthy's page for that step alone, and they come back
246
+ * signed in. That happens only when this page is outside the domain the
247
+ * workspace's passkeys belong to.
248
+ */
249
+ passkeyHere: boolean;
250
+ /**
251
+ * Whether Cauthy's page can use the passkey, for that step alone: where a
252
+ * page that cannot, or a browser that refuses to here, sends the person.
253
+ */
254
+ passkeyOnCauthy: boolean;
255
+ }
256
+ /** A passkey of the signed-in account. Moments are UTC. */
257
+ interface Passkey {
258
+ id: string;
259
+ /** What the person called it ("My laptop"), or "". */
260
+ name: string;
261
+ createdAt: string;
262
+ /** When it last signed the person in, or null. */
263
+ lastUsedAt: string | null;
240
264
  }
241
265
  /** A person's own account, as their account screen shows it. */
242
266
  interface Account$1 {
@@ -340,6 +364,22 @@ interface AccountClient {
340
364
  sessions(): Promise<AccountSession[]>;
341
365
  /** Signs the account out everywhere, here too. */
342
366
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
367
+ /** The account's passkeys. */
368
+ passkeys(): Promise<Passkey[]>;
369
+ /**
370
+ * Makes a passkey on this device, on this page: the browser asks the person
371
+ * for their fingerprint, face or screen lock. An account with a password
372
+ * proves it again first. `cancelled`: the person closed the browser's prompt.
373
+ */
374
+ addPasskey(form?: {
375
+ password?: string;
376
+ name?: string;
377
+ }): Promise<AuthResult<'added', {
378
+ passkey: Passkey;
379
+ }>>;
380
+ removePasskey(id: string): Promise<AuthResult<'removed', {
381
+ id: string;
382
+ }>>;
343
383
  }
344
384
  interface AuthClient {
345
385
  signIn: {
@@ -372,12 +412,21 @@ interface AuthClient {
372
412
  }): Promise<AuthResult<'signed_in', {
373
413
  session: PublicSession;
374
414
  }>>;
375
- /** Sends the browser to Cauthy's page to use a passkey; it comes back signed in, at `returnTo`. */
415
+ /**
416
+ * Signs in with a passkey, on this page: the browser asks the person for
417
+ * their fingerprint, face or screen lock. When this page cannot use the
418
+ * workspace's passkeys (`methods().passkeyHere` is false), the browser goes
419
+ * to Cauthy's page for that step alone and comes back signed in, at
420
+ * `returnTo`: then the answer is `redirected`. `cancelled`: the person
421
+ * closed the browser's prompt.
422
+ */
376
423
  passkey(options?: {
377
424
  returnTo?: string;
378
425
  locale?: string;
379
- }): void;
380
- /** Where `passkey` sends the browser, for a plain link. */
426
+ }): Promise<AuthResult<'signed_in', {
427
+ session: PublicSession;
428
+ }> | AuthResult<'redirected'>>;
429
+ /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
381
430
  passkeyUrl(options?: {
382
431
  returnTo?: string;
383
432
  locale?: string;
@@ -489,6 +538,13 @@ interface AuthTexts {
489
538
  keepCodes: string;
490
539
  passkeys: string;
491
540
  managePasskeys: string;
541
+ passkeysHelp: string;
542
+ noPasskeys: string;
543
+ passkeyName: string;
544
+ addPasskey: string;
545
+ removePasskey: string;
546
+ passkeyAdded: string;
547
+ passkeyUnnamed: string;
492
548
  sessions: string;
493
549
  signOutEverywhere: string;
494
550
  unknownDevice: string;
@@ -629,4 +685,4 @@ declare function useSession(): SessionContext;
629
685
  */
630
686
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
631
687
 
632
- 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 PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
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 };
package/dist/react.d.ts CHANGED
@@ -210,6 +210,9 @@ interface PublicSession {
210
210
 
211
211
  type AuthField = 'email' | 'password' | 'newPassword' | 'name' | 'code';
212
212
 
213
+ /** Whether this browser can use a passkey at all. */
214
+ declare function canUsePasskeys(): boolean;
215
+
213
216
  /** What went wrong, in a form the screen can show. */
214
217
  interface AuthError {
215
218
  /** Stable, for translating: `invalid_credentials`, `invalid_email`, `too_short`, `email_taken`, `unavailable`… */
@@ -235,8 +238,29 @@ interface AuthMethods {
235
238
  /** Whether people may create an account themselves. */
236
239
  signUp: boolean;
237
240
  magicLink: boolean;
238
- /** Whether to offer a passkey. It is asked for on Cauthy's page: the person goes there for that step and comes back signed in. */
241
+ /** Whether to offer a passkey. */
239
242
  passkey: boolean;
243
+ /**
244
+ * Whether the passkey is used on this page itself. False: `signIn.passkey`
245
+ * sends the person to Cauthy's page for that step alone, and they come back
246
+ * signed in. That happens only when this page is outside the domain the
247
+ * workspace's passkeys belong to.
248
+ */
249
+ passkeyHere: boolean;
250
+ /**
251
+ * Whether Cauthy's page can use the passkey, for that step alone: where a
252
+ * page that cannot, or a browser that refuses to here, sends the person.
253
+ */
254
+ passkeyOnCauthy: boolean;
255
+ }
256
+ /** A passkey of the signed-in account. Moments are UTC. */
257
+ interface Passkey {
258
+ id: string;
259
+ /** What the person called it ("My laptop"), or "". */
260
+ name: string;
261
+ createdAt: string;
262
+ /** When it last signed the person in, or null. */
263
+ lastUsedAt: string | null;
240
264
  }
241
265
  /** A person's own account, as their account screen shows it. */
242
266
  interface Account$1 {
@@ -340,6 +364,22 @@ interface AccountClient {
340
364
  sessions(): Promise<AccountSession[]>;
341
365
  /** Signs the account out everywhere, here too. */
342
366
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
367
+ /** The account's passkeys. */
368
+ passkeys(): Promise<Passkey[]>;
369
+ /**
370
+ * Makes a passkey on this device, on this page: the browser asks the person
371
+ * for their fingerprint, face or screen lock. An account with a password
372
+ * proves it again first. `cancelled`: the person closed the browser's prompt.
373
+ */
374
+ addPasskey(form?: {
375
+ password?: string;
376
+ name?: string;
377
+ }): Promise<AuthResult<'added', {
378
+ passkey: Passkey;
379
+ }>>;
380
+ removePasskey(id: string): Promise<AuthResult<'removed', {
381
+ id: string;
382
+ }>>;
343
383
  }
344
384
  interface AuthClient {
345
385
  signIn: {
@@ -372,12 +412,21 @@ interface AuthClient {
372
412
  }): Promise<AuthResult<'signed_in', {
373
413
  session: PublicSession;
374
414
  }>>;
375
- /** Sends the browser to Cauthy's page to use a passkey; it comes back signed in, at `returnTo`. */
415
+ /**
416
+ * Signs in with a passkey, on this page: the browser asks the person for
417
+ * their fingerprint, face or screen lock. When this page cannot use the
418
+ * workspace's passkeys (`methods().passkeyHere` is false), the browser goes
419
+ * to Cauthy's page for that step alone and comes back signed in, at
420
+ * `returnTo`: then the answer is `redirected`. `cancelled`: the person
421
+ * closed the browser's prompt.
422
+ */
376
423
  passkey(options?: {
377
424
  returnTo?: string;
378
425
  locale?: string;
379
- }): void;
380
- /** Where `passkey` sends the browser, for a plain link. */
426
+ }): Promise<AuthResult<'signed_in', {
427
+ session: PublicSession;
428
+ }> | AuthResult<'redirected'>>;
429
+ /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
381
430
  passkeyUrl(options?: {
382
431
  returnTo?: string;
383
432
  locale?: string;
@@ -489,6 +538,13 @@ interface AuthTexts {
489
538
  keepCodes: string;
490
539
  passkeys: string;
491
540
  managePasskeys: string;
541
+ passkeysHelp: string;
542
+ noPasskeys: string;
543
+ passkeyName: string;
544
+ addPasskey: string;
545
+ removePasskey: string;
546
+ passkeyAdded: string;
547
+ passkeyUnnamed: string;
492
548
  sessions: string;
493
549
  signOutEverywhere: string;
494
550
  unknownDevice: string;
@@ -629,4 +685,4 @@ declare function useSession(): SessionContext;
629
685
  */
630
686
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
631
687
 
632
- 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 PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
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 };
package/dist/react.js CHANGED
@@ -29,6 +29,7 @@ __export(react_exports, {
29
29
  SignUp: () => SignUp,
30
30
  VerifyEmail: () => VerifyEmail,
31
31
  can: () => can,
32
+ canUsePasskeys: () => canUsePasskeys,
32
33
  createAuthClient: () => createAuthClient,
33
34
  definePolicy: () => definePolicy,
34
35
  useAuth: () => useAuth,
@@ -37,6 +38,73 @@ __export(react_exports, {
37
38
  module.exports = __toCommonJS(react_exports);
38
39
  var import_react3 = require("react");
39
40
 
41
+ // src/passkey.ts
42
+ function canUsePasskeys() {
43
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
44
+ }
45
+ function bytes(text2) {
46
+ const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
47
+ const bin = atob(b64);
48
+ const out = new Uint8Array(bin.length);
49
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
50
+ return out.buffer;
51
+ }
52
+ function text(buf) {
53
+ if (!buf) return null;
54
+ const view = new Uint8Array(buf);
55
+ let bin = "";
56
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
57
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
58
+ }
59
+ var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
60
+ async function getPasskey(options) {
61
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
62
+ const cred = await navigator.credentials.get({ publicKey });
63
+ if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
64
+ const r = cred.response;
65
+ return {
66
+ id: cred.id,
67
+ rawId: text(cred.rawId),
68
+ type: cred.type,
69
+ response: {
70
+ authenticatorData: text(r.authenticatorData),
71
+ clientDataJSON: text(r.clientDataJSON),
72
+ signature: text(r.signature),
73
+ userHandle: text(r.userHandle)
74
+ },
75
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
76
+ };
77
+ }
78
+ async function makePasskey(options) {
79
+ const publicKey = {
80
+ ...options,
81
+ challenge: bytes(options.challenge),
82
+ user: { ...options.user, id: bytes(options.user.id) },
83
+ excludeCredentials: withId(options.excludeCredentials)
84
+ };
85
+ const cred = await navigator.credentials.create({ publicKey });
86
+ if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
87
+ const r = cred.response;
88
+ return {
89
+ id: cred.id,
90
+ rawId: text(cred.rawId),
91
+ type: cred.type,
92
+ response: {
93
+ clientDataJSON: text(r.clientDataJSON),
94
+ attestationObject: text(r.attestationObject),
95
+ transports: typeof r.getTransports === "function" ? r.getTransports() : []
96
+ },
97
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
98
+ };
99
+ }
100
+ function ceremonyError(e) {
101
+ const name = e instanceof Error || e instanceof DOMException ? e.name : "";
102
+ if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
103
+ if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
104
+ if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
105
+ return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
106
+ }
107
+
40
108
  // src/theme.ts
41
109
  function themeVariables(theme) {
42
110
  const vars = {};
@@ -177,6 +245,28 @@ function createAuthClient(options = {}) {
177
245
  if (o.locale) q.set("locale", o.locale);
178
246
  return `${base}/login?${q}`;
179
247
  };
248
+ async function methods() {
249
+ try {
250
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
251
+ if (res.ok) {
252
+ const m = await res.json();
253
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
254
+ }
255
+ } catch {
256
+ }
257
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
258
+ }
259
+ async function ceremony(begin, finish, form, run, more = {}) {
260
+ const begun = await post(begin, form, null);
261
+ if (!begun.ok) return begun;
262
+ let credential;
263
+ try {
264
+ credential = await run(begun.options);
265
+ } catch (e) {
266
+ return { ok: false, error: ceremonyError(e) };
267
+ }
268
+ return post(finish, { credential, ...more }, null);
269
+ }
180
270
  return {
181
271
  signIn: {
182
272
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -185,8 +275,16 @@ function createAuthClient(options = {}) {
185
275
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
186
276
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
187
277
  passkeyUrl,
188
- passkey(o) {
278
+ async passkey(o) {
279
+ const m = await methods();
280
+ if (m.passkeyHere && canUsePasskeys()) {
281
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
282
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
283
+ } else if (!m.passkeyOnCauthy) {
284
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
285
+ }
189
286
  window.location.assign(passkeyUrl(o));
287
+ return { ok: true, status: "redirected" };
190
288
  },
191
289
  socialUrl,
192
290
  social(provider, o) {
@@ -230,7 +328,16 @@ function createAuthClient(options = {}) {
230
328
  const body = await read("/me/sessions");
231
329
  return body && Array.isArray(body.sessions) ? body.sessions : [];
232
330
  },
233
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
331
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
332
+ async passkeys() {
333
+ const body = await read("/me/passkeys");
334
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
335
+ },
336
+ addPasskey(f = {}) {
337
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
338
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
339
+ },
340
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
234
341
  },
235
342
  async getSession() {
236
343
  try {
@@ -245,14 +352,7 @@ function createAuthClient(options = {}) {
245
352
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
246
353
  return looked;
247
354
  },
248
- async methods() {
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
- }
355
+ methods
256
356
  };
257
357
  }
258
358
 
@@ -359,6 +459,13 @@ var TEXTS = {
359
459
  keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
360
460
  passkeys: "Passkeys",
361
461
  managePasskeys: "Add or remove a passkey",
462
+ passkeysHelp: "Sign in with your fingerprint, your face or your device's lock, without a password.",
463
+ noPasskeys: "No passkey yet.",
464
+ passkeyName: "A name for this device (optional)",
465
+ addPasskey: "Add a passkey on this device",
466
+ removePasskey: "Remove",
467
+ passkeyAdded: "The passkey was added. Next time, sign in with it.",
468
+ passkeyUnnamed: "Passkey",
362
469
  sessions: "Where you are signed in",
363
470
  signOutEverywhere: "Sign out everywhere",
364
471
  unknownDevice: "Unknown device",
@@ -389,7 +496,10 @@ var TEXTS = {
389
496
  wrong_password: "The current password is not right.",
390
497
  step_up_required: "Enter your current password.",
391
498
  weak_password: "Choose a stronger password.",
392
- unauthenticated: "Sign in first."
499
+ unauthenticated: "Sign in first.",
500
+ passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
501
+ passkey_exists: "This device already has a passkey for your account.",
502
+ passkey_unavailable: "Passkeys cannot be used here."
393
503
  }
394
504
  },
395
505
  es: {
@@ -438,6 +548,13 @@ var TEXTS = {
438
548
  keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
439
549
  passkeys: "Llaves de acceso",
440
550
  managePasskeys: "Agregar o quitar una llave de acceso",
551
+ passkeysHelp: "Entra con tu huella, tu rostro o el bloqueo de tu dispositivo, sin contrase\xF1a.",
552
+ noPasskeys: "Todav\xEDa no tienes llaves de acceso.",
553
+ passkeyName: "Un nombre para este dispositivo (opcional)",
554
+ addPasskey: "Agregar una llave en este dispositivo",
555
+ removePasskey: "Quitar",
556
+ passkeyAdded: "La llave de acceso qued\xF3 agregada. La pr\xF3xima vez, entra con ella.",
557
+ passkeyUnnamed: "Llave de acceso",
441
558
  sessions: "D\xF3nde tienes la sesi\xF3n abierta",
442
559
  signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
443
560
  unknownDevice: "Dispositivo desconocido",
@@ -468,7 +585,10 @@ var TEXTS = {
468
585
  wrong_password: "La contrase\xF1a actual no es correcta.",
469
586
  step_up_required: "Escribe tu contrase\xF1a actual.",
470
587
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
471
- unauthenticated: "Inicia sesi\xF3n primero."
588
+ unauthenticated: "Inicia sesi\xF3n primero.",
589
+ passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
590
+ passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
591
+ passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
472
592
  }
473
593
  }
474
594
  };
@@ -606,6 +726,15 @@ function SignIn(props) {
606
726
  if (r.status === "mfa_required") return setStep("code");
607
727
  f.done();
608
728
  };
729
+ const usePasskey = async () => {
730
+ f.setBusy(true);
731
+ f.setError(null);
732
+ const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
733
+ if (r.ok && r.status === "redirected") return;
734
+ f.setBusy(false);
735
+ if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
736
+ f.done();
737
+ };
609
738
  const sendLink = async () => {
610
739
  f.setBusy(true);
611
740
  f.setError(null);
@@ -640,7 +769,7 @@ function SignIn(props) {
640
769
  " ",
641
770
  p.name
642
771
  ] }, p.id)),
643
- methods?.passkey && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () => auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale }), children: f.t.usePasskey }),
772
+ 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
773
  ((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
645
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 }),
646
775
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -826,10 +955,7 @@ function Account(props) {
826
955
  account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
827
956
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
828
957
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
829
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
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
- ] }),
958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
833
959
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
834
960
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
835
961
  /* @__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 +969,60 @@ function Account(props) {
843
969
  ] })
844
970
  ] });
845
971
  }
972
+ function PasskeysPart({ props, account, accountUrl, when }) {
973
+ const auth = useAuth();
974
+ const { f, done, setDone, run, general } = useSection(props);
975
+ const [here, setHere] = (0, import_react.useState)(null);
976
+ const [list, setList] = (0, import_react.useState)([]);
977
+ const [name, setName] = (0, import_react.useState)("");
978
+ const [password, setPassword] = (0, import_react.useState)("");
979
+ (0, import_react.useEffect)(() => {
980
+ void auth.methods().then((m) => {
981
+ const can2 = m.passkeyHere && canUsePasskeys();
982
+ setHere(can2);
983
+ if (can2) void auth.account.passkeys().then(setList);
984
+ });
985
+ }, []);
986
+ if (here === null) return null;
987
+ if (!here) {
988
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
989
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.passkeys }),
990
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: f.cx("link"), href: accountUrl, children: f.t.managePasskeys })
991
+ ] });
992
+ }
993
+ const add = async (e) => {
994
+ e.preventDefault();
995
+ const r = await run(() => auth.account.addPasskey({ name, password }));
996
+ if (!r.ok) {
997
+ if (r.error.code === "cancelled") f.setError(null);
998
+ return;
999
+ }
1000
+ setList([...list, r.passkey]);
1001
+ setName("");
1002
+ setPassword("");
1003
+ setDone(f.t.passkeyAdded);
1004
+ };
1005
+ const remove = async (id) => {
1006
+ const r = await run(() => auth.account.removePasskey(id));
1007
+ if (r.ok) setList(list.filter((p) => p.id !== id));
1008
+ };
1009
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.passkeys }),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.passkeysHelp }),
1012
+ general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1013
+ 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: [
1014
+ p.name || f.t.passkeyUnnamed,
1015
+ " \xB7 ",
1016
+ when(p.createdAt),
1017
+ " ",
1018
+ /* @__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 })
1019
+ ] }, p.id)) }),
1020
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.passkeyName, value: name, autoComplete: "off", onChange: setName }),
1021
+ 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 }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.addPasskey }),
1023
+ done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: done })
1024
+ ] });
1025
+ }
846
1026
  function ProfilePart({ props, account, onSaved }) {
847
1027
  const auth = useAuth();
848
1028
  const { f, done, setDone, run, general } = useSection(props);
@@ -1132,6 +1312,7 @@ function useAuth() {
1132
1312
  SignUp,
1133
1313
  VerifyEmail,
1134
1314
  can,
1315
+ canUsePasskeys,
1135
1316
  createAuthClient,
1136
1317
  definePolicy,
1137
1318
  useAuth,