cauthy-auth 0.11.0 → 0.13.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
@@ -33,6 +33,15 @@ var import_vue = require("vue");
33
33
  function canUsePasskeys() {
34
34
  return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
35
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
+ }
36
45
  function bytes(text2) {
37
46
  const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
38
47
  const bin = atob(b64);
@@ -48,9 +57,9 @@ function text(buf) {
48
57
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
49
58
  }
50
59
  var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
51
- async function getPasskey(options) {
60
+ async function getPasskey(options, how = {}) {
52
61
  const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
53
- const cred = await navigator.credentials.get({ publicKey });
62
+ const cred = await navigator.credentials.get({ publicKey, ...how });
54
63
  if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
55
64
  const r = cred.response;
56
65
  return {
@@ -170,6 +179,24 @@ function firstProblem(...checks) {
170
179
  }
171
180
 
172
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;
173
200
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
174
201
  function createAuthClient(options = {}) {
175
202
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -178,13 +205,13 @@ function createAuthClient(options = {}) {
178
205
  const said = typeof options.locale === "function" ? options.locale() : options.locale;
179
206
  return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
180
207
  };
181
- async function post(path, form, bad) {
208
+ async function post(path, form, bad, captchaToken) {
182
209
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
183
210
  try {
184
211
  const res = await doFetch(`${base}${path}`, {
185
212
  method: "POST",
186
213
  credentials: "same-origin",
187
- headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
214
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {}, ...captchaToken ? { "x-cauthy-captcha": captchaToken } : {} },
188
215
  body: JSON.stringify(form)
189
216
  });
190
217
  const body = await res.json().catch(() => null);
@@ -236,9 +263,11 @@ function createAuthClient(options = {}) {
236
263
  }
237
264
  return {
238
265
  signIn: {
239
- email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
266
+ email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password)), f.captchaToken),
240
267
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
241
- magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email)),
268
+ mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
269
+ mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
270
+ magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
242
271
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
243
272
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
244
273
  passkeyUrl,
@@ -253,15 +282,75 @@ function createAuthClient(options = {}) {
253
282
  window.location.assign(passkeyUrl(o));
254
283
  return { ok: true, status: "redirected" };
255
284
  },
285
+ async passkeyAutofill(o = {}) {
286
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
287
+ const m = await methods();
288
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
289
+ for (; ; ) {
290
+ if (o.signal?.aborted) return cancelled;
291
+ const begun = await post("/sign-in/passkey/begin", {}, null);
292
+ if (!begun.ok) return begun;
293
+ const renew = new AbortController();
294
+ const stop = () => renew.abort();
295
+ o.signal?.addEventListener("abort", stop, { once: true });
296
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
297
+ let credential;
298
+ try {
299
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
300
+ } catch (e) {
301
+ if (o.signal?.aborted) return cancelled;
302
+ if (renew.signal.aborted) continue;
303
+ return { ok: false, error: ceremonyError(e) };
304
+ } finally {
305
+ clearTimeout(timer);
306
+ o.signal?.removeEventListener("abort", stop);
307
+ }
308
+ return post("/sign-in/passkey/finish", { credential }, null);
309
+ }
310
+ },
311
+ async googleOneTap(o = {}) {
312
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
313
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
314
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
315
+ if (!begun.ok) return begun;
316
+ let gis;
317
+ try {
318
+ gis = await loadGoogleIdentity();
319
+ } catch {
320
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
321
+ }
322
+ const credential = await new Promise((done) => {
323
+ o.signal?.addEventListener("abort", () => {
324
+ gis.cancel();
325
+ done(null);
326
+ }, { once: true });
327
+ gis.initialize({
328
+ client_id: begun.clientId,
329
+ nonce: begun.nonce,
330
+ context: o.context ?? "signin",
331
+ callback: (r) => done(r.credential ?? null),
332
+ auto_select: false,
333
+ cancel_on_tap_outside: true,
334
+ itp_support: true,
335
+ use_fedcm_for_prompt: true
336
+ });
337
+ gis.prompt((moment) => {
338
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
339
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
340
+ });
341
+ });
342
+ if (credential === null) return cancelled;
343
+ return post("/sign-in/google-one-tap", { credential }, null);
344
+ },
256
345
  socialUrl,
257
346
  social(provider, o) {
258
347
  window.location.assign(socialUrl(provider, o));
259
348
  }
260
349
  },
261
350
  signUp: {
262
- email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)))
351
+ email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)), f.captchaToken)
263
352
  },
264
- forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
353
+ forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
265
354
  resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
266
355
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
267
356
  signOut: () => post("/sign-out", {}, null),
@@ -279,7 +368,14 @@ function createAuthClient(options = {}) {
279
368
  setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
280
369
  verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
281
370
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
282
- recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
371
+ recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null),
372
+ async factors() {
373
+ const body = await read("/me/mfa/factors");
374
+ return body && typeof body.totp === "boolean" ? body : null;
375
+ },
376
+ addPhone: (f) => post("/me/mfa/phone", { phone: f.phone, channel: f.channel, password: f.password }, null),
377
+ verifyPhone: (f) => post("/me/mfa/phone/verify", { code: f.code }, checkCode(f.code)),
378
+ removePhone: (f) => post("/me/mfa/phone/remove", { password: f.password }, null)
283
379
  },
284
380
  changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
285
381
  async identities() {