@voltro/plugin-auth 0.86.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.
- package/CHANGELOG.md +5321 -0
- package/README.md +83 -0
- package/THIRD-PARTY-NOTICES.md +1130 -11
- package/dist/authExtensions.js +9 -0
- package/dist/emailVerification.js +13 -11
- package/dist/guest.d.ts +99 -0
- package/dist/guest.js +3 -0
- package/dist/guestClient.js +237 -0
- package/dist/guestCredential.js +12 -0
- package/dist/guestPasswordUpgrade.js +85 -0
- package/dist/guestPlugin.js +166 -0
- package/dist/guestSchema.js +20 -0
- package/dist/guestSecrets.js +10 -0
- package/dist/guestService.js +224 -0
- package/dist/handlers.js +113 -113
- package/dist/impersonation.js +79 -78
- package/dist/index.d.ts +265 -15
- package/dist/index.js +27 -19
- package/dist/invitations.js +23 -23
- package/dist/native/guestStorage.js +41 -0
- package/dist/native.d.ts +110 -0
- package/dist/native.js +45 -43
- package/dist/nativeRoutes.js +61 -72
- package/dist/overlayPlugin.js +167 -0
- package/dist/plugin.d.ts +119 -10
- package/dist/plugin.js +99 -78
- package/dist/postgresUserStore.js +19 -7
- package/dist/revocation.js +40 -23
- package/dist/schema.d.ts +41 -1
- package/dist/schema.js +99 -98
- package/dist/sessionFamily.js +15 -0
- package/dist/sessionRegistry.js +93 -78
- package/dist/signedClaims.js +24 -0
- package/dist/strategy.d.ts +13 -9
- package/dist/strategy.js +26 -22
- package/dist/userStore.js +145 -138
- package/dist/web/guestStorage.js +47 -0
- package/dist/web.d.ts +94 -0
- package/dist/web.js +4 -2
- package/package.json +8 -3
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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
|
|
18
|
+
import { definePlugin as Ee } from "@voltro/protocol";
|
|
18
19
|
//#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) => {
|
|
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
|
-
}),
|
|
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 ??
|
|
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 ?
|
|
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 =
|
|
91
|
+
let t = re(G), i = r(e.headers, t);
|
|
94
92
|
if (!i) return null;
|
|
95
|
-
let o = e.headers[
|
|
96
|
-
if (s && !await
|
|
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
|
|
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
|
-
},
|
|
129
|
-
let t = e.headers[
|
|
130
|
-
return ee(t, n,
|
|
131
|
-
}, X = (e, t) => e.path === `${F}${t}`, Z = (e) =>
|
|
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
|
-
},
|
|
148
|
+
}, Ne = {
|
|
151
149
|
method: "*",
|
|
152
150
|
path: F,
|
|
153
151
|
handle: async (t) => {
|
|
154
|
-
let n = t.method === "GET" ? {} :
|
|
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(
|
|
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(
|
|
166
|
+
return Z(O(t.headers.cookie, n, V, e));
|
|
169
167
|
}
|
|
170
|
-
if (t.method === "POST" && X(t, "/native/exchange")) return Z(
|
|
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(
|
|
176
|
-
if (t.method === "POST" && X(t, "/sign-in")) return Z(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
223
|
-
if (P.invitations && t.method === "POST" && X(t, "/invitations/preview")) return Z(
|
|
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(
|
|
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(
|
|
240
|
-
if (P.passkey && t.method === "POST" && X(t, "/passkey/assert/verify")) return Z(
|
|
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 = () =>
|
|
248
|
-
if (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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
372
|
-
return s(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
},
|
|
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:
|
|
417
|
-
privacy:
|
|
418
|
-
httpRoutes: [
|
|
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) =>
|
|
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: () =>
|
|
453
|
+
onDeactivate: () => j.void
|
|
434
454
|
}), $ = P.secrets ?? P.secret;
|
|
435
455
|
return {
|
|
436
|
-
...
|
|
437
|
-
auth: { sessionStrategy:
|
|
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
|
-
},
|
|
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
|
-
}] },
|
|
474
|
+
}] }, je = [
|
|
454
475
|
{
|
|
455
476
|
name: "VOLTRO_SESSION_SECRET_PREVIOUS",
|
|
456
477
|
required: !1,
|
|
@@ -495,4 +516,4 @@ var P = (e) => {
|
|
|
495
516
|
}
|
|
496
517
|
];
|
|
497
518
|
//#endregion
|
|
498
|
-
export { B as authRoutesPlugin, P as
|
|
519
|
+
export { B as authRoutesPlugin, P as getAuthSessionStrategies };
|
|
@@ -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
|
|
158
|
-
|
|
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
|
|
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* () {
|
package/dist/revocation.js
CHANGED
|
@@ -1,33 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isSessionCredentialLiveFor as e } from "./sessionFamily.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
2
3
|
//#region src/revocation.ts
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
}
|
|
24
|
+
};
|
|
13
25
|
return {
|
|
14
|
-
ttlMs:
|
|
26
|
+
ttlMs: s,
|
|
15
27
|
invalidate: (e) => {
|
|
16
|
-
|
|
28
|
+
u.delete(e);
|
|
17
29
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
49
|
//#endregion
|
|
33
|
-
export {
|
|
50
|
+
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
|
-
/**
|
|
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.
|