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/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;
@@ -277,4 +326,4 @@ interface AuthClientOptions {
277
326
  }
278
327
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
279
328
 
280
- 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;
@@ -277,4 +326,4 @@ interface AuthClientOptions {
277
326
  }
278
327
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
279
328
 
280
- 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()),
@@ -254,6 +323,28 @@ function createAuthClient(options = {}) {
254
323
  if (o.locale) q.set("locale", o.locale);
255
324
  return `${base}/login?${q}`;
256
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
+ }
257
348
  return {
258
349
  signIn: {
259
350
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -262,8 +353,16 @@ function createAuthClient(options = {}) {
262
353
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
263
354
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
264
355
  passkeyUrl,
265
- 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
+ }
266
364
  window.location.assign(passkeyUrl(o));
365
+ return { ok: true, status: "redirected" };
267
366
  },
268
367
  socialUrl,
269
368
  social(provider, o) {
@@ -307,7 +406,16 @@ function createAuthClient(options = {}) {
307
406
  const body = await read("/me/sessions");
308
407
  return body && Array.isArray(body.sessions) ? body.sessions : [];
309
408
  },
310
- 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)
311
419
  },
312
420
  async getSession() {
313
421
  try {
@@ -322,19 +430,13 @@ function createAuthClient(options = {}) {
322
430
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
323
431
  return looked;
324
432
  },
325
- async methods() {
326
- try {
327
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
328
- if (res.ok) return await res.json();
329
- } catch {
330
- }
331
- return { providers: [], signUp: true, magicLink: false, passkey: false };
332
- }
433
+ methods
333
434
  };
334
435
  }
335
436
  // Annotate the CommonJS export names for ESM import in node:
336
437
  0 && (module.exports = {
337
438
  MIN_PASSWORD,
439
+ canUsePasskeys,
338
440
  checkCode,
339
441
  checkEmail,
340
442
  checkName,