@voltro/plugin-auth 0.85.0 → 0.87.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +5987 -0
  2. package/README.md +93 -0
  3. package/THIRD-PARTY-NOTICES.md +1122 -31
  4. package/dist/admission.js +9 -0
  5. package/dist/appUserStore.js +19 -0
  6. package/dist/authExtensions.js +9 -0
  7. package/dist/emailVerification.js +64 -0
  8. package/dist/guest.d.ts +99 -0
  9. package/dist/guest.js +3 -0
  10. package/dist/guestClient.js +237 -0
  11. package/dist/guestCredential.js +12 -0
  12. package/dist/guestPasswordUpgrade.js +85 -0
  13. package/dist/guestPlugin.js +166 -0
  14. package/dist/guestSchema.js +20 -0
  15. package/dist/guestSecrets.js +10 -0
  16. package/dist/guestService.js +224 -0
  17. package/dist/handlers.js +337 -0
  18. package/dist/impersonation.js +176 -0
  19. package/dist/index.d.ts +282 -20
  20. package/dist/index.js +30 -88
  21. package/dist/invitations.js +187 -0
  22. package/dist/mail.js +10 -0
  23. package/dist/memberships.js +8 -0
  24. package/dist/native/guestStorage.js +41 -0
  25. package/dist/native.d.ts +110 -0
  26. package/dist/native.js +45 -43
  27. package/dist/nativeRoutes.js +109 -0
  28. package/dist/overlayPlugin.js +167 -0
  29. package/dist/passkeyHandlers.js +162 -0
  30. package/dist/plugin.d.ts +123 -10
  31. package/dist/plugin.js +519 -5
  32. package/dist/postgresUserStore.js +475 -0
  33. package/dist/revocation.js +50 -0
  34. package/dist/schema.d.ts +45 -6
  35. package/dist/schema.js +99 -96
  36. package/dist/sessionFamily.js +15 -0
  37. package/dist/sessionRegistry.js +133 -0
  38. package/dist/signedClaims.js +24 -0
  39. package/dist/strategy.d.ts +17 -9
  40. package/dist/strategy.js +69 -3
  41. package/dist/subjectGuard.js +11 -0
  42. package/dist/totp.js +55 -0
  43. package/dist/userStore.js +292 -0
  44. package/dist/web/guestStorage.js +47 -0
  45. package/dist/web/passkeyClient.js +78 -0
  46. package/dist/web/requireAuth.js +9 -0
  47. package/dist/web/subjectContext.js +13 -0
  48. package/dist/web.d.ts +94 -0
  49. package/dist/web.js +6 -90
  50. package/package.json +8 -3
  51. package/dist/plugin-CbmcyEJs.js +0 -2018
  52. package/dist/strategy-XaGjSicX.js +0 -353
