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/vue.js CHANGED
@@ -29,6 +29,82 @@ __export(vue_exports, {
29
29
  module.exports = __toCommonJS(vue_exports);
30
30
  var import_vue = require("vue");
31
31
 
32
+ // src/passkey.ts
33
+ function canUsePasskeys() {
34
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
35
+ }
36
+ async function canAutofillPasskeys() {
37
+ if (!canUsePasskeys()) return false;
38
+ const pkc = window.PublicKeyCredential;
39
+ try {
40
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
41
+ } catch {
42
+ return false;
43
+ }
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, how = {}) {
61
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
62
+ const cred = await navigator.credentials.get({ publicKey, ...how });
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
+
32
108
  // src/theme.ts
33
109
  function fromWire(wire) {
34
110
  const names = {
@@ -103,6 +179,24 @@ function firstProblem(...checks) {
103
179
  }
104
180
 
105
181
  // src/client.ts
182
+ var googleScript = null;
183
+ function loadGoogleIdentity() {
184
+ const ready = () => window.google?.accounts?.id;
185
+ if (ready()) return Promise.resolve(ready());
186
+ googleScript ??= new Promise((resolve, reject) => {
187
+ const script = document.createElement("script");
188
+ script.src = "https://accounts.google.com/gsi/client";
189
+ script.async = true;
190
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
191
+ script.onerror = () => {
192
+ googleScript = null;
193
+ reject(new Error("google identity services did not load"));
194
+ };
195
+ document.head.appendChild(script);
196
+ });
197
+ return googleScript;
198
+ }
199
+ var PASSKEY_SECONDS = 300;
106
200
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
107
201
  function createAuthClient(options = {}) {
108
202
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -145,6 +239,28 @@ function createAuthClient(options = {}) {
145
239
  if (o.locale) q.set("locale", o.locale);
146
240
  return `${base}/login?${q}`;
147
241
  };
242
+ async function methods() {
243
+ try {
244
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
245
+ if (res.ok) {
246
+ const m = await res.json();
247
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
248
+ }
249
+ } catch {
250
+ }
251
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
252
+ }
253
+ async function ceremony(begin, finish, form, run, more = {}) {
254
+ const begun = await post(begin, form, null);
255
+ if (!begun.ok) return begun;
256
+ let credential;
257
+ try {
258
+ credential = await run(begun.options);
259
+ } catch (e) {
260
+ return { ok: false, error: ceremonyError(e) };
261
+ }
262
+ return post(finish, { credential, ...more }, null);
263
+ }
148
264
  return {
149
265
  signIn: {
150
266
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -153,8 +269,76 @@ function createAuthClient(options = {}) {
153
269
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
154
270
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
155
271
  passkeyUrl,
156
- passkey(o) {
272
+ async passkey(o) {
273
+ const m = await methods();
274
+ if (m.passkeyHere && canUsePasskeys()) {
275
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
276
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
277
+ } else if (!m.passkeyOnCauthy) {
278
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
279
+ }
157
280
  window.location.assign(passkeyUrl(o));
281
+ return { ok: true, status: "redirected" };
282
+ },
283
+ async passkeyAutofill(o = {}) {
284
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
285
+ const m = await methods();
286
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
287
+ for (; ; ) {
288
+ if (o.signal?.aborted) return cancelled;
289
+ const begun = await post("/sign-in/passkey/begin", {}, null);
290
+ if (!begun.ok) return begun;
291
+ const renew = new AbortController();
292
+ const stop = () => renew.abort();
293
+ o.signal?.addEventListener("abort", stop, { once: true });
294
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
295
+ let credential;
296
+ try {
297
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
298
+ } catch (e) {
299
+ if (o.signal?.aborted) return cancelled;
300
+ if (renew.signal.aborted) continue;
301
+ return { ok: false, error: ceremonyError(e) };
302
+ } finally {
303
+ clearTimeout(timer);
304
+ o.signal?.removeEventListener("abort", stop);
305
+ }
306
+ return post("/sign-in/passkey/finish", { credential }, null);
307
+ }
308
+ },
309
+ async googleOneTap(o = {}) {
310
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
311
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
312
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
313
+ if (!begun.ok) return begun;
314
+ let gis;
315
+ try {
316
+ gis = await loadGoogleIdentity();
317
+ } catch {
318
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
319
+ }
320
+ const credential = await new Promise((done) => {
321
+ o.signal?.addEventListener("abort", () => {
322
+ gis.cancel();
323
+ done(null);
324
+ }, { once: true });
325
+ gis.initialize({
326
+ client_id: begun.clientId,
327
+ nonce: begun.nonce,
328
+ context: o.context ?? "signin",
329
+ callback: (r) => done(r.credential ?? null),
330
+ auto_select: false,
331
+ cancel_on_tap_outside: true,
332
+ itp_support: true,
333
+ use_fedcm_for_prompt: true
334
+ });
335
+ gis.prompt((moment) => {
336
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
337
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
338
+ });
339
+ });
340
+ if (credential === null) return cancelled;
341
+ return post("/sign-in/google-one-tap", { credential }, null);
158
342
  },
159
343
  socialUrl,
160
344
  social(provider, o) {
@@ -198,7 +382,16 @@ function createAuthClient(options = {}) {
198
382
  const body = await read("/me/sessions");
199
383
  return body && Array.isArray(body.sessions) ? body.sessions : [];
200
384
  },
201
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
385
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
386
+ async passkeys() {
387
+ const body = await read("/me/passkeys");
388
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
389
+ },
390
+ addPasskey(f = {}) {
391
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
392
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
393
+ },
394
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
202
395
  },
203
396
  async getSession() {
204
397
  try {
@@ -213,14 +406,7 @@ function createAuthClient(options = {}) {
213
406
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
214
407
  return looked;
215
408
  },
216
- async methods() {
217
- try {
218
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
219
- if (res.ok) return await res.json();
220
- } catch {
221
- }
222
- return { providers: [], signUp: true, magicLink: false, passkey: false };
223
- }
409
+ methods
224
410
  };
225
411
  }
226
412
 
@@ -316,7 +502,8 @@ function createSessionStore(options = {}) {
316
502
  email: (f) => then(client.signIn.email(f)),
317
503
  mfa: (f) => then(client.signIn.mfa(f)),
318
504
  magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
319
- emailCode: (f) => then(client.signIn.emailCode(f))
505
+ emailCode: (f) => then(client.signIn.emailCode(f)),
506
+ passkey: (o) => then(client.signIn.passkey(o))
320
507
  },
321
508
  signUp: { email: (f) => then(client.signUp.email(f)) },
322
509
  signOut: () => then(client.signOut()),