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.mjs CHANGED
@@ -5,6 +5,15 @@ import { inject, shallowReadonly, shallowRef } from "vue";
5
5
  function canUsePasskeys() {
6
6
  return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
7
7
  }
8
+ async function canAutofillPasskeys() {
9
+ if (!canUsePasskeys()) return false;
10
+ const pkc = window.PublicKeyCredential;
11
+ try {
12
+ return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
8
17
  function bytes(text2) {
9
18
  const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
10
19
  const bin = atob(b64);
@@ -20,9 +29,9 @@ function text(buf) {
20
29
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
21
30
  }
22
31
  var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
23
- async function getPasskey(options) {
32
+ async function getPasskey(options, how = {}) {
24
33
  const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
25
- const cred = await navigator.credentials.get({ publicKey });
34
+ const cred = await navigator.credentials.get({ publicKey, ...how });
26
35
  if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
27
36
  const r = cred.response;
28
37
  return {
@@ -142,6 +151,24 @@ function firstProblem(...checks) {
142
151
  }
143
152
 
144
153
  // src/client.ts
154
+ var googleScript = null;
155
+ function loadGoogleIdentity() {
156
+ const ready = () => window.google?.accounts?.id;
157
+ if (ready()) return Promise.resolve(ready());
158
+ googleScript ??= new Promise((resolve, reject) => {
159
+ const script = document.createElement("script");
160
+ script.src = "https://accounts.google.com/gsi/client";
161
+ script.async = true;
162
+ script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
163
+ script.onerror = () => {
164
+ googleScript = null;
165
+ reject(new Error("google identity services did not load"));
166
+ };
167
+ document.head.appendChild(script);
168
+ });
169
+ return googleScript;
170
+ }
171
+ var PASSKEY_SECONDS = 300;
145
172
  var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
146
173
  function createAuthClient(options = {}) {
147
174
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
@@ -150,13 +177,13 @@ function createAuthClient(options = {}) {
150
177
  const said = typeof options.locale === "function" ? options.locale() : options.locale;
151
178
  return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
152
179
  };
153
- async function post(path, form, bad) {
180
+ async function post(path, form, bad, captchaToken) {
154
181
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
155
182
  try {
156
183
  const res = await doFetch(`${base}${path}`, {
157
184
  method: "POST",
158
185
  credentials: "same-origin",
159
- headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
186
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {}, ...captchaToken ? { "x-cauthy-captcha": captchaToken } : {} },
160
187
  body: JSON.stringify(form)
161
188
  });
162
189
  const body = await res.json().catch(() => null);
@@ -208,9 +235,11 @@ function createAuthClient(options = {}) {
208
235
  }
209
236
  return {
210
237
  signIn: {
211
- email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
238
+ email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password)), f.captchaToken),
212
239
  mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
213
- magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email)),
240
+ mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
241
+ mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
242
+ magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
214
243
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
215
244
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
216
245
  passkeyUrl,
@@ -225,15 +254,75 @@ function createAuthClient(options = {}) {
225
254
  window.location.assign(passkeyUrl(o));
226
255
  return { ok: true, status: "redirected" };
227
256
  },
257
+ async passkeyAutofill(o = {}) {
258
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
259
+ const m = await methods();
260
+ if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
261
+ for (; ; ) {
262
+ if (o.signal?.aborted) return cancelled;
263
+ const begun = await post("/sign-in/passkey/begin", {}, null);
264
+ if (!begun.ok) return begun;
265
+ const renew = new AbortController();
266
+ const stop = () => renew.abort();
267
+ o.signal?.addEventListener("abort", stop, { once: true });
268
+ const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
269
+ let credential;
270
+ try {
271
+ credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
272
+ } catch (e) {
273
+ if (o.signal?.aborted) return cancelled;
274
+ if (renew.signal.aborted) continue;
275
+ return { ok: false, error: ceremonyError(e) };
276
+ } finally {
277
+ clearTimeout(timer);
278
+ o.signal?.removeEventListener("abort", stop);
279
+ }
280
+ return post("/sign-in/passkey/finish", { credential }, null);
281
+ }
282
+ },
283
+ async googleOneTap(o = {}) {
284
+ const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
285
+ if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
286
+ const begun = await post("/sign-in/google-one-tap/begin", {}, null);
287
+ if (!begun.ok) return begun;
288
+ let gis;
289
+ try {
290
+ gis = await loadGoogleIdentity();
291
+ } catch {
292
+ return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
293
+ }
294
+ const credential = await new Promise((done) => {
295
+ o.signal?.addEventListener("abort", () => {
296
+ gis.cancel();
297
+ done(null);
298
+ }, { once: true });
299
+ gis.initialize({
300
+ client_id: begun.clientId,
301
+ nonce: begun.nonce,
302
+ context: o.context ?? "signin",
303
+ callback: (r) => done(r.credential ?? null),
304
+ auto_select: false,
305
+ cancel_on_tap_outside: true,
306
+ itp_support: true,
307
+ use_fedcm_for_prompt: true
308
+ });
309
+ gis.prompt((moment) => {
310
+ if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
311
+ else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
312
+ });
313
+ });
314
+ if (credential === null) return cancelled;
315
+ return post("/sign-in/google-one-tap", { credential }, null);
316
+ },
228
317
  socialUrl,
229
318
  social(provider, o) {
230
319
  window.location.assign(socialUrl(provider, o));
231
320
  }
232
321
  },
233
322
  signUp: {
234
- 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)))
323
+ 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)
235
324
  },
236
- forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
325
+ forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
237
326
  resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
238
327
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
239
328
  signOut: () => post("/sign-out", {}, null),
@@ -251,7 +340,14 @@ function createAuthClient(options = {}) {
251
340
  setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
252
341
  verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
253
342
  disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
254
- recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
343
+ recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null),
344
+ async factors() {
345
+ const body = await read("/me/mfa/factors");
346
+ return body && typeof body.totp === "boolean" ? body : null;
347
+ },
348
+ addPhone: (f) => post("/me/mfa/phone", { phone: f.phone, channel: f.channel, password: f.password }, null),
349
+ verifyPhone: (f) => post("/me/mfa/phone/verify", { code: f.code }, checkCode(f.code)),
350
+ removePhone: (f) => post("/me/mfa/phone/remove", { password: f.password }, null)
255
351
  },
256
352
  changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
257
353
  async identities() {