cauthy-auth 0.9.0 → 0.11.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
@@ -1,6 +1,73 @@
1
1
  // src/vue.ts
2
2
  import { inject, shallowReadonly, shallowRef } from "vue";
3
3
 
4
+ // src/passkey.ts
5
+ function canUsePasskeys() {
6
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
7
+ }
8
+ function bytes(text2) {
9
+ const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
10
+ const bin = atob(b64);
11
+ const out = new Uint8Array(bin.length);
12
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
13
+ return out.buffer;
14
+ }
15
+ function text(buf) {
16
+ if (!buf) return null;
17
+ const view = new Uint8Array(buf);
18
+ let bin = "";
19
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
20
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
21
+ }
22
+ var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
23
+ async function getPasskey(options) {
24
+ const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
25
+ const cred = await navigator.credentials.get({ publicKey });
26
+ if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
27
+ const r = cred.response;
28
+ return {
29
+ id: cred.id,
30
+ rawId: text(cred.rawId),
31
+ type: cred.type,
32
+ response: {
33
+ authenticatorData: text(r.authenticatorData),
34
+ clientDataJSON: text(r.clientDataJSON),
35
+ signature: text(r.signature),
36
+ userHandle: text(r.userHandle)
37
+ },
38
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
39
+ };
40
+ }
41
+ async function makePasskey(options) {
42
+ const publicKey = {
43
+ ...options,
44
+ challenge: bytes(options.challenge),
45
+ user: { ...options.user, id: bytes(options.user.id) },
46
+ excludeCredentials: withId(options.excludeCredentials)
47
+ };
48
+ const cred = await navigator.credentials.create({ publicKey });
49
+ if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
50
+ const r = cred.response;
51
+ return {
52
+ id: cred.id,
53
+ rawId: text(cred.rawId),
54
+ type: cred.type,
55
+ response: {
56
+ clientDataJSON: text(r.clientDataJSON),
57
+ attestationObject: text(r.attestationObject),
58
+ transports: typeof r.getTransports === "function" ? r.getTransports() : []
59
+ },
60
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
61
+ };
62
+ }
63
+ function ceremonyError(e) {
64
+ const name = e instanceof Error || e instanceof DOMException ? e.name : "";
65
+ if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
66
+ if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
67
+ if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
68
+ return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
69
+ }
70
+
4
71
  // src/theme.ts
5
72
  function fromWire(wire) {
6
73
  const names = {
@@ -79,13 +146,17 @@ var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right
79
146
  function createAuthClient(options = {}) {
80
147
  const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
81
148
  const doFetch = (...args) => (options.fetch ?? fetch)(...args);
149
+ const locale = () => {
150
+ const said = typeof options.locale === "function" ? options.locale() : options.locale;
151
+ return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
152
+ };
82
153
  async function post(path, form, bad) {
83
154
  if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
84
155
  try {
85
156
  const res = await doFetch(`${base}${path}`, {
86
157
  method: "POST",
87
158
  credentials: "same-origin",
88
- headers: { "content-type": "application/json", accept: "application/json" },
159
+ headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
89
160
  body: JSON.stringify(form)
90
161
  });
91
162
  const body = await res.json().catch(() => null);
@@ -113,6 +184,28 @@ function createAuthClient(options = {}) {
113
184
  if (o.locale) q.set("locale", o.locale);
114
185
  return `${base}/login?${q}`;
115
186
  };
187
+ async function methods() {
188
+ try {
189
+ const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
190
+ if (res.ok) {
191
+ const m = await res.json();
192
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
193
+ }
194
+ } catch {
195
+ }
196
+ return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
197
+ }
198
+ async function ceremony(begin, finish, form, run, more = {}) {
199
+ const begun = await post(begin, form, null);
200
+ if (!begun.ok) return begun;
201
+ let credential;
202
+ try {
203
+ credential = await run(begun.options);
204
+ } catch (e) {
205
+ return { ok: false, error: ceremonyError(e) };
206
+ }
207
+ return post(finish, { credential, ...more }, null);
208
+ }
116
209
  return {
117
210
  signIn: {
118
211
  email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
@@ -121,8 +214,16 @@ function createAuthClient(options = {}) {
121
214
  magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
122
215
  emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
123
216
  passkeyUrl,
124
- passkey(o) {
217
+ async passkey(o) {
218
+ const m = await methods();
219
+ if (m.passkeyHere && canUsePasskeys()) {
220
+ const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
221
+ if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
222
+ } else if (!m.passkeyOnCauthy) {
223
+ return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
224
+ }
125
225
  window.location.assign(passkeyUrl(o));
226
+ return { ok: true, status: "redirected" };
126
227
  },
127
228
  socialUrl,
128
229
  social(provider, o) {
@@ -166,7 +267,16 @@ function createAuthClient(options = {}) {
166
267
  const body = await read("/me/sessions");
167
268
  return body && Array.isArray(body.sessions) ? body.sessions : [];
168
269
  },
169
- signOutEverywhere: () => post("/me/sessions/end", {}, null)
270
+ signOutEverywhere: () => post("/me/sessions/end", {}, null),
271
+ async passkeys() {
272
+ const body = await read("/me/passkeys");
273
+ return body && Array.isArray(body.passkeys) ? body.passkeys : [];
274
+ },
275
+ addPasskey(f = {}) {
276
+ if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
277
+ return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
278
+ },
279
+ removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
170
280
  },
171
281
  async getSession() {
172
282
  try {
@@ -181,14 +291,7 @@ function createAuthClient(options = {}) {
181
291
  looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
182
292
  return looked;
183
293
  },
184
- async methods() {
185
- try {
186
- const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
187
- if (res.ok) return await res.json();
188
- } catch {
189
- }
190
- return { providers: [], signUp: true, magicLink: false, passkey: false };
191
- }
294
+ methods
192
295
  };
193
296
  }
194
297
 
@@ -284,7 +387,8 @@ function createSessionStore(options = {}) {
284
387
  email: (f) => then(client.signIn.email(f)),
285
388
  mfa: (f) => then(client.signIn.mfa(f)),
286
389
  magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
287
- emailCode: (f) => then(client.signIn.emailCode(f))
390
+ emailCode: (f) => then(client.signIn.emailCode(f)),
391
+ passkey: (o) => then(client.signIn.passkey(o))
288
392
  },
289
393
  signUp: { email: (f) => then(client.signUp.email(f)) },
290
394
  signOut: () => then(client.signOut()),