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