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.mjs CHANGED
@@ -1,3 +1,79 @@
1
+ // src/passkey.ts
2
+ function canUsePasskeys() {
3
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
4
+ }
5
+ async function canAutofillPasskeys() {
6
+ if (!canUsePasskeys()) return false;
7
+ const pkc = window.PublicKeyCredential;
8
+ try {
9
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+ function bytes(text2) {
15
+ const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
16
+ const bin = atob(b64);
17
+ const out = new Uint8Array(bin.length);
18
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
19
+ return out.buffer;
20
+ }
21
+ function text(buf) {
22
+ if (!buf) return null;
23
+ const view = new Uint8Array(buf);
24
+ let bin = "";
25
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
26
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
27
+ }
28
+ var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
29
+ async function getPasskey(options, how = {}) {
30
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
31
+ const cred = await navigator.credentials.get({ publicKey, ...how });
32
+ if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
33
+ const r = cred.response;
34
+ return {
35
+ id: cred.id,
36
+ rawId: text(cred.rawId),
37
+ type: cred.type,
38
+ response: {
39
+ authenticatorData: text(r.authenticatorData),
40
+ clientDataJSON: text(r.clientDataJSON),
41
+ signature: text(r.signature),
42
+ userHandle: text(r.userHandle)
43
+ },
44
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
45
+ };
46
+ }
47
+ async function makePasskey(options) {
48
+ const publicKey = {
49
+ ...options,
50
+ challenge: bytes(options.challenge),
51
+ user: { ...options.user, id: bytes(options.user.id) },
52
+ excludeCredentials: withId(options.excludeCredentials)
53
+ };
54
+ const cred = await navigator.credentials.create({ publicKey });
55
+ if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
56
+ const r = cred.response;
57
+ return {
58
+ id: cred.id,
59
+ rawId: text(cred.rawId),
60
+ type: cred.type,
61
+ response: {
62
+ clientDataJSON: text(r.clientDataJSON),
63
+ attestationObject: text(r.attestationObject),
64
+ transports: typeof r.getTransports === "function" ? r.getTransports() : []
65
+ },
66
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
67
+ };
68
+ }
69
+ function ceremonyError(e) {
70
+ const name = e instanceof Error || e instanceof DOMException ? e.name : "";
71
+ if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
72
+ if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
73
+ if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
74
+ return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
75
+ }
76
+
1
77
  // src/theme.ts
2
78
  function fromWire(wire) {
3
79
  const names = {
@@ -163,7 +239,8 @@ function createSessionStore(options = {}) {
163
239
  email: (f) => then(client.signIn.email(f)),
164
240
  mfa: (f) => then(client.signIn.mfa(f)),
165
241
  magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
166
- emailCode: (f) => then(client.signIn.emailCode(f))
242
+ emailCode: (f) => then(client.signIn.emailCode(f)),
243
+ passkey: (o) => then(client.signIn.passkey(o))
167
244
  },
168
245
  signUp: { email: (f) => then(client.signUp.email(f)) },
169
246
  signOut: () => then(client.signOut()),
@@ -179,6 +256,24 @@ function createSessionStore(options = {}) {
179
256
  }
180
257
 
181
258
  // src/client.ts
259
+ var googleScript = null;
260
+ function loadGoogleIdentity() {
261
+ const ready = () => window.google?.accounts?.id;
262
+ if (ready()) return Promise.resolve(ready());
263
+ googleScript ??= new Promise((resolve, reject) => {
264
+ const script = document.createElement("script");
265
+ script.src = "https://accounts.google.com/gsi/client";
266
+ script.async = true;
267
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
268
+ script.onerror = () => {
269
+ googleScript = null;
270
+ reject(new Error("google identity services did not load"));
271
+ };
272
+ document.head.appendChild(script);
273
+ });
274
+ return googleScript;
275
+ }
276
+ var PASSKEY_SECONDS = 300;
182
277
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
183
278
  function createAuthClient(options = {}) {
184
279
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -221,6 +316,28 @@ function createAuthClient(options = {}) {
221
316
  if (o.locale) q.set("locale", o.locale);
222
317
  return `${base}/login?${q}`;
223
318
  };
319
+ async function methods() {
320
+ try {
321
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
322
+ if (res.ok) {
323
+ const m = await res.json();
324
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
325
+ }
326
+ } catch {
327
+ }
328
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
329
+ }
330
+ async function ceremony(begin, finish, form, run, more = {}) {
331
+ const begun = await post(begin, form, null);
332
+ if (!begun.ok) return begun;
333
+ let credential;
334
+ try {
335
+ credential = await run(begun.options);
336
+ } catch (e) {
337
+ return { ok: false, error: ceremonyError(e) };
338
+ }
339
+ return post(finish, { credential, ...more }, null);
340
+ }
224
341
  return {
225
342
  signIn: {
226
343
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -229,8 +346,76 @@ function createAuthClient(options = {}) {
229
346
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
230
347
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
231
348
  passkeyUrl,
232
- passkey(o) {
349
+ async passkey(o) {
350
+ const m = await methods();
351
+ if (m.passkeyHere && canUsePasskeys()) {
352
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
353
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
354
+ } else if (!m.passkeyOnCauthy) {
355
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
356
+ }
233
357
  window.location.assign(passkeyUrl(o));
358
+ return { ok: true, status: "redirected" };
359
+ },
360
+ async passkeyAutofill(o = {}) {
361
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
362
+ const m = await methods();
363
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
364
+ for (; ; ) {
365
+ if (o.signal?.aborted) return cancelled;
366
+ const begun = await post("/sign-in/passkey/begin", {}, null);
367
+ if (!begun.ok) return begun;
368
+ const renew = new AbortController();
369
+ const stop = () => renew.abort();
370
+ o.signal?.addEventListener("abort", stop, { once: true });
371
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
372
+ let credential;
373
+ try {
374
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
375
+ } catch (e) {
376
+ if (o.signal?.aborted) return cancelled;
377
+ if (renew.signal.aborted) continue;
378
+ return { ok: false, error: ceremonyError(e) };
379
+ } finally {
380
+ clearTimeout(timer);
381
+ o.signal?.removeEventListener("abort", stop);
382
+ }
383
+ return post("/sign-in/passkey/finish", { credential }, null);
384
+ }
385
+ },
386
+ async googleOneTap(o = {}) {
387
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
388
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
389
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
390
+ if (!begun.ok) return begun;
391
+ let gis;
392
+ try {
393
+ gis = await loadGoogleIdentity();
394
+ } catch {
395
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
396
+ }
397
+ const credential = await new Promise((done) => {
398
+ o.signal?.addEventListener("abort", () => {
399
+ gis.cancel();
400
+ done(null);
401
+ }, { once: true });
402
+ gis.initialize({
403
+ client_id: begun.clientId,
404
+ nonce: begun.nonce,
405
+ context: o.context ?? "signin",
406
+ callback: (r) => done(r.credential ?? null),
407
+ auto_select: false,
408
+ cancel_on_tap_outside: true,
409
+ itp_support: true,
410
+ use_fedcm_for_prompt: true
411
+ });
412
+ gis.prompt((moment) => {
413
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
414
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
415
+ });
416
+ });
417
+ if (credential === null) return cancelled;
418
+ return post("/sign-in/google-one-tap", { credential }, null);
234
419
  },
235
420
  socialUrl,
236
421
  social(provider, o) {
@@ -274,7 +459,16 @@ function createAuthClient(options = {}) {
274
459
  const body = await read("/me/sessions");
275
460
  return body && Array.isArray(body.sessions) ? body.sessions : [];
276
461
  },
277
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
462
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
463
+ async passkeys() {
464
+ const body = await read("/me/passkeys");
465
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
466
+ },
467
+ addPasskey(f = {}) {
468
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
469
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
470
+ },
471
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
278
472
  },
279
473
  async getSession() {
280
474
  try {
@@ -289,18 +483,13 @@ function createAuthClient(options = {}) {
289
483
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
290
484
  return looked;
291
485
  },
292
- async methods() {
293
- try {
294
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
295
- if (res.ok) return await res.json();
296
- } catch {
297
- }
298
- return { providers: [], signUp: true, magicLink: false, passkey: false };
299
- }
486
+ methods
300
487
  };
301
488
  }
302
489
  export {
303
490
  MIN_PASSWORD,
491
+ canAutofillPasskeys,
492
+ canUsePasskeys,
304
493
  checkCode,
305
494
  checkEmail,
306
495
  checkName,