cauthy-auth 0.9.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;
@@ -432,6 +481,13 @@ interface AuthClientOptions {
432
481
  /** Where the server mounted the routes. Default: `/api/auth`. */
433
482
  basePath?: string;
434
483
  fetch?: typeof fetch;
484
+ /**
485
+ * The language the person reads, for what Cauthy mails them (a sign-in
486
+ * link, a new password, a confirmation): a tag like `es` or `es-CO`, or a
487
+ * function that answers it when the person can change it. Default: the
488
+ * page's own (`<html lang>`), else the browser's.
489
+ */
490
+ locale?: string | (() => string | undefined);
435
491
  }
436
492
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
437
493
 
@@ -482,6 +538,13 @@ interface AuthTexts {
482
538
  keepCodes: string;
483
539
  passkeys: string;
484
540
  managePasskeys: string;
541
+ passkeysHelp: string;
542
+ noPasskeys: string;
543
+ passkeyName: string;
544
+ addPasskey: string;
545
+ removePasskey: string;
546
+ passkeyAdded: string;
547
+ passkeyUnnamed: string;
485
548
  sessions: string;
486
549
  signOutEverywhere: string;
487
550
  unknownDevice: string;
@@ -622,4 +685,4 @@ declare function useSession(): SessionContext;
622
685
  */
623
686
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
624
687
 
625
- 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;
@@ -432,6 +481,13 @@ interface AuthClientOptions {
432
481
  /** Where the server mounted the routes. Default: `/api/auth`. */
433
482
  basePath?: string;
434
483
  fetch?: typeof fetch;
484
+ /**
485
+ * The language the person reads, for what Cauthy mails them (a sign-in
486
+ * link, a new password, a confirmation): a tag like `es` or `es-CO`, or a
487
+ * function that answers it when the person can change it. Default: the
488
+ * page's own (`<html lang>`), else the browser's.
489
+ */
490
+ locale?: string | (() => string | undefined);
435
491
  }
436
492
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
437
493
 
@@ -482,6 +538,13 @@ interface AuthTexts {
482
538
  keepCodes: string;
483
539
  passkeys: string;
484
540
  managePasskeys: string;
541
+ passkeysHelp: string;
542
+ noPasskeys: string;
543
+ passkeyName: string;
544
+ addPasskey: string;
545
+ removePasskey: string;
546
+ passkeyAdded: string;
547
+ passkeyUnnamed: string;
485
548
  sessions: string;
486
549
  signOutEverywhere: string;
487
550
  unknownDevice: string;
@@ -622,4 +685,4 @@ declare function useSession(): SessionContext;
622
685
  */
623
686
  declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
624
687
 
625
- 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 = {};
@@ -139,13 +207,17 @@ var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right
139
207
  function createAuthClient(options = {}) {
140
208
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
141
209
  const doFetch = (...args) => (options.fetch ?? fetch)(...args);
210
+ const locale = () => {
211
+ const said = typeof options.locale === "function" ? options.locale() : options.locale;
212
+ return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
213
+ };
142
214
  async function post(path, form, bad) {
143
215
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
144
216
  try {
145
217
  const res = await doFetch(`${base}${path}`, {
146
218
  method: "POST",
147
219
  credentials: "same-origin",
148
- headers: { "content-type": "application/json", accept: "application/json" },
220
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
149
221
  body: JSON.stringify(form)
150
222
  });
151
223
  const body = await res.json().catch(() => null);
@@ -173,6 +245,28 @@ function createAuthClient(options = {}) {
173
245
  if (o.locale) q.set("locale", o.locale);
174
246
  return `${base}/login?${q}`;
175
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
+ }
176
270
  return {
177
271
  signIn: {
178
272
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -181,8 +275,16 @@ function createAuthClient(options = {}) {
181
275
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
182
276
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
183
277
  passkeyUrl,
184
- 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
+ }
185
286
  window.location.assign(passkeyUrl(o));
287
+ return { ok: true, status: "redirected" };
186
288
  },
187
289
  socialUrl,
188
290
  social(provider, o) {
@@ -226,7 +328,16 @@ function createAuthClient(options = {}) {
226
328
  const body = await read("/me/sessions");
227
329
  return body && Array.isArray(body.sessions) ? body.sessions : [];
228
330
  },
229
- 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)
230
341
  },
231
342
  async getSession() {
232
343
  try {
@@ -241,14 +352,7 @@ function createAuthClient(options = {}) {
241
352
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
242
353
  return looked;
243
354
  },
244
- async methods() {
245
- try {
246
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
247
- if (res.ok) return await res.json();
248
- } catch {
249
- }
250
- return { providers: [], signUp: true, magicLink: false, passkey: false };
251
- }
355
+ methods
252
356
  };
253
357
  }
254
358
 
@@ -355,6 +459,13 @@ var TEXTS = {
355
459
  keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
356
460
  passkeys: "Passkeys",
357
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",
358
469
  sessions: "Where you are signed in",
359
470
  signOutEverywhere: "Sign out everywhere",
360
471
  unknownDevice: "Unknown device",
@@ -385,7 +496,10 @@ var TEXTS = {
385
496
  wrong_password: "The current password is not right.",
386
497
  step_up_required: "Enter your current password.",
387
498
  weak_password: "Choose a stronger password.",
388
- 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."
389
503
  }
390
504
  },
391
505
  es: {
@@ -434,6 +548,13 @@ var TEXTS = {
434
548
  keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
435
549
  passkeys: "Llaves de acceso",
436
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",
437
558
  sessions: "D\xF3nde tienes la sesi\xF3n abierta",
438
559
  signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
439
560
  unknownDevice: "Dispositivo desconocido",
@@ -464,7 +585,10 @@ var TEXTS = {
464
585
  wrong_password: "La contrase\xF1a actual no es correcta.",
465
586
  step_up_required: "Escribe tu contrase\xF1a actual.",
466
587
  weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
467
- 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."
468
592
  }
469
593
  }
470
594
  };
@@ -602,6 +726,15 @@ function SignIn(props) {
602
726
  if (r.status === "mfa_required") return setStep("code");
603
727
  f.done();
604
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
+ };
605
738
  const sendLink = async () => {
606
739
  f.setBusy(true);
607
740
  f.setError(null);
@@ -636,7 +769,7 @@ function SignIn(props) {
636
769
  " ",
637
770
  p.name
638
771
  ] }, p.id)),
639
- 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 }),
640
773
  ((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
641
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 }),
642
775
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -822,10 +955,7 @@ function Account(props) {
822
955
  account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
823
956
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
824
957
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
825
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
826
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.passkeys }),
827
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: cx("link"), href: accountUrl(), children: t.managePasskeys })
828
- ] }),
958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
829
959
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
830
960
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
831
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: [
@@ -839,6 +969,60 @@ function Account(props) {
839
969
  ] })
840
970
  ] });
841
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
+ }
842
1026
  function ProfilePart({ props, account, onSaved }) {
843
1027
  const auth = useAuth();
844
1028
  const { f, done, setDone, run, general } = useSection(props);
@@ -1128,6 +1312,7 @@ function useAuth() {
1128
1312
  SignUp,
1129
1313
  VerifyEmail,
1130
1314
  can,
1315
+ canUsePasskeys,
1131
1316
  createAuthClient,
1132
1317
  definePolicy,
1133
1318
  useAuth,