@@ -0,0 +1,292 @@
1
+ import { hasOwnedSessionFamily as e, indexSessionFamilies as t, sessionFamilyId as n } from "./sessionFamily.js";
2
+ import { Data as r, Effect as i } from "effect";
3
+ import { generateId as a } from "@voltro/database";
4
+ //#region src/userStore.ts
5
+ var o = class extends r.TaggedError("UserAlreadyExistsError") {}, s = class extends r.TaggedError("UserNotFoundError") {}, c = (e) => a({
6
+ kind: "typeid",
7
+ prefix: e
8
+ }, e), l = (r = []) => {
9
+ let a = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), u = [], d = [], f = [], p = [], m = [], h = [], g = [], _ = /* @__PURE__ */ new Map();
10
+ for (let e of r) a.set(e.id, e), e.email !== null && l.set(e.email.toLowerCase(), e);
11
+ let v = (e, t) => i.gen(function* () {
12
+ let n = a.get(e);
13
+ if (!n) return yield* i.fail(new s({ userId: e }));
14
+ let r = t(n);
15
+ return a.set(e, r), r.email !== null && l.set(r.email.toLowerCase(), r), r;
16
+ });
17
+ return {
18
+ findByEmail: (e) => i.sync(() => l.get(e.toLowerCase()) ?? null),
19
+ findById: (e) => i.sync(() => a.get(e) ?? null),
20
+ insert: (e) => i.gen(function* () {
21
+ let t = e.email === null ? null : e.email.toLowerCase();
22
+ if (t !== null && l.has(t)) return yield* i.fail(new o({ email: t }));
23
+ let n = {
24
+ ...e,
25
+ email: t,
26
+ createdAt: /* @__PURE__ */ new Date()
27
+ };
28
+ return a.set(n.id, n), t !== null && l.set(t, n), n;
29
+ }),
30
+ updatePassword: (e, t) => v(e, (e) => ({
31
+ ...e,
32
+ passwordHash: t
33
+ })),
34
+ setMfaSecret: (e, t) => v(e, (e) => ({
35
+ ...e,
36
+ mfaSecret: t,
37
+ mfaEnrolledAt: null
38
+ })),
39
+ markMfaEnrolled: (e) => v(e, (e) => ({
40
+ ...e,
41
+ mfaEnrolledAt: /* @__PURE__ */ new Date()
42
+ })),
43
+ clearMfa: (e) => v(e, (e) => ({
44
+ ...e,
45
+ mfaSecret: null,
46
+ mfaEnrolledAt: null
47
+ })),
48
+ markEmailVerified: (e) => v(e, (e) => e.emailVerifiedAt ? e : {
49
+ ...e,
50
+ emailVerifiedAt: /* @__PURE__ */ new Date()
51
+ }),
52
+ listMemberships: (e) => i.sync(() => u.filter((t) => t.userId === e)),
53
+ addMembership: (e) => i.sync(() => {
54
+ let t = u.findIndex((t) => t.userId === e.userId && t.tenantId === e.tenantId), n = {
55
+ ...e,
56
+ joinedAt: /* @__PURE__ */ new Date()
57
+ };
58
+ if (t >= 0) {
59
+ let e = u[t], r = {
60
+ ...n,
61
+ joinedAt: e.joinedAt
62
+ };
63
+ return u[t] = r, r;
64
+ }
65
+ return u.push(n), n;
66
+ }),
67
+ membershipRole: (e, t) => i.sync(() => u.find((n) => n.userId === e && n.tenantId === t)?.role ?? null),
68
+ listSessions: (r) => i.sync(() => {
69
+ let i = t(d);
70
+ return d.filter((t) => t.userId === r && t.rotatedAt == null && i.get(t.id) === t && e(t, i.get(n(t) ?? ""))).map((e) => structuredClone(e)).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime());
71
+ }),
72
+ insertSession: (e) => i.sync(() => {
73
+ let t = /* @__PURE__ */ new Date(), n = {
74
+ ...e,
75
+ familyId: e.familyId ?? e.id,
76
+ rotatedAt: e.rotatedAt ?? null,
77
+ createdAt: t,
78
+ lastSeenAt: t
79
+ };
80
+ return d.push(structuredClone(n)), structuredClone(n);
81
+ }),
82
+ findSession: (r) => i.sync(() => {
83
+ let i = t(d), a = i.get(r);
84
+ return a && a.rotatedAt == null && e(a, i.get(n(a) ?? "")) ? structuredClone(a) : null;
85
+ }),
86
+ touchSession: (e, t) => i.sync(() => {
87
+ let n = d.findIndex((t) => t.id === e);
88
+ if (n >= 0 && d[n].rotatedAt == null) {
89
+ d[n] = {
90
+ ...d[n],
91
+ expiresAt: t,
92
+ lastSeenAt: /* @__PURE__ */ new Date()
93
+ };
94
+ let r = d[n].familyId ?? e, i = d.findIndex((e) => e.id === r);
95
+ i >= 0 && d[i].expiresAt < t && (d[i] = {
96
+ ...d[i],
97
+ expiresAt: t
98
+ });
99
+ }
100
+ }),
101
+ revokeSession: (e, t) => i.sync(() => {
102
+ let n = d.find((n) => n.id === t && n.userId === e);
103
+ if (!n) return !1;
104
+ let r = n.familyId ?? n.id, i = !1;
105
+ for (let t = d.length - 1; t >= 0; t--) {
106
+ let n = d[t];
107
+ n.userId === e && (n.familyId ?? n.id) === r && (d.splice(t, 1), i = !0);
108
+ }
109
+ return i;
110
+ }),
111
+ revokeAllOtherSessions: (e, t) => i.sync(() => {
112
+ let n = d.find((n) => n.id === t && n.userId === e), r = n ? n.familyId ?? n.id : void 0, i = 0;
113
+ for (let t = d.length - 1; t >= 0; t--) {
114
+ let n = d[t];
115
+ n.userId === e && (n.familyId ?? n.id) !== r && (d.splice(t, 1), n.rotatedAt ?? (i += 1));
116
+ }
117
+ return i;
118
+ }),
119
+ revokeAllSessions: (e) => i.sync(() => {
120
+ let t = 0;
121
+ for (let n = d.length - 1; n >= 0; n--) if (d[n].userId === e) {
122
+ let e = d[n];
123
+ d.splice(n, 1), e.rotatedAt ?? t++;
124
+ }
125
+ return t;
126
+ }),
127
+ insertToken: (e) => i.sync(() => {
128
+ let t = {
129
+ ...e,
130
+ consumedAt: null,
131
+ createdAt: /* @__PURE__ */ new Date()
132
+ };
133
+ return f.push(t), t;
134
+ }),
135
+ consumeToken: (e, t) => i.sync(() => {
136
+ let n = Date.now(), r = f.find((r) => r.tokenHash === e && r.purpose === t && r.consumedAt === null && r.expiresAt.getTime() > n);
137
+ if (!r) return null;
138
+ let i = {
139
+ ...r,
140
+ consumedAt: /* @__PURE__ */ new Date()
141
+ }, a = f.indexOf(r);
142
+ return f[a] = i, i;
143
+ }),
144
+ latestToken: (e, t) => i.sync(() => {
145
+ let n = null;
146
+ for (let r of f) r.userId === e && r.purpose === t && (n === null || r.createdAt.getTime() > n.createdAt.getTime()) && (n = r);
147
+ return n;
148
+ }),
149
+ insertInvitation: (e) => i.sync(() => {
150
+ let t = {
151
+ ...e,
152
+ email: e.email.toLowerCase(),
153
+ acceptedAt: null,
154
+ acceptedByUserId: null,
155
+ revokedAt: null,
156
+ revokedByUserId: null,
157
+ createdAt: /* @__PURE__ */ new Date()
158
+ };
159
+ return h.push(t), t;
160
+ }),
161
+ findInvitationByHash: (e) => i.sync(() => h.find((t) => t.tokenHash === e) ?? null),
162
+ acceptInvitation: (e, t, n) => i.sync(() => {
163
+ let r = h.findIndex((t) => t.tokenHash === e && t.acceptedAt === null && t.revokedAt === null && t.expiresAt.getTime() > n.getTime());
164
+ if (r < 0) return null;
165
+ let i = {
166
+ ...h[r],
167
+ acceptedAt: n,
168
+ acceptedByUserId: t
169
+ };
170
+ return h[r] = i, i;
171
+ }),
172
+ revokeInvitation: (e, t, n, r) => i.sync(() => {
173
+ let i = h.findIndex((n) => n.id === e && n.tenantId === t && n.acceptedAt === null && n.revokedAt === null);
174
+ return i < 0 ? !1 : (h[i] = {
175
+ ...h[i],
176
+ revokedAt: r,
177
+ revokedByUserId: n
178
+ }, !0);
179
+ }),
180
+ listInvitations: (e) => i.sync(() => h.filter((t) => t.tenantId === e).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime())),
181
+ countPendingInvitations: (e, t) => i.sync(() => h.filter((n) => n.tenantId === e && n.acceptedAt === null && n.revokedAt === null && n.expiresAt.getTime() > t.getTime()).length),
182
+ supersedePendingInvitations: (e, t, n, r) => i.sync(() => {
183
+ let i = t.toLowerCase(), a = 0;
184
+ for (let t = 0; t < h.length; t++) {
185
+ let o = h[t];
186
+ o.tenantId === e && o.email === i && o.acceptedAt === null && o.revokedAt === null && (h[t] = {
187
+ ...o,
188
+ revokedAt: r,
189
+ revokedByUserId: n
190
+ }, a++);
191
+ }
192
+ return a;
193
+ }),
194
+ insertImpersonationGrant: (e) => i.sync(() => {
195
+ let t = {
196
+ ...e,
197
+ startedAt: /* @__PURE__ */ new Date(),
198
+ endedAt: null,
199
+ endedReason: null
200
+ };
201
+ return g.push(t), t;
202
+ }),
203
+ findImpersonationGrantBySession: (e) => i.sync(() => g.find((t) => t.sessionId === e) ?? null),
204
+ endImpersonationGrant: (e, t, n) => i.sync(() => {
205
+ let r = g.findIndex((t) => t.sessionId === e && t.endedAt === null);
206
+ return r < 0 ? !1 : (g[r] = {
207
+ ...g[r],
208
+ endedAt: n,
209
+ endedReason: t
210
+ }, !0);
211
+ }),
212
+ replaceRecoveryCodes: (e, t) => i.sync(() => {
213
+ for (let t = m.length - 1; t >= 0; t--) m[t].userId === e && m.splice(t, 1);
214
+ let n = /* @__PURE__ */ new Date();
215
+ for (let r of t) m.push({
216
+ id: c("rc"),
217
+ userId: e,
218
+ codeHash: r,
219
+ consumedAt: null,
220
+ createdAt: n
221
+ });
222
+ }),
223
+ consumeRecoveryCode: (e, t) => i.sync(() => {
224
+ let n = m.findIndex((n) => n.userId === e && n.codeHash === t && n.consumedAt === null);
225
+ return n < 0 ? !1 : (m[n] = {
226
+ ...m[n],
227
+ consumedAt: /* @__PURE__ */ new Date()
228
+ }, !0);
229
+ }),
230
+ countRecoveryCodes: (e) => i.sync(() => m.filter((t) => t.userId === e && t.consumedAt === null).length),
231
+ listPasskeys: (e) => i.sync(() => p.filter((t) => t.userId === e)),
232
+ findPasskey: (e) => i.sync(() => p.find((t) => t.credentialId === e) ?? null),
233
+ insertPasskey: (e) => i.sync(() => {
234
+ let t = /* @__PURE__ */ new Date(), n = {
235
+ ...e,
236
+ createdAt: t,
237
+ lastUsedAt: null
238
+ };
239
+ return p.push(n), n;
240
+ }),
241
+ advancePasskeyCounter: (e, t) => i.sync(() => {
242
+ let n = p.findIndex((t) => t.credentialId === e);
243
+ return n < 0 || t <= p[n].counter ? !1 : (p[n] = {
244
+ ...p[n],
245
+ counter: t,
246
+ lastUsedAt: /* @__PURE__ */ new Date()
247
+ }, !0);
248
+ }),
249
+ isLockedOut: (e, t) => i.sync(() => {
250
+ let n = _.get(e.toLowerCase());
251
+ return n?.lockedUntil != null && t < n.lockedUntil ? {
252
+ locked: !0,
253
+ retryAfterSeconds: Math.ceil((n.lockedUntil - t) / 1e3)
254
+ } : {
255
+ locked: !1,
256
+ retryAfterSeconds: 0
257
+ };
258
+ }),
259
+ recordLoginFailure: (e, t, n) => i.sync(() => {
260
+ let r = e.toLowerCase(), i = _.get(r), a = i?.windowStartedAt == null || t - i.windowStartedAt >= n.windowMs, o = i?.lockedUntil != null && t >= i.lockedUntil, s = !i || a || o, c = s ? 1 : i.failedCount + 1, l = s ? t : i.windowStartedAt, u = c >= n.maxAttempts ? t + n.lockMs : null;
261
+ _.set(r, {
262
+ failedCount: c,
263
+ windowStartedAt: l,
264
+ lockedUntil: u
265
+ });
266
+ }),
267
+ clearLoginFailures: (e) => i.sync(() => {
268
+ _.delete(e.toLowerCase());
269
+ })
270
+ };
271
+ }, u = (e, t = {}) => {
272
+ let n = { ...t.metadata };
273
+ return t.memberships && (n.memberships = t.memberships.map((e) => ({
274
+ tenantId: e.tenantId,
275
+ role: e.role
276
+ }))), {
277
+ type: "user",
278
+ id: e.id,
279
+ tenantId: t.tenantId ?? e.tenantId,
280
+ ...Object.keys(n).length > 0 ? { metadata: n } : {}
281
+ };
282
+ }, d = (e) => {
283
+ if (e.type !== "user") return null;
284
+ let t = e.metadata?.sessionId;
285
+ return typeof t == "string" && t.length > 0 ? t : null;
286
+ }, f = (e) => {
287
+ if (e.type !== "user") return [];
288
+ let t = e.metadata?.memberships;
289
+ return Array.isArray(t) ? t.filter((e) => typeof e == "object" && !!e && typeof e.tenantId == "string" && typeof e.role == "string") : [];
290
+ };
291
+ //#endregion
292
+ export { o as UserAlreadyExistsError, s as UserNotFoundError, l as memoryUserStore, c as newAuthId, d as sessionIdOfSubject, u as subjectFromUser, f as subjectMemberships };
@@ -0,0 +1,47 @@
1
+ //#region src/web/guestStorage.ts
2
+ var e = (e) => {
3
+ if (!e.trim()) throw Error("Guest storage requires an app namespace");
4
+ let t = (t, n) => new Promise((r, i) => {
5
+ let a = !1, o = globalThis.indexedDB.open("voltro:guest:" + e, 1);
6
+ o.onupgradeneeded = () => {
7
+ o.result.createObjectStore("credentials");
8
+ }, o.onerror = () => i(/* @__PURE__ */ Error("Guest IndexedDB unavailable")), o.onblocked = () => {
9
+ a = !0, i(/* @__PURE__ */ Error("Guest IndexedDB blocked"));
10
+ }, o.onsuccess = () => {
11
+ let e = o.result;
12
+ if (a) {
13
+ e.close();
14
+ return;
15
+ }
16
+ let s, c;
17
+ try {
18
+ s = e.transaction("credentials", t), c = n(s.objectStore("credentials"));
19
+ } catch {
20
+ e.close(), i(/* @__PURE__ */ Error("Guest IndexedDB transaction unavailable"));
21
+ return;
22
+ }
23
+ s.oncomplete = () => {
24
+ e.close(), r(c.result);
25
+ }, s.onerror = s.onabort = () => {
26
+ e.close(), i(/* @__PURE__ */ Error("Guest IndexedDB transaction failed"));
27
+ };
28
+ };
29
+ });
30
+ return {
31
+ exclusive: (t) => globalThis.navigator?.locks ? globalThis.navigator.locks.request("voltro:guest:" + e, t) : Promise.reject(/* @__PURE__ */ Error("Guest cross-tab locking unavailable")),
32
+ read: async () => {
33
+ let e = await t("readonly", (e) => e.get("device"));
34
+ if (e === void 0) return null;
35
+ if (typeof e != "string" || !e) throw Error("Guest IndexedDB credential invalid");
36
+ return e;
37
+ },
38
+ write: async (e) => {
39
+ await t("readwrite", (t) => t.put(e, "device"));
40
+ },
41
+ remove: async () => {
42
+ await t("readwrite", (e) => e.delete("device"));
43
+ }
44
+ };
45
+ };
46
+ //#endregion
47
+ export { e as indexedDbGuestStore };
@@ -0,0 +1,78 @@
1
+ //#region src/web/passkeyClient.ts
2
+ var e = (e) => {
3
+ let t = new Uint8Array(e), n = "";
4
+ for (let e of t) n += String.fromCharCode(e);
5
+ return btoa(n).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }, t = (e) => {
7
+ let t = e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4), n = atob(t), r = new Uint8Array(n.length);
8
+ for (let e = 0; e < n.length; e++) r[e] = n.charCodeAt(e);
9
+ return r.buffer;
10
+ }, n = () => typeof window < "u" && window.PublicKeyCredential !== void 0 && typeof navigator < "u" && !!navigator.credentials, r = async (e, t, n) => await (await (n.fetchImpl ?? fetch)(e, {
11
+ method: "POST",
12
+ credentials: "include",
13
+ headers: {
14
+ "content-type": "application/json",
15
+ ...n.csrfToken ? { "x-csrf-token": n.csrfToken } : {}
16
+ },
17
+ body: JSON.stringify(t)
18
+ })).json(), i = async (i, a = {}) => {
19
+ if (!n()) throw Error("passkeys not supported in this browser");
20
+ let o = a.basePath ?? "/auth", s = await r(`${o}/passkey/register/options`, i, a), c = s.challenge, l = s.rp, u = s.user, d = s.excludeCredentials ?? [], f = await navigator.credentials.create({ publicKey: {
21
+ challenge: t(c),
22
+ rp: l,
23
+ user: {
24
+ id: new TextEncoder().encode(u.id),
25
+ name: u.name,
26
+ displayName: u.displayName
27
+ },
28
+ pubKeyCredParams: [{
29
+ type: "public-key",
30
+ alg: -7
31
+ }, {
32
+ type: "public-key",
33
+ alg: -257
34
+ }],
35
+ excludeCredentials: d.map((e) => ({
36
+ type: "public-key",
37
+ id: t(e.id)
38
+ })),
39
+ authenticatorSelection: {
40
+ userVerification: "preferred",
41
+ residentKey: "preferred"
42
+ },
43
+ timeout: 3e5
44
+ } });
45
+ if (!f) throw Error("credential creation was cancelled");
46
+ let p = f.response, m = typeof p.getAuthenticatorData == "function" ? p.getAuthenticatorData() : /* @__PURE__ */ new ArrayBuffer(0), h = await r(`${o}/passkey/register/verify`, {
47
+ userId: i.userId,
48
+ credentialId: e(f.rawId),
49
+ clientDataJSON: e(p.clientDataJSON),
50
+ authenticatorData: e(m)
51
+ }, a);
52
+ if (!h.ok) throw Error(`passkey registration failed: ${String(h.error)}`);
53
+ return { credentialId: h.credentialId };
54
+ }, a = async (i = {}, a = {}) => {
55
+ if (!n()) throw Error("passkeys not supported in this browser");
56
+ let o = a.basePath ?? "/auth", s = await r(`${o}/passkey/assert/options`, i.userId ? { userId: i.userId } : {}, a), c = s.challenge, l = s.rpId, u = s.allowCredentials ?? [], d = await navigator.credentials.get({ publicKey: {
57
+ challenge: t(c),
58
+ rpId: l,
59
+ allowCredentials: u.map((e) => ({
60
+ type: "public-key",
61
+ id: t(e.id)
62
+ })),
63
+ userVerification: "preferred",
64
+ timeout: 3e5
65
+ } });
66
+ if (!d) throw Error("assertion was cancelled");
67
+ let f = d.response, p = await r(`${o}/passkey/assert/verify`, {
68
+ credentialId: e(d.rawId),
69
+ clientDataJSON: e(f.clientDataJSON),
70
+ authenticatorData: e(f.authenticatorData),
71
+ signature: e(f.signature),
72
+ ...i.userId ? { userId: i.userId } : {}
73
+ }, a);
74
+ if (!p.ok) throw Error(`passkey sign-in failed: ${String(p.error)}`);
75
+ return { subject: p.subject };
76
+ };
77
+ //#endregion
78
+ export { n as isPasskeySupported, i as registerPasskey, a as signInWithPasskey };
@@ -0,0 +1,9 @@
1
+ import { useOptionalSubject as e } from "./subjectContext.js";
2
+ import { Fragment as t, jsx as n } from "react/jsx-runtime";
3
+ //#region src/web/requireAuth.tsx
4
+ var r = ({ children: r, fallback: i }) => {
5
+ let a = e();
6
+ return a ? typeof r == "function" ? /* @__PURE__ */ n(t, { children: r(a) }) : /* @__PURE__ */ n(t, { children: r }) : /* @__PURE__ */ n(t, { children: i });
7
+ };
8
+ //#endregion
9
+ export { r as RequireAuth };
@@ -0,0 +1,13 @@
1
+ import { createContext as e, useContext as t } from "react";
2
+ import { jsx as n } from "react/jsx-runtime";
3
+ //#region src/web/subjectContext.tsx
4
+ var r = e(null), i = ({ subject: e, children: t }) => /* @__PURE__ */ n(r.Provider, {
5
+ value: e,
6
+ children: t
7
+ }), a = () => {
8
+ let e = t(r);
9
+ if (!e) throw Error("useSubject() called without a signed-in user. Use useOptionalSubject() or wrap the consumer in <RequireAuth>.");
10
+ return e;
11
+ }, o = () => t(r);
12
+ //#endregion
13
+ export { i as SubjectProvider, o as useOptionalSubject, a as useSubject };
package/dist/web.d.ts CHANGED
@@ -1,5 +1,99 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { Subject } from '@voltro/protocol';
3
+ import { SubjectIdentity } from '@voltro/protocol';
4
+
5
+ /** Serializes operations in one client. A rejected existing credential never
6
+ * creates a replacement identity. Storage failures never enable memory fallback. */
7
+ export declare const createGuestClient: (options: GuestClientOptions) => {
8
+ connect: () => Promise<GuestClientSession>;
9
+ /** Prepare without replacing the active proof, persist both proofs, then
10
+ * commit. A later connect recovers the same identity after a lost response. */
11
+ rotate: () => Promise<GuestClientSession>;
12
+ /** Revoke on the server first. Keep the durable proof on transport failure
13
+ * so the caller can retry; erase locally only after confirmed revocation. */
14
+ revoke: () => Promise<void>;
15
+ /** Turn this guest into an email/password account under the same subject ID
16
+ * (`authRoutesPlugin({ guestUpgrade })`). The device proof is erased only after the
17
+ * server confirmed the account; on any refusal or transport failure it stays. */
18
+ upgrade: (input: {
19
+ readonly email: string;
20
+ readonly password: string;
21
+ }) => Promise<{
22
+ readonly subject: unknown;
23
+ }>;
24
+ /** The registered account signed in on this device (cookie clients), or null. */
25
+ account: () => Promise<GuestClientAccount | null>;
26
+ /** The signed-in account if there is one, else this device's guest (resumed or created once).
27
+ * The usual entry for a game that offers accounts: a reload after `upgrade` or `signIn` stays the account. */
28
+ start: () => Promise<GuestClientPlayer>;
29
+ /** Sign in to an existing account with email and password (`authRoutesPlugin`), e.g. on a second
30
+ * device. This device's guest proof stays: after `signOut`, `start` resumes that guest. */
31
+ signIn: (input: {
32
+ readonly email: string;
33
+ readonly password: string;
34
+ }) => Promise<GuestClientAccount>;
35
+ /** End the account session on the server and clear its cookie. */
36
+ signOut: () => Promise<void>;
37
+ };
38
+
39
+ /** A registered account signed in on this device (by upgrade or sign-in), through the session cookie. */
40
+ export declare interface GuestClientAccount {
41
+ readonly subject: Extract<SubjectIdentity, {
42
+ readonly type: 'user';
43
+ }>;
44
+ }
45
+
46
+ export declare class GuestClientError extends Error {
47
+ readonly code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response';
48
+ readonly status?: number | undefined;
49
+ constructor(code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response', status?: number | undefined);
50
+ }
51
+
52
+ export declare interface GuestClientOptions {
53
+ readonly apiBaseUrl: string;
54
+ readonly storage: GuestCredentialStore;
55
+ readonly prefix?: string;
56
+ readonly fetch?: typeof globalThis.fetch;
57
+ /** Web clients include cookies and obtain a signed CSRF token for each post. */
58
+ readonly cookies?: boolean;
59
+ /** Prefix of the app's auth routes, for `upgrade`, `account`, `signIn` and `signOut` (default `/auth`). */
60
+ readonly accountPrefix?: string;
61
+ }
62
+
63
+ /** What `start` found: the signed-in account, else this device's guest. */
64
+ export declare type GuestClientPlayer = {
65
+ readonly kind: 'account';
66
+ readonly subject: GuestClientAccount['subject'];
67
+ } | {
68
+ readonly kind: 'guest';
69
+ readonly subject: GuestClientSession['subject'];
70
+ readonly session: string;
71
+ };
72
+
73
+ export declare interface GuestClientSession {
74
+ readonly subject: Extract<SubjectIdentity, {
75
+ readonly type: 'user';
76
+ }> & {
77
+ readonly kind: 'guest';
78
+ };
79
+ readonly session: string;
80
+ }
81
+
82
+ /** Platform-backed durable secret storage. Native implementations use the
83
+ * operating system's secure store; writes resolve only after persistence. */
84
+ export declare interface GuestCredentialStore {
85
+ /** Serialize the whole read/create/write operation across users of this
86
+ * namespace, including other browser tabs or native client instances. */
87
+ readonly exclusive: <A>(work: () => Promise<A>) => Promise<A>;
88
+ readonly read: () => Promise<string | null>;
89
+ /** Persist the opaque value, including an in-progress rotation journal. */
90
+ readonly write: (value: string) => Promise<void>;
91
+ readonly remove: () => Promise<void>;
92
+ }
93
+
94
+ /** IndexedDB is the web device-proof store. Missing/blocked storage rejects;
95
+ * there is deliberately no localStorage or volatile substitute. */
96
+ export declare const indexedDbGuestStore: (namespace: string) => GuestCredentialStore;
3
97
 
4
98
  /** True when this browser can do WebAuthn at all. */
5
99
  export declare const isPasskeySupported: () => boolean;
package/dist/web.js CHANGED
@@ -1,90 +1,6 @@
1
- import { createContext as e, useContext as t } from "react";
2
- import { Fragment as n, jsx as r } from "react/jsx-runtime";
3
- //#region src/web/subjectContext.tsx
4
- var i = e(null), a = ({ subject: e, children: t }) => /* @__PURE__ */ r(i.Provider, {
5
- value: e,
6
- children: t
7
- }), o = () => {
8
- let e = t(i);
9
- if (!e) throw Error("useSubject() called without a signed-in user. Use useOptionalSubject() or wrap the consumer in <RequireAuth>.");
10
- return e;
11
- }, s = () => t(i), c = ({ children: e, fallback: t }) => {
12
- let i = s();
13
- return i ? typeof e == "function" ? /* @__PURE__ */ r(n, { children: e(i) }) : /* @__PURE__ */ r(n, { children: e }) : /* @__PURE__ */ r(n, { children: t });
14
- }, l = (e) => {
15
- let t = new Uint8Array(e), n = "";
16
- for (let e of t) n += String.fromCharCode(e);
17
- return btoa(n).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
18
- }, u = (e) => {
19
- let t = e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4), n = atob(t), r = new Uint8Array(n.length);
20
- for (let e = 0; e < n.length; e++) r[e] = n.charCodeAt(e);
21
- return r.buffer;
22
- }, d = () => typeof window < "u" && window.PublicKeyCredential !== void 0 && typeof navigator < "u" && !!navigator.credentials, f = async (e, t, n) => await (await (n.fetchImpl ?? fetch)(e, {
23
- method: "POST",
24
- credentials: "include",
25
- headers: {
26
- "content-type": "application/json",
27
- ...n.csrfToken ? { "x-csrf-token": n.csrfToken } : {}
28
- },
29
- body: JSON.stringify(t)
30
- })).json(), p = async (e, t = {}) => {
31
- if (!d()) throw Error("passkeys not supported in this browser");
32
- let n = t.basePath ?? "/auth", r = await f(`${n}/passkey/register/options`, e, t), i = r.challenge, a = r.rp, o = r.user, s = r.excludeCredentials ?? [], c = await navigator.credentials.create({ publicKey: {
33
- challenge: u(i),
34
- rp: a,
35
- user: {
36
- id: new TextEncoder().encode(o.id),
37
- name: o.name,
38
- displayName: o.displayName
39
- },
40
- pubKeyCredParams: [{
41
- type: "public-key",
42
- alg: -7
43
- }, {
44
- type: "public-key",
45
- alg: -257
46
- }],
47
- excludeCredentials: s.map((e) => ({
48
- type: "public-key",
49
- id: u(e.id)
50
- })),
51
- authenticatorSelection: {
52
- userVerification: "preferred",
53
- residentKey: "preferred"
54
- },
55
- timeout: 3e5
56
- } });
57
- if (!c) throw Error("credential creation was cancelled");
58
- let p = c.response, m = typeof p.getAuthenticatorData == "function" ? p.getAuthenticatorData() : /* @__PURE__ */ new ArrayBuffer(0), h = await f(`${n}/passkey/register/verify`, {
59
- userId: e.userId,
60
- credentialId: l(c.rawId),
61
- clientDataJSON: l(p.clientDataJSON),
62
- authenticatorData: l(m)
63
- }, t);
64
- if (!h.ok) throw Error(`passkey registration failed: ${String(h.error)}`);
65
- return { credentialId: h.credentialId };
66
- }, m = async (e = {}, t = {}) => {
67
- if (!d()) throw Error("passkeys not supported in this browser");
68
- let n = t.basePath ?? "/auth", r = await f(`${n}/passkey/assert/options`, e.userId ? { userId: e.userId } : {}, t), i = r.challenge, a = r.rpId, o = r.allowCredentials ?? [], s = await navigator.credentials.get({ publicKey: {
69
- challenge: u(i),
70
- rpId: a,
71
- allowCredentials: o.map((e) => ({
72
- type: "public-key",
73
- id: u(e.id)
74
- })),
75
- userVerification: "preferred",
76
- timeout: 3e5
77
- } });
78
- if (!s) throw Error("assertion was cancelled");
79
- let c = s.response, p = await f(`${n}/passkey/assert/verify`, {
80
- credentialId: l(s.rawId),
81
- clientDataJSON: l(c.clientDataJSON),
82
- authenticatorData: l(c.authenticatorData),
83
- signature: l(c.signature),
84
- ...e.userId ? { userId: e.userId } : {}
85
- }, t);
86
- if (!p.ok) throw Error(`passkey sign-in failed: ${String(p.error)}`);
87
- return { subject: p.subject };
88
- };
89
- //#endregion
90
- export { c as RequireAuth, a as SubjectProvider, d as isPasskeySupported, p as registerPasskey, m as signInWithPasskey, s as useOptionalSubject, o as useSubject };
1
+ import { SubjectProvider as e, useOptionalSubject as t, useSubject as n } from "./web/subjectContext.js";
2
+ import { RequireAuth as r } from "./web/requireAuth.js";
3
+ import { GuestClientError as i, createGuestClient as a } from "./guestClient.js";
4
+ import { indexedDbGuestStore as o } from "./web/guestStorage.js";
5
+ import { isPasskeySupported as s, registerPasskey as c, signInWithPasskey as l } from "./web/passkeyClient.js";
6
+ export { i as GuestClientError, r as RequireAuth, e as SubjectProvider, a as createGuestClient, o as indexedDbGuestStore, s as isPasskeySupported, c as registerPasskey, l as signInWithPasskey, t as useOptionalSubject, n as useSubject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-auth",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "Authentication primitives: password hashing, session creation, schema mixin + tables. Pairs with the `auth` app template for the UI; both independently usable. Server-side only (uses node:crypto).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -73,6 +73,11 @@
73
73
  "import": "./dist/native.js",
74
74
  "default": "./dist/native.js"
75
75
  },
76
+ "./guest": {
77
+ "types": "./dist/guest.d.ts",
78
+ "import": "./dist/guest.js",
79
+ "default": "./dist/guest.js"
80
+ },
76
81
  "./package.json": "./package.json"
77
82
  },
78
83
  "main": "./dist/index.js",
@@ -83,8 +88,8 @@
83
88
  "node": ">=24.0.0"
84
89
  },
85
90
  "dependencies": {
86
- "@voltro/database": "0.85.0",
87
- "@voltro/protocol": "0.85.0"
91
+ "@voltro/database": "0.87.0",
92
+ "@voltro/protocol": "0.87.0"
88
93
  },
89
94
  "peerDependencies": {
90
95
  "effect": "4.0.1",