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/client.d.mts CHANGED
@@ -4,6 +4,9 @@ import { A as AuthField } from './validate-B_iC-4y8.mjs';
4
4
  export { F as FieldProblem, M as MIN_PASSWORD, c as checkCode, a as checkEmail, b as checkName, d as checkNewPassword, e as checkPassword } from './validate-B_iC-4y8.mjs';
5
5
  import 'jose';
6
6
 
7
+ /** Whether this browser can use a passkey at all. */
8
+ declare function canUsePasskeys(): boolean;
9
+
7
10
  interface SessionState {
8
11
  /** The signed-in person and their organizations, or null. */
9
12
  session: PublicSession | null;
@@ -70,8 +73,29 @@ interface AuthMethods {
70
73
  /** Whether people may create an account themselves. */
71
74
  signUp: boolean;
72
75
  magicLink: boolean;
73
- /** 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. */
76
+ /** Whether to offer a passkey. */
74
77
  passkey: boolean;
78
+ /**
79
+ * Whether the passkey is used on this page itself. False: `signIn.passkey`
80
+ * sends the person to Cauthy's page for that step alone, and they come back
81
+ * signed in. That happens only when this page is outside the domain the
82
+ * workspace's passkeys belong to.
83
+ */
84
+ passkeyHere: boolean;
85
+ /**
86
+ * Whether Cauthy's page can use the passkey, for that step alone: where a
87
+ * page that cannot, or a browser that refuses to here, sends the person.
88
+ */
89
+ passkeyOnCauthy: boolean;
90
+ }
91
+ /** A passkey of the signed-in account. Moments are UTC. */
92
+ interface Passkey {
93
+ id: string;
94
+ /** What the person called it ("My laptop"), or "". */
95
+ name: string;
96
+ createdAt: string;
97
+ /** When it last signed the person in, or null. */
98
+ lastUsedAt: string | null;
75
99
  }
76
100
  /** A person's own account, as their account screen shows it. */
77
101
  interface Account {
@@ -175,6 +199,22 @@ interface AccountClient {
175
199
  sessions(): Promise<AccountSession[]>;
176
200
  /** Signs the account out everywhere, here too. */
177
201
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
202
+ /** The account's passkeys. */
203
+ passkeys(): Promise<Passkey[]>;
204
+ /**
205
+ * Makes a passkey on this device, on this page: the browser asks the person
206
+ * for their fingerprint, face or screen lock. An account with a password
207
+ * proves it again first. `cancelled`: the person closed the browser's prompt.
208
+ */
209
+ addPasskey(form?: {
210
+ password?: string;
211
+ name?: string;
212
+ }): Promise<AuthResult<'added', {
213
+ passkey: Passkey;
214
+ }>>;
215
+ removePasskey(id: string): Promise<AuthResult<'removed', {
216
+ id: string;
217
+ }>>;
178
218
  }
179
219
  interface AuthClient {
180
220
  signIn: {
@@ -207,12 +247,21 @@ interface AuthClient {
207
247
  }): Promise<AuthResult<'signed_in', {
208
248
  session: PublicSession;
209
249
  }>>;
210
- /** Sends the browser to Cauthy's page to use a passkey; it comes back signed in, at `returnTo`. */
250
+ /**
251
+ * Signs in with a passkey, on this page: the browser asks the person for
252
+ * their fingerprint, face or screen lock. When this page cannot use the
253
+ * workspace's passkeys (`methods().passkeyHere` is false), the browser goes
254
+ * to Cauthy's page for that step alone and comes back signed in, at
255
+ * `returnTo`: then the answer is `redirected`. `cancelled`: the person
256
+ * closed the browser's prompt.
257
+ */
211
258
  passkey(options?: {
212
259
  returnTo?: string;
213
260
  locale?: string;
214
- }): void;
215
- /** Where `passkey` sends the browser, for a plain link. */
261
+ }): Promise<AuthResult<'signed_in', {
262
+ session: PublicSession;
263
+ }> | AuthResult<'redirected'>>;
264
+ /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
216
265
  passkeyUrl(options?: {
217
266
  returnTo?: string;
218
267
  locale?: string;
@@ -267,7 +316,14 @@ interface AuthClientOptions {
267
316
  /** Where the server mounted the routes. Default: `/api/auth`. */
268
317
  basePath?: string;
269
318
  fetch?: typeof fetch;
319
+ /**
320
+ * The language the person reads, for what Cauthy mails them (a sign-in
321
+ * link, a new password, a confirmation): a tag like `es` or `es-CO`, or a
322
+ * function that answers it when the person can change it. Default: the
323
+ * page's own (`<html lang>`), else the browser's.
324
+ */
325
+ locale?: string | (() => string | undefined);
270
326
  }
271
327
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
272
328
 
273
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type SessionState, type SessionStore, type SessionStoreOptions, createAuthClient, createSessionStore };
329
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type Passkey, type SessionState, type SessionStore, type SessionStoreOptions, canUsePasskeys, createAuthClient, createSessionStore };
package/dist/client.d.ts CHANGED
@@ -4,6 +4,9 @@ import { A as AuthField } from './validate-B_iC-4y8.js';
4
4
  export { F as FieldProblem, M as MIN_PASSWORD, c as checkCode, a as checkEmail, b as checkName, d as checkNewPassword, e as checkPassword } from './validate-B_iC-4y8.js';
5
5
  import 'jose';
6
6
 
7
+ /** Whether this browser can use a passkey at all. */
8
+ declare function canUsePasskeys(): boolean;
9
+
7
10
  interface SessionState {
8
11
  /** The signed-in person and their organizations, or null. */
9
12
  session: PublicSession | null;
@@ -70,8 +73,29 @@ interface AuthMethods {
70
73
  /** Whether people may create an account themselves. */
71
74
  signUp: boolean;
72
75
  magicLink: boolean;
73
- /** 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. */
76
+ /** Whether to offer a passkey. */
74
77
  passkey: boolean;
78
+ /**
79
+ * Whether the passkey is used on this page itself. False: `signIn.passkey`
80
+ * sends the person to Cauthy's page for that step alone, and they come back
81
+ * signed in. That happens only when this page is outside the domain the
82
+ * workspace's passkeys belong to.
83
+ */
84
+ passkeyHere: boolean;
85
+ /**
86
+ * Whether Cauthy's page can use the passkey, for that step alone: where a
87
+ * page that cannot, or a browser that refuses to here, sends the person.
88
+ */
89
+ passkeyOnCauthy: boolean;
90
+ }
91
+ /** A passkey of the signed-in account. Moments are UTC. */
92
+ interface Passkey {
93
+ id: string;
94
+ /** What the person called it ("My laptop"), or "". */
95
+ name: string;
96
+ createdAt: string;
97
+ /** When it last signed the person in, or null. */
98
+ lastUsedAt: string | null;
75
99
  }
76
100
  /** A person's own account, as their account screen shows it. */
77
101
  interface Account {
@@ -175,6 +199,22 @@ interface AccountClient {
175
199
  sessions(): Promise<AccountSession[]>;
176
200
  /** Signs the account out everywhere, here too. */
177
201
  signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
202
+ /** The account's passkeys. */
203
+ passkeys(): Promise<Passkey[]>;
204
+ /**
205
+ * Makes a passkey on this device, on this page: the browser asks the person
206
+ * for their fingerprint, face or screen lock. An account with a password
207
+ * proves it again first. `cancelled`: the person closed the browser's prompt.
208
+ */
209
+ addPasskey(form?: {
210
+ password?: string;
211
+ name?: string;
212
+ }): Promise<AuthResult<'added', {
213
+ passkey: Passkey;
214
+ }>>;
215
+ removePasskey(id: string): Promise<AuthResult<'removed', {
216
+ id: string;
217
+ }>>;
178
218
  }
179
219
  interface AuthClient {
180
220
  signIn: {
@@ -207,12 +247,21 @@ interface AuthClient {
207
247
  }): Promise<AuthResult<'signed_in', {
208
248
  session: PublicSession;
209
249
  }>>;
210
- /** Sends the browser to Cauthy's page to use a passkey; it comes back signed in, at `returnTo`. */
250
+ /**
251
+ * Signs in with a passkey, on this page: the browser asks the person for
252
+ * their fingerprint, face or screen lock. When this page cannot use the
253
+ * workspace's passkeys (`methods().passkeyHere` is false), the browser goes
254
+ * to Cauthy's page for that step alone and comes back signed in, at
255
+ * `returnTo`: then the answer is `redirected`. `cancelled`: the person
256
+ * closed the browser's prompt.
257
+ */
211
258
  passkey(options?: {
212
259
  returnTo?: string;
213
260
  locale?: string;
214
- }): void;
215
- /** Where `passkey` sends the browser, for a plain link. */
261
+ }): Promise<AuthResult<'signed_in', {
262
+ session: PublicSession;
263
+ }> | AuthResult<'redirected'>>;
264
+ /** Where `passkey` sends the browser when this page cannot use the passkey itself. */
216
265
  passkeyUrl(options?: {
217
266
  returnTo?: string;
218
267
  locale?: string;
@@ -267,7 +316,14 @@ interface AuthClientOptions {
267
316
  /** Where the server mounted the routes. Default: `/api/auth`. */
268
317
  basePath?: string;
269
318
  fetch?: typeof fetch;
319
+ /**
320
+ * The language the person reads, for what Cauthy mails them (a sign-in
321
+ * link, a new password, a confirmation): a tag like `es` or `es-CO`, or a
322
+ * function that answers it when the person can change it. Default: the
323
+ * page's own (`<html lang>`), else the browser's.
324
+ */
325
+ locale?: string | (() => string | undefined);
270
326
  }
271
327
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
272
328
 
273
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type SessionState, type SessionStore, type SessionStoreOptions, createAuthClient, createSessionStore };
329
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type LinkedIdentity, type Passkey, type SessionState, type SessionStore, type SessionStoreOptions, canUsePasskeys, createAuthClient, createSessionStore };
package/dist/client.js CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var client_exports = {};
22
22
  __export(client_exports, {
23
23
  MIN_PASSWORD: () => MIN_PASSWORD,
24
+ canUsePasskeys: () => canUsePasskeys,
24
25
  checkCode: () => checkCode,
25
26
  checkEmail: () => checkEmail,
26
27
  checkName: () => checkName,
@@ -31,6 +32,73 @@ __export(client_exports, {
31
32
  });
32
33
  module.exports = __toCommonJS(client_exports);
33
34
 
35
+ // src/passkey.ts
36
+ function canUsePasskeys() {
37
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
38
+ }
39
+ function bytes(text2) {
40
+ const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
41
+ const bin = atob(b64);
42
+ const out = new Uint8Array(bin.length);
43
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
44
+ return out.buffer;
45
+ }
46
+ function text(buf) {
47
+ if (!buf) return null;
48
+ const view = new Uint8Array(buf);
49
+ let bin = "";
50
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
51
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
52
+ }
53
+ var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
54
+ async function getPasskey(options) {
55
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
56
+ const cred = await navigator.credentials.get({ publicKey });
57
+ if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
58
+ const r = cred.response;
59
+ return {
60
+ id: cred.id,
61
+ rawId: text(cred.rawId),
62
+ type: cred.type,
63
+ response: {
64
+ authenticatorData: text(r.authenticatorData),
65
+ clientDataJSON: text(r.clientDataJSON),
66
+ signature: text(r.signature),
67
+ userHandle: text(r.userHandle)
68
+ },
69
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
70
+ };
71
+ }
72
+ async function makePasskey(options) {
73
+ const publicKey = {
74
+ ...options,
75
+ challenge: bytes(options.challenge),
76
+ user: { ...options.user, id: bytes(options.user.id) },
77
+ excludeCredentials: withId(options.excludeCredentials)
78
+ };
79
+ const cred = await navigator.credentials.create({ publicKey });
80
+ if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
81
+ const r = cred.response;
82
+ return {
83
+ id: cred.id,
84
+ rawId: text(cred.rawId),
85
+ type: cred.type,
86
+ response: {
87
+ clientDataJSON: text(r.clientDataJSON),
88
+ attestationObject: text(r.attestationObject),
89
+ transports: typeof r.getTransports === "function" ? r.getTransports() : []
90
+ },
91
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
92
+ };
93
+ }
94
+ function ceremonyError(e) {
95
+ const name = e instanceof Error || e instanceof DOMException ? e.name : "";
96
+ if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
97
+ if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
98
+ if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
99
+ return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
100
+ }
101
+
34
102
  // src/theme.ts
35
103
  function fromWire(wire) {
36
104
  const names = {
@@ -196,7 +264,8 @@ function createSessionStore(options = {}) {
196
264
  email: (f) => then(client.signIn.email(f)),
197
265
  mfa: (f) => then(client.signIn.mfa(f)),
198
266
  magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
199
- emailCode: (f) => then(client.signIn.emailCode(f))
267
+ emailCode: (f) => then(client.signIn.emailCode(f)),
268
+ passkey: (o) => then(client.signIn.passkey(o))
200
269
  },
201
270
  signUp: { email: (f) => then(client.signUp.email(f)) },
202
271
  signOut: () => then(client.signOut()),
@@ -216,13 +285,17 @@ var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right
216
285
  function createAuthClient(options = {}) {
217
286
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
218
287
  const doFetch = (...args) => (options.fetch ?? fetch)(...args);
288
+ const locale = () => {
289
+ const said = typeof options.locale === "function" ? options.locale() : options.locale;
290
+ return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
291
+ };
219
292
  async function post(path, form, bad) {
220
293
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
221
294
  try {
222
295
  const res = await doFetch(`${base}${path}`, {
223
296
  method: "POST",
224
297
  credentials: "same-origin",
225
- headers: { "content-type": "application/json", accept: "application/json" },
298
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
226
299
  body: JSON.stringify(form)
227
300
  });
228
301
  const body = await res.json().catch(() => null);
@@ -250,6 +323,28 @@ function createAuthClient(options = {}) {
250
323
  if (o.locale) q.set("locale", o.locale);
251
324
  return `${base}/login?${q}`;
252
325
  };
326
+ async function methods() {
327
+ try {
328
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
329
+ if (res.ok) {
330
+ const m = await res.json();
331
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
332
+ }
333
+ } catch {
334
+ }
335
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
336
+ }
337
+ async function ceremony(begin, finish, form, run, more = {}) {
338
+ const begun = await post(begin, form, null);
339
+ if (!begun.ok) return begun;
340
+ let credential;
341
+ try {
342
+ credential = await run(begun.options);
343
+ } catch (e) {
344
+ return { ok: false, error: ceremonyError(e) };
345
+ }
346
+ return post(finish, { credential, ...more }, null);
347
+ }
253
348
  return {
254
349
  signIn: {
255
350
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -258,8 +353,16 @@ function createAuthClient(options = {}) {
258
353
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
259
354
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
260
355
  passkeyUrl,
261
- passkey(o) {
356
+ async passkey(o) {
357
+ const m = await methods();
358
+ if (m.passkeyHere && canUsePasskeys()) {
359
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
360
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
361
+ } else if (!m.passkeyOnCauthy) {
362
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
363
+ }
262
364
  window.location.assign(passkeyUrl(o));
365
+ return { ok: true, status: "redirected" };
263
366
  },
264
367
  socialUrl,
265
368
  social(provider, o) {
@@ -303,7 +406,16 @@ function createAuthClient(options = {}) {
303
406
  const body = await read("/me/sessions");
304
407
  return body && Array.isArray(body.sessions) ? body.sessions : [];
305
408
  },
306
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
409
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
410
+ async passkeys() {
411
+ const body = await read("/me/passkeys");
412
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
413
+ },
414
+ addPasskey(f = {}) {
415
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
416
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
417
+ },
418
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
307
419
  },
308
420
  async getSession() {
309
421
  try {
@@ -318,19 +430,13 @@ function createAuthClient(options = {}) {
318
430
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
319
431
  return looked;
320
432
  },
321
- async methods() {
322
- try {
323
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
324
- if (res.ok) return await res.json();
325
- } catch {
326
- }
327
- return { providers: [], signUp: true, magicLink: false, passkey: false };
328
- }
433
+ methods
329
434
  };
330
435
  }
331
436
  // Annotate the CommonJS export names for ESM import in node:
332
437
  0 && (module.exports = {
333
438
  MIN_PASSWORD,
439
+ canUsePasskeys,
334
440
  checkCode,
335
441
  checkEmail,
336
442
  checkName,