@voltro/plugin-auth 0.86.0 → 0.88.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 (55) hide show
  1. package/CHANGELOG.md +5484 -0
  2. package/README.md +83 -0
  3. package/THIRD-PARTY-NOTICES.md +3 -227
  4. package/dist/admission.js +1 -2
  5. package/dist/appUserStore.js +1 -2
  6. package/dist/authExtensions.js +8 -0
  7. package/dist/csrf.js +1 -2
  8. package/dist/emailVerification.js +14 -13
  9. package/dist/guest.d.ts +99 -0
  10. package/dist/guest.js +3 -0
  11. package/dist/guestClient.js +236 -0
  12. package/dist/guestCredential.js +11 -0
  13. package/dist/guestPasswordUpgrade.js +84 -0
  14. package/dist/guestPlugin.js +165 -0
  15. package/dist/guestSchema.js +19 -0
  16. package/dist/guestSecrets.js +9 -0
  17. package/dist/guestService.js +223 -0
  18. package/dist/handlers.js +114 -115
  19. package/dist/impersonation.js +80 -80
  20. package/dist/index.d.ts +265 -15
  21. package/dist/index.js +27 -19
  22. package/dist/invitations.js +24 -25
  23. package/dist/mail.js +1 -2
  24. package/dist/memberships.js +1 -2
  25. package/dist/native/guestStorage.js +40 -0
  26. package/dist/native.d.ts +110 -0
  27. package/dist/native.js +46 -45
  28. package/dist/nativeRoutes.js +62 -74
  29. package/dist/overlayPlugin.js +166 -0
  30. package/dist/passkeyHandlers.js +1 -2
  31. package/dist/password.js +1 -2
  32. package/dist/plugin.d.ts +119 -10
  33. package/dist/plugin.js +100 -80
  34. package/dist/postgresUserStore.js +20 -9
  35. package/dist/revocation.js +41 -25
  36. package/dist/schema.d.ts +41 -1
  37. package/dist/schema.js +100 -100
  38. package/dist/session.js +1 -2
  39. package/dist/sessionFamily.js +14 -0
  40. package/dist/sessionRegistry.js +94 -80
  41. package/dist/signedClaims.js +23 -0
  42. package/dist/strategy.d.ts +13 -9
  43. package/dist/strategy.js +27 -24
  44. package/dist/subjectGuard.js +1 -2
  45. package/dist/tokens.js +1 -2
  46. package/dist/totp.js +1 -2
  47. package/dist/userStore.js +146 -140
  48. package/dist/web/guestStorage.js +46 -0
  49. package/dist/web/passkeyClient.js +1 -2
  50. package/dist/web/requireAuth.js +1 -2
  51. package/dist/web/subjectContext.js +1 -2
  52. package/dist/web.d.ts +94 -0
  53. package/dist/web.js +4 -2
  54. package/dist/webauthn.js +1 -2
  55. package/package.json +9 -4
package/dist/plugin.js CHANGED
@@ -2,24 +2,22 @@ import { hashPassword as e } from "./password.js";
2
2
  import { clearSessionCookie as t, issueSession as n, readSessionFromHeaders as r } from "./session.js";
3
3
  import { makeSessionRevocationChecker as i } from "./revocation.js";
4
4
  import { sessionIdOfSubject as a } from "./userStore.js";
