cauthy-auth 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -21,6 +21,8 @@ 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
+ canAutofillPasskeys: () => canAutofillPasskeys,
25
+ canUsePasskeys: () => canUsePasskeys,
24
26
  checkCode: () => checkCode,
25
27
  checkEmail: () => checkEmail,
26
28
  checkName: () => checkName,
@@ -31,6 +33,82 @@ __export(client_exports, {
31
33
  });
32
34
  module.exports = __toCommonJS(client_exports);
33
35
 
36
+ // src/passkey.ts
37
+ function canUsePasskeys() {
38
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
39
+ }
40
+ async function canAutofillPasskeys() {
41
+ if (!canUsePasskeys()) return false;
42
+ const pkc = window.PublicKeyCredential;
43
+ try {
44
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ function bytes(text2) {
50
+ const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
51
+ const bin = atob(b64);
52
+ const out = new Uint8Array(bin.length);
53
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
54
+ return out.buffer;
55
+ }
56
+ function text(buf) {
57
+ if (!buf) return null;
58
+ const view = new Uint8Array(buf);
59
+ let bin = "";
60
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
61
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
62
+ }
63
+ var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
64
+ async function getPasskey(options, how = {}) {
65
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
66
+ const cred = await navigator.credentials.get({ publicKey, ...how });
67
+ if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
68
+ const r = cred.response;
69
+ return {
70
+ id: cred.id,
71
+ rawId: text(cred.rawId),
72
+ type: cred.type,
73
+ response: {
74
+ authenticatorData: text(r.authenticatorData),
75
+ clientDataJSON: text(r.clientDataJSON),
76
+ signature: text(r.signature),
77
+ userHandle: text(r.userHandle)
78
+ },
79
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
80
+ };
81
+ }
82
+ async function makePasskey(options) {
83
+ const publicKey = {
84
+ ...options,
85
+ challenge: bytes(options.challenge),
86
+ user: { ...options.user, id: bytes(options.user.id) },
87
+ excludeCredentials: withId(options.excludeCredentials)
88
+ };
89
+ const cred = await navigator.credentials.create({ publicKey });
90
+ if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
91
+ const r = cred.response;
92
+ return {
93
+ id: cred.id,
94
+ rawId: text(cred.rawId),
95
+ type: cred.type,
96
+ response: {
97
+ clientDataJSON: text(r.clientDataJSON),
98
+ attestationObject: text(r.attestationObject),
99
+ transports: typeof r.getTransports === "function" ? r.getTransports() : []
100
+ },
101
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
102
+ };
103
+ }
104
+ function ceremonyError(e) {
105
+ const name = e instanceof Error || e instanceof DOMException ? e.name : "";
106
+ if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
107
+ if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
108
+ if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
109
+ return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
110
+ }
111
+
34
112
  // src/theme.ts
35
113
  function fromWire(wire) {
36
114
  const names = {
@@ -196,7 +274,8 @@ function createSessionStore(options = {}) {
196
274
  email: (f) => then(client.signIn.email(f)),
197
275
  mfa: (f) => then(client.signIn.mfa(f)),
198
276
  magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
199
- emailCode: (f) => then(client.signIn.emailCode(f))
277
+ emailCode: (f) => then(client.signIn.emailCode(f)),
278
+ passkey: (o) => then(client.signIn.passkey(o))
200
279
  },
201
280
  signUp: { email: (f) => then(client.signUp.email(f)) },
202
281
  signOut: () => then(client.signOut()),
@@ -212,6 +291,24 @@ function createSessionStore(options = {}) {
212
291
  }
213
292
 
214
293
  // src/client.ts
294
+ var googleScript = null;
295
+ function loadGoogleIdentity() {
296
+ const ready = () => window.google?.accounts?.id;
297
+ if (ready()) return Promise.resolve(ready());
298
+ googleScript ??= new Promise((resolve, reject) => {
299
+ const script = document.createElement("script");
300
+ script.src = "https://accounts.google.com/gsi/client";
301
+ script.async = true;
302
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
303
+ script.onerror = () => {
304
+ googleScript = null;
305
+ reject(new Error("google identity services did not load"));
306
+ };
307
+ document.head.appendChild(script);
308
+ });
309
+ return googleScript;
310
+ }
311
+ var PASSKEY_SECONDS = 300;
215
312
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
216
313
  function createAuthClient(options = {}) {
217
314
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -254,6 +351,28 @@ function createAuthClient(options = {}) {
254
351
  if (o.locale) q.set("locale", o.locale);
255
352
  return `${base}/login?${q}`;
256
353
  };
354
+ async function methods() {
355
+ try {
356
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
357
+ if (res.ok) {
358
+ const m = await res.json();
359
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
360
+ }
361
+ } catch {
362
+ }
363
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
364
+ }
365
+ async function ceremony(begin, finish, form, run, more = {}) {
366
+ const begun = await post(begin, form, null);
367
+ if (!begun.ok) return begun;
368
+ let credential;
369
+ try {
370
+ credential = await run(begun.options);
371
+ } catch (e) {
372
+ return { ok: false, error: ceremonyError(e) };
373
+ }
374
+ return post(finish, { credential, ...more }, null);
375
+ }
257
376
  return {
258
377
  signIn: {
259
378
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -262,8 +381,76 @@ function createAuthClient(options = {}) {
262
381
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
263
382
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
264
383
  passkeyUrl,
265
- passkey(o) {
384
+ async passkey(o) {
385
+ const m = await methods();
386
+ if (m.passkeyHere && canUsePasskeys()) {
387
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
388
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
389
+ } else if (!m.passkeyOnCauthy) {
390
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
391
+ }
266
392
  window.location.assign(passkeyUrl(o));
393
+ return { ok: true, status: "redirected" };
394
+ },
395
+ async passkeyAutofill(o = {}) {
396
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
397
+ const m = await methods();
398
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
399
+ for (; ; ) {
400
+ if (o.signal?.aborted) return cancelled;
401
+ const begun = await post("/sign-in/passkey/begin", {}, null);
402
+ if (!begun.ok) return begun;
403
+ const renew = new AbortController();
404
+ const stop = () => renew.abort();
405
+ o.signal?.addEventListener("abort", stop, { once: true });
406
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
407
+ let credential;
408
+ try {
409
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
410
+ } catch (e) {
411
+ if (o.signal?.aborted) return cancelled;
412
+ if (renew.signal.aborted) continue;
413
+ return { ok: false, error: ceremonyError(e) };
414
+ } finally {
415
+ clearTimeout(timer);
416
+ o.signal?.removeEventListener("abort", stop);
417
+ }
418
+ return post("/sign-in/passkey/finish", { credential }, null);
419
+ }
420
+ },
421
+ async googleOneTap(o = {}) {
422
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
423
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
424
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
425
+ if (!begun.ok) return begun;
426
+ let gis;
427
+ try {
428
+ gis = await loadGoogleIdentity();
429
+ } catch {
430
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
431
+ }
432
+ const credential = await new Promise((done) => {
433
+ o.signal?.addEventListener("abort", () => {
434
+ gis.cancel();
435
+ done(null);
436
+ }, { once: true });
437
+ gis.initialize({
438
+ client_id: begun.clientId,
439
+ nonce: begun.nonce,
440
+ context: o.context ?? "signin",
441
+ callback: (r) => done(r.credential ?? null),
442
+ auto_select: false,
443
+ cancel_on_tap_outside: true,
444
+ itp_support: true,
445
+ use_fedcm_for_prompt: true
446
+ });
447
+ gis.prompt((moment) => {
448
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
449
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
450
+ });
451
+ });
452
+ if (credential === null) return cancelled;
453
+ return post("/sign-in/google-one-tap", { credential }, null);
267
454
  },
268
455
  socialUrl,
269
456
  social(provider, o) {
@@ -307,7 +494,16 @@ function createAuthClient(options = {}) {
307
494
  const body = await read("/me/sessions");
308
495
  return body && Array.isArray(body.sessions) ? body.sessions : [];
309
496
  },
310
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
497
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
498
+ async passkeys() {
499
+ const body = await read("/me/passkeys");
500
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
501
+ },
502
+ addPasskey(f = {}) {
503
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
504
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
505
+ },
506
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
311
507
  },
312
508
  async getSession() {
313
509
  try {
@@ -322,19 +518,14 @@ function createAuthClient(options = {}) {
322
518
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
323
519
  return looked;
324
520
  },
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
- }
521
+ methods
333
522
  };
334
523
  }
335
524
  // Annotate the CommonJS export names for ESM import in node:
336
525
  0 && (module.exports = {
337
526
  MIN_PASSWORD,
527
+ canAutofillPasskeys,
528
+ canUsePasskeys,
338
529
  checkCode,
339
530
  checkEmail,
340
531
  checkName,