5
- import { deferredUserStore as o } from "./appUserStore.js";
6
- import { CSRF_COOKIE_NAME as s, CSRF_HEADER_NAME as c, verifyCsrf as ee } from "./csrf.js";
7
- import { emailVerificationActive as te, emailVerificationGuard as ne, handleEmailVerifyConfirm as re, handleEmailVerifyRequest as l } from "./emailVerification.js";
8
- import { configSessionSecrets as ie, configSigningKey as ae, handleCsrf as oe, handleListMemberships as se, handleListSessions as ce, handleMagicLinkConsume as le, handleMagicLinkRequest as ue, handleMfaEnrollStart as de, handleMfaEnrollVerify as fe, handleMfaRegenerateRecoveryCodes as pe, handleMfaUnenroll as me, handleMfaVerify as he, handlePasswordResetConfirm as ge, handlePasswordResetRequest as _e, handleRevokeAllOtherSessions as ve, handleRevokeSession as ye, handleSignIn as be, handleSignOut as xe, handleSignUp as Se, handleSwitchTenant as Ce, issueUserSession as u } from "./handlers.js";
9
- import { handlePasskeyAssertOptions as d, handlePasskeyAssertVerify as f, handlePasskeyRegisterOptions as p, handlePasskeyRegisterVerify as m, memoryChallengeStore as h } from "./passkeyHandlers.js";
10
- import { handleAcceptInvitation as g, handleAcceptInvitationWithSignUp as _, handleCreateInvitation as v, handleListInvitations as y, handlePreviewInvitation as b, handleRevokeInvitation as x } from "./invitations.js";
11
- import { IMPERSONATION_FORBIDDEN_SUBPATHS as S, handleImpersonationStart as C, handleImpersonationStop as w, isImpersonated as T } from "./impersonation.js";
12
- import { handleNativeAuthorize as E, handleNativeComplete as D, handleNativeExchange as O } from "./nativeRoutes.js";
13
- import { voltroPasswordStrategy as k } from "./strategy.js";
14
- import { Effect as A } from "effect";
15
- import { SESSION_HEADER_NAME as j, readCookie as we } from "@voltro/protocol/session";
5
+ import { CSRF_COOKIE_NAME as o, CSRF_HEADER_NAME as s, verifyCsrf as ee } from "./csrf.js";
6
+ import { emailVerificationActive as te, emailVerificationGuard as ne, handleEmailVerifyConfirm as c, handleEmailVerifyRequest as l } from "./emailVerification.js";
7
+ import { configSessionSecrets as re, configSigningKey as ie, handleCsrf as ae, handleListMemberships as oe, handleListSessions as se, handleMagicLinkConsume as ce, handleMagicLinkRequest as le, handleMfaEnrollStart as ue, handleMfaEnrollVerify as de, handleMfaRegenerateRecoveryCodes as fe, handleMfaUnenroll as pe, handleMfaVerify as me, handlePasswordResetConfirm as he, handlePasswordResetRequest as ge, handleRevokeAllOtherSessions as _e, handleRevokeSession as ve, handleSignIn as ye, handleSignOut as be, handleSignUp as xe, handleSwitchTenant as Se, issueUserSession as u } from "./handlers.js";
8
+ import { completeGuestPasswordUpgrade as Ce } from "./guestPasswordUpgrade.js";
9
+ import { deferredUserStore as d } from "./appUserStore.js";
10
+ import { handlePasskeyAssertOptions as f, handlePasskeyAssertVerify as p, handlePasskeyRegisterOptions as m, handlePasskeyRegisterVerify as h, memoryChallengeStore as g } from "./passkeyHandlers.js";
11
+ import { handleAcceptInvitation as _, handleAcceptInvitationWithSignUp as v, handleCreateInvitation as y, handleListInvitations as b, handlePreviewInvitation as x, handleRevokeInvitation as S } from "./invitations.js";
12
+ import { IMPERSONATION_FORBIDDEN_SUBPATHS as C, handleImpersonationStart as w, handleImpersonationStop as T, isImpersonated as E } from "./impersonation.js";
13
+ import { handleNativeAuthorize as D, handleNativeComplete as O, handleNativeExchange as k } from "./nativeRoutes.js";
14
+ import { voltroPasswordStrategy as A } from "./strategy.js";
15
+ import { Effect as j } from "effect";
16
+ import { SESSION_HEADER_NAME as we, readCookie as Te } from "@voltro/protocol/session";
16
17
  import { registerRetention as M, retentionTtlMsFromEnv as N } from "@voltro/database";
17
- import { definePlugin as Te } from "@voltro/protocol";
18
- //#region src/plugin.ts
19
- var P = (e) => {
20
- for (let t of e) if (F(t)) return t.auth.sessionStrategy;
21
- return null;
22
- }, F = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", Ee = "/auth", De = (e) => {
18
+ import { definePlugin as Ee } from "@voltro/protocol";
19
+ // src/plugin.ts
20
+ var P = (e) => e.filter(F).map((e) => e.auth.sessionStrategy), F = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", De = "/auth", Oe = (e) => {
23
21
  let t = Buffer.from(e.rawBody).toString("utf8");
24
22
  if (!t) return {};
25
23
  if ((e.headers["content-type"] ?? "").includes("application/x-www-form-urlencoded")) {
@@ -41,12 +39,12 @@ var P = (e) => {
41
39
  ...e.setCookie === void 0 ? {} : { "set-cookie": e.setCookie },
42
40
  ...e.location === void 0 ? {} : { location: e.location }
43
41
  }
44
- }), Oe = "actors", L = (e, t) => ({
42
+ }), ke = "actors", L = (e, t) => ({
45
43
  status: e,
46
44
  body: JSON.stringify(t),
47
45
  contentType: "application/json; charset=utf-8"
48
46
  }), R = (e) => typeof e == "string" ? e : void 0, z = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {}, B = (P) => {
49
- let F = P.prefix ?? Ee, B = { current: typeof P.store == "function" ? void 0 : P.store }, V = typeof P.store == "function" ? o(() => B.current) : P.store, H = P.challengeStore ?? h(), U = { current: void 0 }, W = P.admit === void 0 ? void 0 : (e, t) => P.admit(e, { store: t.store ?? U.current }), G = {
47
+ let F = P.prefix ?? De, B = { current: typeof P.store == "function" ? void 0 : P.store }, V = typeof P.store == "function" ? d(() => B.current) : P.store, H = P.challengeStore ?? g(), U = { current: void 0 }, W = P.admit === void 0 ? void 0 : (e, t) => P.admit(e, { store: t.store ?? U.current }), G = {
50
48
  ...P.secret === void 0 ? {} : { secret: P.secret },
51
49
  ...P.secrets === void 0 ? {} : { secrets: P.secrets },
52
50
  defaultTenantId: P.defaultTenantId,
@@ -62,7 +60,7 @@ var P = (e) => {
62
60
  ...W === void 0 ? {} : { admit: W },
63
61
  ensureActor: P.ensureActor ?? ((e) => {
64
62
  let t = U.current;
65
- return t === void 0 ? A.void : A.promise(() => t.insertIgnore(Oe, {
63
+ return t === void 0 ? j.void : j.promise(() => t.insertIgnore(ke, {
66
64
  id: e.id,
67
65
  kind: "user",
68
66
  displayName: null
@@ -90,17 +88,17 @@ var P = (e) => {
90
88
  envVar: "VOLTRO_IMPERSONATION_GRANTS_TTL_HOURS"
91
89
  });
92
90
  let q = i(V, P.sessionRevocation ?? {}), J = async (e) => {
93
- let t = ie(G), i = r(e.headers, t);
91
+ let t = re(G), i = r(e.headers, t);
94
92
  if (!i) return null;
95
- let o = e.headers[j] ? "header" : "cookie", s = a(i.subject);
96
- if (s && !await A.runPromise(q.isLive(s))) return null;
93
+ let o = e.headers[we] ? "header" : "cookie", s = a(i.subject);
94
+ if (s && (i.subject.type !== "user" || !await j.runPromise(q.isLiveFor(s, i.subject.id)))) return null;
97
95
  if (i.renew || i.kid !== t.current.kid) {
98
96
  let e = Math.max(i.exp - i.iat, 60), r = n(i.subject, t, {
99
97
  ttlSeconds: e,
100
98
  ...G.cookieDomain ? { domain: G.cookieDomain } : {},
101
99
  secure: G.cookieSecure ?? !0
102
100
  });
103
- return s && await A.runPromise(V.touchSession(s, new Date(Date.now() + e * 1e3))), {
101
+ return s && await j.runPromise(V.touchSession(s, new Date(Date.now() + e * 1e3))), {
104
102
  subject: i.subject,
105
103
  sessionId: s,
106
104
  renewSetCookie: r.setCookie,
@@ -125,10 +123,10 @@ var P = (e) => {
125
123
  "set-cookie": t
126
124
  }
127
125
  };
128
- }, je = (e) => {
129
- let t = e.headers[c], n = we(e.headers.cookie, s);
130
- return ee(t, n, ae(P));
131
- }, X = (e, t) => e.path === `${F}${t}`, Z = (e) => A.runPromise(e).then(I), Q = {
126
+ }, Me = (e) => {
127
+ let t = e.headers[s], n = Te(e.headers.cookie, o);
128
+ return ee(t, n, ie(P));
129
+ }, X = (e, t) => e.path === `${F}${t}`, Z = (e) => j.runPromise(e).then(I), Q = {
132
130
  issue: (e, t) => u(e, V, G, {
133
131
  sessionId: t.sessionId,
134
132
  ttlSeconds: t.ttlSeconds,
@@ -147,11 +145,11 @@ var P = (e) => {
147
145
  ...G.cookieDomain ? { domain: G.cookieDomain } : {},
148
146
  secure: G.cookieSecure ?? !0
149
147
  })
150
- }, Me = {
148
+ }, Ne = {
151
149
  method: "*",
152
150
  path: F,
153
151
  handle: async (t) => {
154
- let n = t.method === "GET" ? {} : De(t);
152
+ let n = t.method === "GET" ? {} : Oe(t);
155
153
  if (P.native !== void 0) {
156
154
  let e = {
157
155
  prefix: F,
@@ -161,19 +159,19 @@ var P = (e) => {
161
159
  };
162
160
  if (t.method === "GET" && X(t, "/native/authorize")) {
163
161
  let n = await J(t);
164
- return Z(E(new URLSearchParams(t.query), n, V, e));
162
+ return Z(D(new URLSearchParams(t.query), n, V, e));
165
163
  }
166
164
  if (t.method === "GET" && X(t, "/native/complete")) {
167
165
  let n = await J(t);
168
- return Z(D(t.headers.cookie, n, V, e));
166
+ return Z(O(t.headers.cookie, n, V, e));
169
167
  }
170
- if (t.method === "POST" && X(t, "/native/exchange")) return Z(O({
168
+ if (t.method === "POST" && X(t, "/native/exchange")) return Z(k({
171
169
  code: R(n.code) ?? "",
172
170
  codeVerifier: R(n.codeVerifier) ?? ""
173
171
  }, V, e));
174
172
  } else if (t.path.startsWith(`${F}/native/`)) return L(501, { error: "native_login_not_configured" });
175
- if (t.method === "GET" && X(t, "/csrf")) return I(oe(G));
176
- if (t.method === "POST" && X(t, "/sign-in")) return Z(be({
173
+ if (t.method === "GET" && X(t, "/csrf")) return I(ae(G));
174
+ if (t.method === "POST" && X(t, "/sign-in")) return Z(ye({
177
175
  email: R(n.email) ?? "",
178
176
  password: R(n.password) ?? "",
179
177
  ...n.redirectAfter === void 0 ? {} : { redirectAfter: !!n.redirectAfter },
@@ -186,30 +184,52 @@ var P = (e) => {
186
184
  status: 400,
187
185
  headers: { "content-type": "application/json" },
188
186
  body: JSON.stringify({ error: "tenant_not_open" })
189
- } : Z(Se({
187
+ } : Z(xe({
190
188
  email: R(n.email) ?? "",
191
189
  password: R(n.password) ?? "",
192
190
  ...z("tenantId", e),
193
191
  ...n.redirectAfter === void 0 ? {} : { redirectAfter: !!n.redirectAfter }
194
192
  }, V, G));
195
193
  }
194
+ if (P.guestUpgrade && t.method === "POST" && X(t, "/upgrade-guest")) {
195
+ let e = await j.runPromise(Ce({
196
+ credential: R(n.credential) ?? "",
197
+ email: R(n.email) ?? "",
198
+ password: R(n.password) ?? "",
199
+ service: P.guestUpgrade.service(),
200
+ users: V,
201
+ config: G,
202
+ ipAddress: t.remoteAddr ?? null,
203
+ userAgent: t.headers["user-agent"] ?? null
204
+ }));
205
+ return e.kind === "refused" ? L(e.status, {
206
+ error: e.code,
207
+ message: e.message
208
+ }) : {
209
+ ...L(200, {
210
+ ok: !0,
211
+ subject: e.subject
212
+ }),
213
+ headers: { "set-cookie": e.setCookie }
214
+ };
215
+ }
196
216
  if (t.method === "POST" && X(t, "/sign-out")) {
197
217
  let e = await J(t);
198
- return e && e.subject.type === "user" && e.sessionId && (await A.runPromise(V.revokeSession(e.subject.id, e.sessionId)), q.invalidate(e.sessionId)), I(xe(G));
218
+ return e && e.subject.type === "user" && e.sessionId && (await j.runPromise(V.revokeSession(e.subject.id, e.sessionId)), q.invalidate(e.sessionId)), I(be(G));
199
219
  }
200
- if (t.method === "POST" && X(t, "/magic-link")) return Z(ue({
220
+ if (t.method === "POST" && X(t, "/magic-link")) return Z(le({
201
221
  email: R(n.email) ?? "",
202
222
  ...z("callbackPath", n.callbackPath)
203
223
  }, V, G));
204
- if (t.method === "POST" && X(t, "/magic-link/callback")) return Z(le({
224
+ if (t.method === "POST" && X(t, "/magic-link/callback")) return Z(ce({
205
225
  token: R(n.token) ?? "",
206
226
  ...n.redirectAfter === void 0 ? {} : { redirectAfter: !!n.redirectAfter }
207
227
  }, V, G));
208
- if (t.method === "POST" && X(t, "/password-reset")) return Z(_e({
228
+ if (t.method === "POST" && X(t, "/password-reset")) return Z(ge({
209
229
  email: R(n.email) ?? "",
210
230
  ...z("callbackPath", n.callbackPath)
211
231
  }, V, G));
212
- if (t.method === "POST" && X(t, "/password-reset/confirm")) return Z(ge({
232
+ if (t.method === "POST" && X(t, "/password-reset/confirm")) return Z(he({
213
233
  token: R(n.token) ?? "",
214
234
  newPassword: R(n.newPassword) ?? ""
215
235
  }, V));
@@ -219,16 +239,16 @@ var P = (e) => {
219
239
  let r = await J(t);
220
240
  return !r || r.subject.type !== "user" ? L(400, { error: "email required" }) : Z(l({ userId: r.subject.id }, V, G));
221
241
  }
222
- if (t.method === "POST" && X(t, "/verify-email/callback")) return Z(re({ token: R(n.token) ?? "" }, V));
223
- if (P.invitations && t.method === "POST" && X(t, "/invitations/preview")) return Z(b({ token: R(n.token) ?? "" }, V, K));
224
- if (P.invitations && t.method === "POST" && X(t, "/invitations/sign-up")) return Z(_({
242
+ if (t.method === "POST" && X(t, "/verify-email/callback")) return Z(c({ token: R(n.token) ?? "" }, V));
243
+ if (P.invitations && t.method === "POST" && X(t, "/invitations/preview")) return Z(x({ token: R(n.token) ?? "" }, V, K));
244
+ if (P.invitations && t.method === "POST" && X(t, "/invitations/sign-up")) return Z(v({
225
245
  token: R(n.token) ?? "",
226
246
  password: R(n.password) ?? ""
227
247
  }, V, {
228
248
  ...K,
229
249
  hashPassword: e
230
250
  }));
231
- if (t.method === "POST" && X(t, "/mfa/verify")) return Z(he({
251
+ if (t.method === "POST" && X(t, "/mfa/verify")) return Z(me({
232
252
  pendingToken: R(n.pendingToken) ?? "",
233
253
  ...z("code", n.code),
234
254
  ...z("recoveryCode", n.recoveryCode),
@@ -236,18 +256,18 @@ var P = (e) => {
236
256
  ...t.remoteAddr ? { ipAddress: t.remoteAddr } : {},
237
257
  ...t.headers["user-agent"] ? { userAgent: t.headers["user-agent"] } : {}
238
258
  }, V, G));
239
- if (P.passkey && t.method === "POST" && X(t, "/passkey/assert/options")) return Z(d({ ...z("userId", n.userId) }, V, H, P.passkey));
240
- if (P.passkey && t.method === "POST" && X(t, "/passkey/assert/verify")) return Z(f({
259
+ if (P.passkey && t.method === "POST" && X(t, "/passkey/assert/options")) return Z(f({ ...z("userId", n.userId) }, V, H, P.passkey));
260
+ if (P.passkey && t.method === "POST" && X(t, "/passkey/assert/verify")) return Z(p({
241
261
  credentialId: R(n.credentialId) ?? "",
242
262
  clientDataJSON: R(n.clientDataJSON) ?? "",
243
263
  authenticatorData: R(n.authenticatorData) ?? "",
244
264
  signature: R(n.signature) ?? "",
245
265
  ...z("userId", n.userId)
246
266
  }, V, H, P.passkey, G));
247
- let r = await J(t), i = r?.subject ?? null, a = () => !i || i.type !== "user" ? L(401, { error: "unauthenticated" }) : { userId: i.id }, o = () => je(t) ? null : L(403, { error: "csrf" }), s = (e) => Z(e).then((e) => Y(e, r?.renewSetCookie));
248
- if (i && T(i)) {
267
+ let r = await J(t), i = r?.subject ?? null, a = () => !i || i.type !== "user" ? L(401, { error: "unauthenticated" }) : { userId: i.id }, o = () => Me(t) ? null : L(403, { error: "csrf" }), s = (e) => Z(e).then((e) => Y(e, r?.renewSetCookie));
268
+ if (i && E(i)) {
249
269
  let e = t.path.startsWith(F) ? t.path.slice(F.length) : "";
250
- if (S.includes(e)) return L(403, {
270
+ if (C.includes(e)) return L(403, {
251
271
  error: "forbidden_while_impersonating",
252
272
  message: "This action changes account credentials or session scope and is refused while impersonating. Stop the impersonation first."
253
273
  });
@@ -264,11 +284,11 @@ var P = (e) => {
264
284
  }
265
285
  if (t.method === "GET" && X(t, "/sessions")) {
266
286
  let e = a();
267
- return "status" in e ? e : s(ce({ userId: e.userId }, V));
287
+ return "status" in e ? e : s(se({ userId: e.userId }, V));
268
288
  }
269
289
  if (t.method === "GET" && X(t, "/memberships")) {
270
290
  let e = a();
271
- return "status" in e ? e : s(se({ userId: e.userId }, V));
291
+ return "status" in e ? e : s(oe({ userId: e.userId }, V));
272
292
  }
273
293
  if (t.method === "POST" && X(t, "/sessions/revoke")) {
274
294
  let e = a();
@@ -276,21 +296,21 @@ var P = (e) => {
276
296
  let t = o();
277
297
  if (t) return t;
278
298
  let r = R(n.sessionId) ?? "";
279
- return s(ye({
299
+ return s(ve({
280
300
  userId: e.userId,
281
301
  sessionId: r
282
302
  }, V)).then((e) => (r && q.invalidate(r), e));
283
303
  }
284
304
  if (t.method === "POST" && X(t, "/sessions/revoke-others")) {
285
305
  let e = a();
286
- return "status" in e ? e : o() || s(ve({
306
+ return "status" in e ? e : o() || s(_e({
287
307
  userId: e.userId,
288
308
  keepSessionId: r?.sessionId ?? "__none__"
289
309
  }, V));
290
310
  }
291
311
  if (P.invitations && t.method === "POST" && X(t, "/invitations")) {
292
312
  let e = a();
293
- return "status" in e ? e : o() || s(v({
313
+ return "status" in e ? e : o() || s(y({
294
314
  inviterUserId: e.userId,
295
315
  tenantId: R(n.tenantId) ?? i?.tenantId ?? "",
296
316
  email: R(n.email) ?? "",
@@ -299,14 +319,14 @@ var P = (e) => {
299
319
  }
300
320
  if (P.invitations && t.method === "GET" && t.path === `${F}/invitations`) {
301
321
  let e = a();
302
- return "status" in e ? e : s(y({
322
+ return "status" in e ? e : s(b({
303
323
  callerUserId: e.userId,
304
324
  tenantId: i?.tenantId ?? ""
305
325
  }, V, K));
306
326
  }
307
327
  if (P.invitations && t.method === "POST" && X(t, "/invitations/revoke")) {
308
328
  let e = a();
309
- return "status" in e ? e : o() || s(x({
329
+ return "status" in e ? e : o() || s(S({
310
330
  callerUserId: e.userId,
311
331
  tenantId: R(n.tenantId) ?? i?.tenantId ?? "",
312
332
  invitationId: R(n.invitationId) ?? ""
@@ -318,14 +338,14 @@ var P = (e) => {
318
338
  let e = o();
319
339
  if (e) return e;
320
340
  }
321
- return s(g({
341
+ return s(_({
322
342
  token: R(n.token) ?? "",
323
343
  ..."status" in e ? {} : { userId: e.userId }
324
344
  }, V, K));
325
345
  }
326
346
  if (P.impersonation && t.method === "POST" && X(t, "/impersonate/start")) {
327
347
  let e = a();
328
- return "status" in e ? e : o() || Z(C({
348
+ return "status" in e ? e : o() || Z(w({
329
349
  actorSubject: i,
330
350
  actorSessionId: r?.sessionId ?? "",
331
351
  ...z("targetUserId", n.userId),
@@ -343,7 +363,7 @@ var P = (e) => {
343
363
  }
344
364
  if (P.impersonation && t.method === "POST" && X(t, "/impersonate/stop")) {
345
365
  let e = a();
346
- return "status" in e ? e : o() || Z(w({
366
+ return "status" in e ? e : o() || Z(T({
347
367
  subject: i,
348
368
  sessionId: r?.sessionId ?? ""
349
369
  }, V, {
@@ -355,7 +375,7 @@ var P = (e) => {
355
375
  }
356
376
  if (t.method === "POST" && X(t, "/switch-tenant")) {
357
377
  let e = a();
358
- return "status" in e ? e : o() || Z(Ce({
378
+ return "status" in e ? e : o() || Z(Se({
359
379
  userId: e.userId,
360
380
  targetTenantId: R(n.targetTenantId) ?? "",
361
381
  ...typeof n.clientId == "number" ? { clientId: n.clientId } : {},
@@ -368,8 +388,8 @@ var P = (e) => {
368
388
  if ("status" in e) return e;
369
389
  let t = o();
370
390
  if (t) return t;
371
- let n = await A.runPromise(V.findById(e.userId));
372
- return s(de({
391
+ let n = await j.runPromise(V.findById(e.userId));
392
+ return s(ue({
373
393
  userId: e.userId,
374
394
  issuer: P.mfa.issuer,
375
395
  accountName: n?.email ?? e.userId
@@ -377,29 +397,29 @@ var P = (e) => {
377
397
  }
378
398
  if (P.mfa && t.method === "POST" && X(t, "/mfa/enroll/verify")) {
379
399
  let e = a();
380
- return "status" in e ? e : o() || s(fe({
400
+ return "status" in e ? e : o() || s(de({
381
401
  userId: e.userId,
382
402
  code: R(n.code) ?? ""
383
403
  }, V));
384
404
  }
385
405
  if (P.mfa && t.method === "POST" && X(t, "/mfa/unenroll")) {
386
406
  let e = a();
387
- return "status" in e ? e : o() || s(me({ userId: e.userId }, V));
407
+ return "status" in e ? e : o() || s(pe({ userId: e.userId }, V));
388
408
  }
389
409
  if (P.mfa && t.method === "POST" && X(t, "/mfa/recovery-codes/regenerate")) {
390
410
  let e = a();
391
- return "status" in e ? e : o() || s(pe({ userId: e.userId }, V));
411
+ return "status" in e ? e : o() || s(fe({ userId: e.userId }, V));
392
412
  }
393
413
  if (P.passkey && t.method === "POST" && X(t, "/passkey/register/options")) {
394
414
  let e = a();
395
- return "status" in e ? e : o() || s(p({
415
+ return "status" in e ? e : o() || s(m({
396
416
  userId: e.userId,
397
417
  userName: R(n.userName) ?? e.userId
398
418
  }, V, H, P.passkey));
399
419
  }
400
420
  if (P.passkey && t.method === "POST" && X(t, "/passkey/register/verify")) {
401
421
  let e = a();
402
- return "status" in e ? e : o() || s(m({
422
+ return "status" in e ? e : o() || s(h({
403
423
  userId: e.userId,
404
424
  credentialId: R(n.credentialId) ?? "",
405
425
  clientDataJSON: R(n.clientDataJSON) ?? "",
@@ -409,17 +429,17 @@ var P = (e) => {
409
429
  }
410
430
  return !P.passkey && t.path.startsWith(`${F}/passkey/`) ? L(501, { error: "passkey_not_configured" }) : L(404, { error: "not_found" });
411
431
  }
412
- }, Ne = Te({
432
+ }, Pe = Ee({
413
433
  name: P.name ? `@voltro/plugin-auth#${P.name}` : "@voltro/plugin-auth",
414
434
  description: "Auth routes — mounts the full auth surface (password, magic-link, password-reset, CSRF, session probe/list/revoke, memberships, switch-tenant, passkey ceremonies) under /auth on the framework listener, with keyed secret rotation + request-time revocation.",
415
435
  permissions: [],
416
- declaredEnv: Ae,
417
- privacy: ke,
418
- httpRoutes: [Me],
436
+ declaredEnv: je,
437
+ privacy: Ae,
438
+ httpRoutes: [Ne],
419
439
  bindDataStore: (e, t) => {
420
440
  U.current = e, typeof P.store == "function" && (B.current = P.store(t?.sql === void 0 ? {} : { sql: t.sql }));
421
441
  },
422
- onActivate: (e) => A.sync(() => {
442
+ onActivate: (e) => j.sync(() => {
423
443
  e.logger.info("auth routes active", {
424
444
  prefix: F,
425
445
  passkey: P.passkey ? P.passkey.rpId : "disabled",
@@ -430,17 +450,18 @@ var P = (e) => {
430
450
  impersonation: P.impersonation ? `enabled(max ${P.impersonation.maxDurationSeconds ?? 900}s)` : "disabled"
431
451
  });
432
452
  }),
433
- onDeactivate: () => A.void
453
+ onDeactivate: () => j.void
434
454
  }), $ = P.secrets ?? P.secret;
435
455
  return {
436
- ...Ne,
437
- auth: { sessionStrategy: k({
456
+ ...Pe,
457
+ auth: { sessionStrategy: A({
438
458
  ...$ === void 0 ? {} : { secret: $ },
459
+ userKinds: ["registered"],
439
460
  store: V,
440
461
  revocation: q
441
462
  }) }
442
463
  };
443
- }, ke = { collects: [{
464
+ }, Ae = { collects: [{
444
465
  category: "identifiers",
445
466
  linked: !0,
446
467
  tracking: !1,
@@ -450,7 +471,7 @@ var P = (e) => {
450
471
  linked: !0,
451
472
  tracking: !1,
452
473
  purposes: ["app-functionality"]
453
- }] }, Ae = [
474
+ }] }, je = [
454
475
  {
455
476
  name: "VOLTRO_SESSION_SECRET_PREVIOUS",
456
477
  required: !1,
@@ -494,5 +515,4 @@ var P = (e) => {
494
515
  example: "30000"
495
516
  }
496
517
  ];
497
- //#endregion
498
- export { B as authRoutesPlugin, P as getAuthSessionStrategy };
518
+ export { B as authRoutesPlugin, P as getAuthSessionStrategies };
@@ -1,6 +1,6 @@
1
1
  import { UserAlreadyExistsError as e, UserNotFoundError as t, newAuthId as n } from "./userStore.js";
2
2
  import { Effect as r } from "effect";
3
- //#region src/postgresUserStore.ts
3
+ // src/postgresUserStore.ts
4
4
  var i = (e) => {
5
5
  let t = e;
6
6
  for (let e = 0; typeof t == "object" && t && e < 6; e++) {
@@ -65,7 +65,7 @@ var i = (e) => {
65
65
  `;
66
66
  return t[0] ? g(t[0]) : null;
67
67
  }).pipe(r.orElseSucceed(() => null)), ee = (t) => r.gen(function* () {
68
- let n = t.email.toLowerCase(), a = /* @__PURE__ */ new Date(), s = [
68
+ let n = t.email === null ? null : t.email.toLowerCase(), a = /* @__PURE__ */ new Date(), s = [
69
69
  u("id"),
70
70
  u("email"),
71
71
  u("tenantId"),
@@ -83,7 +83,7 @@ var i = (e) => {
83
83
  ...t,
84
84
  email: n,
85
85
  createdAt: a
86
- })), r.catch((t) => i(t) === "23505" ? r.fail(new e({ email: n })) : r.die(t)));
86
+ })), r.catch((t) => i(t) === "23505" && n !== null ? r.fail(new e({ email: n })) : r.die(t)));
87
87
  }), y = (e) => r.gen(function* () {
88
88
  let n = (yield* o`
89
89
  SELECT ${p().selectList} FROM ${p().usersTable} WHERE ${u("id")} = ${e} LIMIT 1
@@ -153,9 +153,15 @@ var i = (e) => {
153
153
  `)[0]?.role ?? null;
154
154
  }).pipe(r.orElseSucceed(() => null)), O = (e) => r.gen(function* () {
155
155
  return (yield* o`
156
- SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt", "familyId", "rotatedAt"
157
- FROM sessions WHERE "userId" = ${e} AND "rotatedAt" IS NULL
158
- ORDER BY "createdAt" DESC
156
+ SELECT credential.id, credential."userId", credential."tenantId", credential."expiresAt", credential."ipAddress", credential."userAgent", credential."createdAt", credential."lastSeenAt", credential."familyId", credential."rotatedAt"
157
+ FROM sessions AS credential
158
+ INNER JOIN sessions AS root
159
+ ON root.id = COALESCE(credential."familyId", credential.id)
160
+ AND root."userId" = credential."userId"
161
+ AND COALESCE(root."familyId", root.id) = root.id
162
+ WHERE credential."userId" = ${e} AND credential."rotatedAt" IS NULL
163
+ AND credential.id <> '' AND credential."userId" <> '' AND root.id <> ''
164
+ ORDER BY credential."createdAt" DESC
159
165
  `).map((e) => ({ ...e }));
160
166
  }).pipe(r.orElseSucceed(() => [])), k = (e) => r.gen(function* () {
161
167
  let t = /* @__PURE__ */ new Date();
@@ -174,8 +180,14 @@ var i = (e) => {
174
180
  };
175
181
  }).pipe(r.orDie), A = (e) => r.gen(function* () {
176
182
  let t = yield* o`
177
- SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt", "familyId", "rotatedAt"
178
- FROM sessions WHERE id = ${e} AND "rotatedAt" IS NULL LIMIT 1
183
+ SELECT credential.id, credential."userId", credential."tenantId", credential."expiresAt", credential."ipAddress", credential."userAgent", credential."createdAt", credential."lastSeenAt", credential."familyId", credential."rotatedAt"
184
+ FROM sessions AS credential
185
+ INNER JOIN sessions AS root
186
+ ON root.id = COALESCE(credential."familyId", credential.id)
187
+ AND root."userId" = credential."userId"
188
+ AND COALESCE(root."familyId", root.id) = root.id
189
+ WHERE credential.id = ${e} AND credential."rotatedAt" IS NULL
190
+ AND credential.id <> '' AND credential."userId" <> '' AND root.id <> '' LIMIT 1
179
191
  `;
180
192
  return t[0] ? { ...t[0] } : null;
181
193
  }).pipe(r.orElseSucceed(() => null)), j = (e, t) => o.withTransaction(r.gen(function* () {
@@ -459,5 +471,4 @@ var i = (e) => {
459
471
  }).pipe(r.asVoid, $("clear", void 0))
460
472
  };
461
473
  };
462
- //#endregion
463
474
  export { o as postgresUserStore };
@@ -1,33 +1,49 @@
1
- import { Effect as e } from "effect";
2
- //#region src/revocation.ts
3
- var t = 3e4, n = 1e4, r = (r, i = {}) => {
4
- let a = i.ttlMs ?? t, o = i.maxEntries ?? n, s = i.now ?? Date.now, c = /* @__PURE__ */ new Map(), l = () => {
5
- if (c.size <= o) return;
6
- let e = s();
7
- for (let [t, n] of c) n.freshUntil <= e && c.delete(t);
8
- for (let e of c.keys()) {
9
- if (c.size <= o) break;
10
- c.delete(e);
1
+ import { isSessionCredentialLiveFor as e } from "./sessionFamily.js";
2
+ import { Effect as t } from "effect";
3
+ // src/revocation.ts
4
+ var n = 3e4, r = 1e4, i = (e) => {
5
+ if (typeof e != "object" || !e) throw TypeError("Session revocation options must be an object.");
6
+ if ("isLive" in e || "isLiveFor" in e || "invalidate" in e || Reflect.ownKeys(e).some((e) => typeof e != "string" || ![
7
+ "ttlMs",
8
+ "maxEntries",
9
+ "now"
10
+ ].includes(e))) throw TypeError("Supply revocation options or a checker implementing isLiveFor(sessionId, userId), invalidate(sessionId) and ttlMs.");
11
+ if (e.ttlMs !== void 0 && (!Number.isFinite(e.ttlMs) || e.ttlMs < 0)) throw TypeError("Session revocation ttlMs must be finite and nonnegative.");
12
+ if (e.maxEntries !== void 0 && (!Number.isSafeInteger(e.maxEntries) || e.maxEntries < 0)) throw TypeError("Session revocation maxEntries must be a nonnegative safe integer.");
13
+ if (e.now !== void 0 && typeof e.now != "function") throw TypeError("Session revocation now must be a clock function.");
14
+ }, a = (a, o = {}) => {
15
+ i(o);
16
+ let s = o.ttlMs ?? n, c = o.maxEntries ?? r, l = o.now ?? Date.now, u = /* @__PURE__ */ new Map(), d = () => {
17
+ if (u.size <= c) return;
18
+ let e = l();
19
+ for (let [t, n] of u) n.freshUntil <= e && u.delete(t);
20
+ for (let e of u.keys()) {
21
+ if (u.size <= c) break;
22
+ u.delete(e);
11
23
  }
12
- }, u = (e) => e !== null && e.expiresAt.getTime() > s();
24
+ };
13
25
  return {
14
- ttlMs: a,
26
+ ttlMs: s,
15
27
  invalidate: (e) => {
16
- c.delete(e);
28
+ u.delete(e);
17
29
  },
18
- isLive: (t) => e.gen(function* () {
19
- let e = s();
20
- if (a > 0) {
21
- let n = c.get(t);
22
- if (n && n.freshUntil > e) return n.live;
30
+ isLiveFor: (n, r) => t.gen(function* () {
31
+ if (typeof n != "string" || n.length === 0 || typeof r != "string" || r.length === 0) return !1;
32
+ let t = l();
33
+ if (!Number.isFinite(t)) return !1;
34
+ if (s > 0) {
35
+ let e = u.get(n);
36
+ if (e && e.freshUntil > t) return e.live && e.userId === r;
23
37
  }
24
- let n = yield* r.findSession(t), i = u(n);
25
- return a > 0 && (c.set(t, {
26
- live: i,
27
- freshUntil: e + a
28
- }), l()), i;
38
+ let i = yield* a.findSession(n), o = l();
39
+ if (!Number.isFinite(o)) return !1;
40
+ let f = i !== null && i.id === n && typeof i.userId == "string" && i.userId.length > 0 ? i.userId : null, p = i?.expiresAt instanceof Date ? i.expiresAt.getTime() : NaN, m = f !== null && e(i, n, f, o);
41
+ return s > 0 && c > 0 && (u.set(n, {
42
+ userId: f,
43
+ live: m,
44
+ freshUntil: m ? Math.min(o + s, p) : o + s
45
+ }), d()), m && f === r;
29
46
  })
30
47
  };
31
48
  };
32
- //#endregion
33
- export { r as makeSessionRevocationChecker };
49
+ export { a as makeSessionRevocationChecker, i as validateSessionRevocationOptions };
package/dist/schema.d.ts CHANGED
@@ -1,6 +1,29 @@
1
+ import { ColumnBuilder } from '@voltro/database';
2
+ import { FieldDefinitions } from '@voltro/database';
3
+ import { Table } from '@voltro/database';
1
4
  import { TableLike } from '@voltro/database';
2
5
 
3
- /** Tables the plugin contributes, as an array. */
6
+ /** Device credentials never appear in SQL, logs, exports or session claims. */
7
+ export declare const authGuestDevicesTable: Table<"authGuestDevices", FieldDefinitions<{
8
+ readonly id: ColumnBuilder<string, "id", boolean>;
9
+ readonly guestId: ColumnBuilder<string, "text", boolean>;
10
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
11
+ readonly secretHash: ColumnBuilder<string, "text", boolean>;
12
+ readonly generation: ColumnBuilder<number, "integer", boolean>;
13
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
14
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
15
+ }>, true, never>;
16
+
17
+ /** Guest identity is independent of the password user's email record. */
18
+ export declare const authGuestsTable: Table<"authGuests", FieldDefinitions<{
19
+ readonly id: ColumnBuilder<string, "id", boolean>;
20
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
21
+ readonly upgradeable: ColumnBuilder<boolean, "boolean", boolean>;
22
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
23
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
24
+ readonly lastSeenAt: ColumnBuilder<Date, "timestamp", boolean>;
25
+ }>, true, never>;
26
+
4
27
  export declare const authTables: readonly [TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike];
5
28
 
6
29
  /**
@@ -12,6 +35,23 @@ export declare const authTables: readonly [TableLike, TableLike, TableLike, Tabl
12
35
  */
13
36
  export declare const authTokensTable: TableLike;
14
37
 
38
+ export declare const guestTables: readonly [ Table<"authGuests", FieldDefinitions<{
39
+ readonly id: ColumnBuilder<string, "id", boolean>;
40
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
41
+ readonly upgradeable: ColumnBuilder<boolean, "boolean", boolean>;
42
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
43
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
44
+ readonly lastSeenAt: ColumnBuilder<Date, "timestamp", boolean>;
45
+ }>, true, never>, Table<"authGuestDevices", FieldDefinitions<{
46
+ readonly id: ColumnBuilder<string, "id", boolean>;
47
+ readonly guestId: ColumnBuilder<string, "text", boolean>;
48
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
49
+ readonly secretHash: ColumnBuilder<string, "text", boolean>;
50
+ readonly generation: ColumnBuilder<number, "integer", boolean>;
51
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
52
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
53
+ }>, true, never>];
54
+
15
55
  /**
16
56
  * Impersonation grants — one row per "support agent A is acting as user B",
17
57
  * written when the impersonated session is minted and closed when it ends.