@voltro/plugin-auth 0.33.0 → 0.34.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.
@@ -0,0 +1,1330 @@
1
+ import { hashPassword as e, verifyPassword as t, verifyPasswordWithRehash as n } from "./password.js";
2
+ import { SESSION_COOKIE_NAME as r, clearSessionCookie as i, issueSession as a, readSessionKeyed as o, resolveSessionSecret as s, resolveSessionSecrets as c, sessionSecretsOf as l } from "./session.js";
3
+ import { a as u, l as d, o as f, s as p, t as m } from "./strategy-Bty6_XUj.js";
4
+ import { hashToken as h, mintSecret as g, mintToken as _ } from "./tokens.js";
5
+ import { CSRF_COOKIE_NAME as v, CSRF_HEADER_NAME as y, issueCsrfToken as b, verifyCsrf as ee } from "./csrf.js";
6
+ import { generateChallenge as x, verifyAssertion as te, verifyRegistration as ne } from "./webauthn.js";
7
+ import { createHmac as S, randomBytes as re, timingSafeEqual as ie } from "node:crypto";
8
+ import { Data as ae, Effect as C } from "effect";
9
+ import { buildSetCookie as oe, readCookie as se } from "@voltro/protocol/session";
10
+ import { registerRetention as ce, retentionTtlMsFromEnv as le } from "@voltro/database";
11
+ import { definePlugin as ue } from "@voltro/protocol";
12
+ //#region src/subjectGuard.ts
13
+ var de = { ok: !0 }, fe = (e, t) => C.gen(function* () {
14
+ for (let n of e) {
15
+ let e = yield* n(t);
16
+ if (!e.ok) return e;
17
+ }
18
+ return de;
19
+ }), w = 30, T = 6, pe = 20, me = "sha1", he = 1, E = class extends ae.TaggedError("TotpVerifyError") {}, D = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", ge = (e) => {
20
+ let t = 0, n = 0, r = "";
21
+ for (let i of e) for (n = n << 8 | i, t += 8; t >= 5;) r += D[n >>> t - 5 & 31], t -= 5;
22
+ return t > 0 && (r += D[n << 5 - t & 31]), r;
23
+ }, _e = (e) => {
24
+ let t = e.toUpperCase().replace(/[\s=]+/g, "");
25
+ if (!/^[A-Z2-7]+$/.test(t)) return null;
26
+ let n = new Uint8Array(Math.floor(t.length * 5 / 8)), r = 0, i = 0, a = 0;
27
+ for (let e of t) {
28
+ let t = D.indexOf(e);
29
+ if (t === -1) return null;
30
+ i = i << 5 | t, r += 5, r >= 8 && (n[a++] = i >>> r - 8 & 255, r -= 8);
31
+ }
32
+ return n.subarray(0, a);
33
+ }, ve = (e) => {
34
+ let t = Buffer.alloc(8);
35
+ return t.writeBigUInt64BE(BigInt(e), 0), t;
36
+ }, ye = (e, t) => {
37
+ let n = S(me, Buffer.from(e));
38
+ n.update(ve(t));
39
+ let r = n.digest(), i = r[r.length - 1] & 15, a = (r[i] & 127) << 24 | (r[i + 1] & 255) << 16 | (r[i + 2] & 255) << 8 | r[i + 3] & 255;
40
+ return String(a % 10 ** T).padStart(T, "0");
41
+ }, be = () => ge(re(pe)), xe = (e) => `otpauth://totp/${encodeURIComponent(`${e.issuer}:${e.accountName}`)}?${new URLSearchParams({
42
+ secret: e.secret,
43
+ issuer: e.issuer,
44
+ algorithm: "SHA1",
45
+ digits: String(T),
46
+ period: String(w)
47
+ }).toString()}`, Se = (e, t = Date.now()) => {
48
+ let n = _e(e);
49
+ return n ? ye(n, Math.floor(t / 1e3 / w)) : "000000";
50
+ }, O = (e, t, n = Date.now()) => C.gen(function* () {
51
+ let r = _e(e);
52
+ if (!r) return yield* C.fail(new E({ reason: "malformedSecret" }));
53
+ let i = t.replace(/\s+/g, "");
54
+ if (!/^[0-9]{6}$/.test(i)) return yield* C.fail(new E({ reason: "invalidCode" }));
55
+ let a = Math.floor(n / 1e3 / w), o = Buffer.from(i);
56
+ for (let e = -1; e <= he; e++) {
57
+ let t = ye(r, a + e), n = Buffer.from(t);
58
+ if (n.length === o.length && ie(n, o)) return !0;
59
+ }
60
+ return yield* C.fail(new E({ reason: "invalidCode" }));
61
+ }), Ce = (e = 10) => {
62
+ let t = [];
63
+ for (let n = 0; n < e; n++) {
64
+ let e = ge(re(16)).slice(0, 25).match(/.{1,5}/g) ?? [];
65
+ t.push(e.join("-"));
66
+ }
67
+ return t;
68
+ }, we = 60, Te = "email_not_verified", Ee = "emailVerified", De = (e) => e?.policy ?? "off", k = (e) => De(e) !== "off", A = (e, t) => {
69
+ if (e.emailVerifiedAt != null) return !0;
70
+ let n = t?.exemptAccountsCreatedBefore;
71
+ return n !== void 0 && e.createdAt.getTime() < n.getTime();
72
+ }, Oe = (e) => {
73
+ if (e.type !== "user") return null;
74
+ let t = e.metadata?.[Ee];
75
+ return typeof t == "boolean" ? t : null;
76
+ }, ke = (e) => {
77
+ let t = De(e) === "strict";
78
+ return (n) => C.succeed(!t || A(n, e) ? { ok: !0 } : {
79
+ ok: !1,
80
+ code: Te,
81
+ message: "This account's email address has not been confirmed yet. Request a new confirmation link and follow it, then sign in again."
82
+ });
83
+ }, j = (e, t) => ({
84
+ status: e,
85
+ body: JSON.stringify(t),
86
+ contentType: "application/json; charset=utf-8"
87
+ }), Ae = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, M = (e, t, n) => C.gen(function* () {
88
+ let r = n.emailVerification, i = (n.now ?? Date.now)(), a = (r?.resendCooldownSeconds ?? 60) * 1e3;
89
+ if (a > 0) {
90
+ let n = yield* t.latestToken(e.id, "email-verify");
91
+ if (n && i - n.createdAt.getTime() < a) return !1;
92
+ }
93
+ let o = _("email-verify", r?.ttlSeconds ?? 86400);
94
+ if (yield* t.insertToken({
95
+ id: u("tok"),
96
+ tokenHash: o.tokenHash,
97
+ userId: e.id,
98
+ purpose: "email-verify",
99
+ expiresAt: o.expiresAt
100
+ }).pipe(C.ignore), !n.sendEmail) return !1;
101
+ let s = Ae(n.appBaseUrl ?? "", r?.callbackPath ?? "/auth/verify-email/callback", o.token);
102
+ return yield* C.tryPromise(() => n.sendEmail({
103
+ to: e.email,
104
+ kind: "email-verify",
105
+ actionUrl: s,
106
+ subject: "Confirm your email address",
107
+ text: `Confirm your email address: ${s}`,
108
+ html: `<p><a href="${s}">Confirm your email address</a> — this link expires in 24 hours.</p>`
109
+ })).pipe(C.ignore), !0;
110
+ }), N = (e, t, n) => C.gen(function* () {
111
+ if (!e.email && !e.userId) return j(400, { error: "email or userId required" });
112
+ let r = e.userId ? yield* t.findById(e.userId) : yield* t.findByEmail(e.email);
113
+ return r && !A(r, n.emailVerification) && (yield* M(r, t, n)), j(202, { ok: !0 });
114
+ }), je = (e, t) => C.gen(function* () {
115
+ if (!e.token) return j(400, { error: "token required" });
116
+ let n = yield* t.consumeToken(h(e.token), "email-verify");
117
+ if (!n) return j(401, { error: "invalid or expired token" });
118
+ let r = yield* t.findById(n.userId);
119
+ return r ? (yield* t.markEmailVerified(r.id).pipe(C.ignore), j(200, {
120
+ ok: !0,
121
+ verified: !0
122
+ })) : j(401, { error: "invalid or expired token" });
123
+ }), Me = 5, Ne = 900, Pe = 900, Fe = (e) => ({
124
+ maxAttempts: e.lockout?.maxAttempts ?? Me,
125
+ windowMs: (e.lockout?.windowSeconds ?? Ne) * 1e3,
126
+ lockMs: (e.lockout?.lockSeconds ?? Pe) * 1e3
127
+ }), P = (e) => e.secrets ?? (e.secret === void 0 ? c() : l(e.secret)), Ie = (e) => e.secret ?? s(), F = (e, t, n = {}) => ({
128
+ status: e,
129
+ body: JSON.stringify(t),
130
+ contentType: "application/json; charset=utf-8",
131
+ ...n
132
+ }), I = (e, t = {}) => ({
133
+ status: 302,
134
+ body: "",
135
+ contentType: "text/plain",
136
+ location: e,
137
+ ...t
138
+ }), Le = 3600 * 24 * 7, Re = (e, t) => e.type === "user" ? {
139
+ ...e,
140
+ metadata: {
141
+ ...e.metadata ?? {},
142
+ sessionId: t
143
+ }
144
+ } : e, L = (e, t, n, r = {}) => C.gen(function* () {
145
+ r.clearLoginFailures !== !1 && (yield* t.clearLoginFailures(e.email));
146
+ let i = yield* t.listMemberships(e.id), o = r.sessionId ?? u("sess"), s = r.ttlSeconds ?? Le;
147
+ r.sessionRow !== "existing" && (yield* t.insertSession({
148
+ id: o,
149
+ userId: e.id,
150
+ tenantId: e.tenantId,
151
+ expiresAt: new Date(Date.now() + s * 1e3),
152
+ ipAddress: r.ipAddress ?? null,
153
+ userAgent: r.userAgent ?? null
154
+ }).pipe(C.ignore));
155
+ let c = { ...r.metadata };
156
+ k(n.emailVerification) && (c[Ee] = A(e, n.emailVerification));
157
+ let l = Re(p(e, {
158
+ memberships: i,
159
+ ...Object.keys(c).length > 0 ? { metadata: c } : {}
160
+ }), o);
161
+ return {
162
+ setCookie: a(l, P(n), {
163
+ ttlSeconds: s,
164
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
165
+ secure: n.cookieSecure ?? !0
166
+ }).setCookie,
167
+ subject: l
168
+ };
169
+ }), R = (e, t) => C.gen(function* () {
170
+ let n = t.subjectGuards ?? [];
171
+ if (n.length === 0) return null;
172
+ let r = yield* fe(n, e);
173
+ return r.ok ? null : F(403, {
174
+ error: r.code,
175
+ message: r.message
176
+ });
177
+ }), ze = (r, i, a) => C.gen(function* () {
178
+ if (!r.email || !r.password) return F(400, { error: "email and password required" });
179
+ let o = yield* i.findByEmail(r.email), s = (a.now ?? Date.now)(), c = yield* i.isLockedOut(r.email, s);
180
+ if (c.locked) return F(429, {
181
+ error: "account_locked",
182
+ retryAfterSeconds: c.retryAfterSeconds
183
+ });
184
+ let l = o?.passwordHash, d = typeof l == "string" && l.length > 0 ? yield* n(r.password, l) : yield* C.gen(function* () {
185
+ let n = yield* e("decoy-not-a-real-password").pipe(C.orElseSucceed(() => ""));
186
+ return yield* t(r.password, n), { valid: !1 };
187
+ });
188
+ if (!o || !d.valid) return yield* i.recordLoginFailure(r.email, s, Fe(a)), F(401, { error: "invalid credentials" });
189
+ let f = yield* R(o, a);
190
+ if (f) return f;
191
+ if (d.rehash && (yield* i.updatePassword(o.id, d.rehash).pipe(C.ignore)), o.mfaEnrolledAt) {
192
+ let e = _("mfa-pending", 300);
193
+ return yield* i.insertToken({
194
+ id: u("tok"),
195
+ tokenHash: e.tokenHash,
196
+ userId: o.id,
197
+ purpose: "mfa-pending",
198
+ expiresAt: e.expiresAt
199
+ }).pipe(C.ignore), F(200, {
200
+ ok: !0,
201
+ mfaRequired: !0,
202
+ pendingToken: e.token,
203
+ expiresInSeconds: 300
204
+ });
205
+ }
206
+ let { setCookie: p, subject: m } = yield* L(o, i, a, {
207
+ ipAddress: r.ipAddress ?? null,
208
+ userAgent: r.userAgent ?? null
209
+ });
210
+ return r.redirectAfter ? I(a.successRedirect ?? "/", { setCookie: p }) : F(200, {
211
+ ok: !0,
212
+ subject: m
213
+ }, { setCookie: p });
214
+ }), Be = (e, t, n) => C.gen(function* () {
215
+ if (!e.pendingToken) return F(400, { error: "pending token required" });
216
+ if (!e.code && !e.recoveryCode) return F(400, { error: "code or recoveryCode required" });
217
+ let r = yield* t.consumeToken(h(e.pendingToken), "mfa-pending");
218
+ if (!r) return F(401, { error: "invalid or expired challenge" });
219
+ let i = yield* t.findById(r.userId);
220
+ if (!i || !i.mfaEnrolledAt || !i.mfaSecret) return F(401, { error: "mfa not enrolled" });
221
+ let a = (n.now ?? Date.now)(), o = yield* t.isLockedOut(i.email, a);
222
+ if (o.locked) return F(429, {
223
+ error: "account_locked",
224
+ retryAfterSeconds: o.retryAfterSeconds
225
+ });
226
+ let s = yield* R(i, n);
227
+ if (s) return s;
228
+ if (!(e.recoveryCode ? yield* t.consumeRecoveryCode(i.id, h(e.recoveryCode.trim())) : yield* O(i.mfaSecret, e.code ?? "").pipe(C.map(() => !0), C.catchTag("TotpVerifyError", () => C.succeed(!1))))) return yield* t.recordLoginFailure(i.email, a, Fe(n)), F(401, { error: "invalid_code" });
229
+ let { setCookie: c, subject: l } = yield* L(i, t, n, {
230
+ ipAddress: e.ipAddress ?? null,
231
+ userAgent: e.userAgent ?? null
232
+ });
233
+ return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: c }) : F(200, {
234
+ ok: !0,
235
+ subject: l
236
+ }, { setCookie: c });
237
+ }), Ve = (t, n, r) => C.gen(function* () {
238
+ if (!t.email || !t.password) return F(400, { error: "email and password required" });
239
+ if (t.password.length < 8) return F(400, { error: "password must be at least 8 characters" });
240
+ if (yield* n.findByEmail(t.email)) return F(409, { error: "email already registered" });
241
+ let i = t.tenantId ?? r.defaultTenantId, a = yield* e(t.password), o = yield* n.insert({
242
+ id: u("user"),
243
+ email: t.email,
244
+ passwordHash: a,
245
+ tenantId: i
246
+ });
247
+ yield* n.addMembership({
248
+ userId: o.id,
249
+ tenantId: i,
250
+ role: "owner"
251
+ }).pipe(C.ignore), k(r.emailVerification) && (r.emailVerification?.sendOnSignUp ?? !0) && (yield* M(o, n, r).pipe(C.ignore));
252
+ let { setCookie: s, subject: c } = yield* L(o, n, r);
253
+ return t.redirectAfter ? I(r.successRedirect ?? "/", { setCookie: s }) : F(200, {
254
+ ok: !0,
255
+ subject: c
256
+ }, { setCookie: s });
257
+ }).pipe(C.catchAll((e) => C.succeed(F(500, {
258
+ error: "signup_failed",
259
+ cause: String(e)
260
+ })))), He = (e) => ({
261
+ status: 302,
262
+ body: "",
263
+ location: e.successRedirect ?? "/",
264
+ setCookie: i({
265
+ ...e.cookieDomain ? { domain: e.cookieDomain } : {},
266
+ secure: e.cookieSecure ?? !0
267
+ })
268
+ }), Ue = (e, t) => C.gen(function* () {
269
+ let n = yield* t.findById(e.userId);
270
+ if (!n) return F(404, { error: "user_not_found" });
271
+ let r = be();
272
+ return yield* t.setMfaSecret(n.id, r).pipe(C.catchAll(() => C.succeed(n))), F(200, {
273
+ ok: !0,
274
+ secret: r,
275
+ otpauthUrl: xe({
276
+ issuer: e.issuer,
277
+ accountName: e.accountName,
278
+ secret: r
279
+ })
280
+ });
281
+ }), We = (e, t) => C.gen(function* () {
282
+ let n = yield* t.findById(e.userId);
283
+ if (!n) return F(404, { error: "user_not_found" });
284
+ if (!n.mfaSecret) return F(400, { error: "no_enrollment_in_progress" });
285
+ if (n.mfaEnrolledAt) return F(409, { error: "already_enrolled" });
286
+ if (!(yield* O(n.mfaSecret, e.code).pipe(C.map(() => !0), C.catchTag("TotpVerifyError", () => C.succeed(!1))))) return F(401, { error: "invalid_code" });
287
+ yield* t.markMfaEnrolled(n.id).pipe(C.catchAll(() => C.succeed(n)));
288
+ let r = Ce();
289
+ return yield* t.replaceRecoveryCodes(n.id, r.map((e) => h(e))).pipe(C.ignore), F(200, {
290
+ ok: !0,
291
+ enrolled: !0,
292
+ recoveryCodes: r
293
+ });
294
+ }), Ge = (e, t) => C.gen(function* () {
295
+ let n = yield* t.findById(e.userId);
296
+ if (!n) return F(404, { error: "user_not_found" });
297
+ if (!n.mfaEnrolledAt) return F(400, { error: "mfa_not_enrolled" });
298
+ let r = Ce();
299
+ return yield* t.replaceRecoveryCodes(n.id, r.map((e) => h(e))).pipe(C.ignore), F(200, {
300
+ ok: !0,
301
+ recoveryCodes: r
302
+ });
303
+ }), Ke = (e, t) => C.gen(function* () {
304
+ let n = yield* t.findById(e.userId);
305
+ return n ? (yield* t.clearMfa(n.id).pipe(C.catchAll(() => C.succeed(n))), yield* t.replaceRecoveryCodes(n.id, []).pipe(C.ignore), F(200, {
306
+ ok: !0,
307
+ enrolled: !1
308
+ })) : F(404, { error: "user_not_found" });
309
+ }), qe = (e) => {
310
+ let t = b(Ie(e)), n = oe(v, t, {
311
+ httpOnly: !1,
312
+ sameSite: "lax",
313
+ secure: e.cookieSecure ?? !0,
314
+ ...e.cookieDomain ? { domain: e.cookieDomain } : {},
315
+ path: "/"
316
+ });
317
+ return F(200, { csrfToken: t }, { setCookie: n });
318
+ }, Je = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, Ye = (e, t, n) => C.gen(function* () {
319
+ if (!e.email) return F(400, { error: "email required" });
320
+ let r = yield* t.findByEmail(e.email);
321
+ if (r) {
322
+ let i = _("magic-link");
323
+ yield* t.insertToken({
324
+ id: u("tok"),
325
+ tokenHash: i.tokenHash,
326
+ userId: r.id,
327
+ purpose: "magic-link",
328
+ expiresAt: i.expiresAt
329
+ }).pipe(C.ignore);
330
+ let a = Je(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/magic-link/callback", i.token);
331
+ n.sendEmail && (yield* C.tryPromise(() => n.sendEmail({
332
+ to: r.email,
333
+ kind: "magic-link",
334
+ actionUrl: a,
335
+ subject: "Your sign-in link",
336
+ text: `Sign in: ${a}`,
337
+ html: `<p><a href="${a}">Sign in</a> — this link expires shortly.</p>`
338
+ })).pipe(C.ignore));
339
+ }
340
+ return F(202, { ok: !0 });
341
+ }), Xe = (e, t, n) => C.gen(function* () {
342
+ if (!e.token) return F(400, { error: "token required" });
343
+ let r = yield* t.consumeToken(h(e.token), "magic-link");
344
+ if (!r) return F(401, { error: "invalid or expired token" });
345
+ let i = yield* t.findById(r.userId);
346
+ if (!i) return F(401, { error: "invalid or expired token" });
347
+ let a = yield* t.markEmailVerified(i.id).pipe(C.orElseSucceed(() => i)), o = yield* R(a, n);
348
+ if (o) return o;
349
+ let { setCookie: s, subject: c } = yield* L(a, t, n);
350
+ return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: s }) : F(200, {
351
+ ok: !0,
352
+ subject: c
353
+ }, { setCookie: s });
354
+ }), Ze = (e, t, n) => C.gen(function* () {
355
+ if (!e.email) return F(400, { error: "email required" });
356
+ let r = yield* t.findByEmail(e.email);
357
+ if (r) {
358
+ let i = _("password-reset");
359
+ yield* t.insertToken({
360
+ id: u("tok"),
361
+ tokenHash: i.tokenHash,
362
+ userId: r.id,
363
+ purpose: "password-reset",
364
+ expiresAt: i.expiresAt
365
+ }).pipe(C.ignore);
366
+ let a = Je(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/password-reset/callback", i.token);
367
+ n.sendEmail && (yield* C.tryPromise(() => n.sendEmail({
368
+ to: r.email,
369
+ kind: "password-reset",
370
+ actionUrl: a,
371
+ subject: "Reset your password",
372
+ text: `Reset your password: ${a}`,
373
+ html: `<p><a href="${a}">Reset your password</a> — this link expires shortly.</p>`
374
+ })).pipe(C.ignore));
375
+ }
376
+ return F(202, { ok: !0 });
377
+ }), Qe = (t, n) => C.gen(function* () {
378
+ if (!t.token || !t.newPassword) return F(400, { error: "token and newPassword required" });
379
+ if (t.newPassword.length < 8) return F(400, { error: "password must be at least 8 characters" });
380
+ let r = yield* n.consumeToken(h(t.token), "password-reset");
381
+ if (!r) return F(401, { error: "invalid or expired token" });
382
+ let i = yield* e(t.newPassword).pipe(C.orElseSucceed(() => ""));
383
+ return i ? (yield* n.updatePassword(r.userId, i).pipe(C.ignore), yield* n.markEmailVerified(r.userId).pipe(C.ignore), yield* n.revokeAllOtherSessions(r.userId, "__none__").pipe(C.ignore), F(200, { ok: !0 })) : F(500, { error: "hash_failed" });
384
+ }), $e = (e, t) => C.gen(function* () {
385
+ return F(200, { sessions: (yield* t.listSessions(e.userId)).map((e) => ({
386
+ id: e.id,
387
+ tenantId: e.tenantId,
388
+ ipAddress: e.ipAddress ?? null,
389
+ userAgent: e.userAgent ?? null,
390
+ createdAt: e.createdAt.toISOString(),
391
+ lastSeenAt: e.lastSeenAt.toISOString(),
392
+ expiresAt: e.expiresAt.toISOString()
393
+ })) });
394
+ }), et = (e, t) => C.gen(function* () {
395
+ return (yield* t.revokeSession(e.userId, e.sessionId)) ? F(200, {
396
+ ok: !0,
397
+ revoked: 1
398
+ }) : F(404, { error: "session_not_found" });
399
+ }), tt = (e, t) => C.gen(function* () {
400
+ return F(200, {
401
+ ok: !0,
402
+ revoked: yield* t.revokeAllOtherSessions(e.userId, e.keepSessionId)
403
+ });
404
+ }), nt = (e, t, n, i) => C.gen(function* () {
405
+ let o = yield* t.findById(e.userId);
406
+ if (!o) return F(404, { error: "user_not_found" });
407
+ let s = yield* t.membershipRole(e.userId, e.targetTenantId);
408
+ if (s === null) return F(403, { error: "not a member of the target tenant" });
409
+ let c = yield* t.listMemberships(o.id), l = e.metadata === void 0 ? void 0 : Object.fromEntries(Object.entries(e.metadata).filter(([e]) => e !== "memberships")), u = p(o, {
410
+ tenantId: e.targetTenantId,
411
+ memberships: c,
412
+ ...l === void 0 ? {} : { metadata: l }
413
+ }), d = e.sessionId ? Re(u, e.sessionId) : u, f = a(d, P(n), {
414
+ ttlSeconds: Le,
415
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
416
+ secure: n.cookieSecure ?? !0
417
+ });
418
+ return e.clientId !== void 0 && i && i(e.clientId, { cookies: { [r]: f.value } }), F(200, {
419
+ ok: !0,
420
+ tenantId: e.targetTenantId,
421
+ role: s,
422
+ subject: d
423
+ }, { setCookie: f.setCookie });
424
+ }), rt = (e, t) => C.gen(function* () {
425
+ return F(200, { memberships: (yield* t.listMemberships(e.userId)).map((e) => ({
426
+ tenantId: e.tenantId,
427
+ role: e.role,
428
+ joinedAt: e.joinedAt.toISOString()
429
+ })) });
430
+ }), z = (e, t, n = {}) => ({
431
+ status: e,
432
+ body: JSON.stringify(t),
433
+ contentType: "application/json; charset=utf-8",
434
+ ...n
435
+ }), it = 3600 * 24 * 7, at = () => {
436
+ let e = /* @__PURE__ */ new Map();
437
+ return {
438
+ put: (t, n, r) => C.sync(() => {
439
+ e.set(t, {
440
+ challenge: n,
441
+ expiresAt: Date.now() + r * 1e3
442
+ });
443
+ }),
444
+ take: (t) => C.sync(() => {
445
+ let n = e.get(t);
446
+ return e.delete(t), !n || n.expiresAt < Date.now() ? null : n.challenge;
447
+ })
448
+ };
449
+ }, ot = (e) => ({
450
+ put: (t, n, r) => C.gen(function* () {
451
+ let i = /* @__PURE__ */ new Date(), a = new Date(i.getTime() + r * 1e3);
452
+ (yield* e`
453
+ UPDATE "passkeyChallenges"
454
+ SET challenge = ${n}, "expiresAt" = ${a}, "createdAt" = ${i}
455
+ WHERE key = ${t}
456
+ RETURNING key
457
+ `).length === 0 && (yield* e`
458
+ INSERT INTO "passkeyChallenges" (id, key, challenge, "expiresAt", "createdAt")
459
+ VALUES (${u("pkc")}, ${t}, ${n}, ${a}, ${i})
460
+ `);
461
+ }).pipe(C.asVoid, C.orElseSucceed(() => void 0)),
462
+ take: (t) => C.gen(function* () {
463
+ let n = (yield* e`
464
+ DELETE FROM "passkeyChallenges" WHERE key = ${t}
465
+ RETURNING challenge, "expiresAt"
466
+ `)[0];
467
+ return n && n.expiresAt.getTime() > Date.now() ? n.challenge : null;
468
+ }).pipe(C.orElseSucceed(() => null))
469
+ }), B = 300, st = (e, t, n, r) => C.gen(function* () {
470
+ if (!(yield* t.findById(e.userId))) return z(404, { error: "user_not_found" });
471
+ let i = x();
472
+ yield* n.put(`${e.userId}:register`, i, B);
473
+ let a = yield* t.listPasskeys(e.userId);
474
+ return z(200, {
475
+ challenge: i,
476
+ rp: {
477
+ id: r.rpId,
478
+ name: r.rpName
479
+ },
480
+ user: {
481
+ id: e.userId,
482
+ name: e.userName,
483
+ displayName: e.userName
484
+ },
485
+ pubKeyCredParams: [{
486
+ type: "public-key",
487
+ alg: -7
488
+ }, {
489
+ type: "public-key",
490
+ alg: -257
491
+ }],
492
+ excludeCredentials: a.map((e) => ({
493
+ type: "public-key",
494
+ id: e.credentialId
495
+ })),
496
+ authenticatorSelection: {
497
+ userVerification: "preferred",
498
+ residentKey: "preferred"
499
+ },
500
+ timeout: B * 1e3
501
+ });
502
+ }), ct = (e, t, n, r) => C.gen(function* () {
503
+ if (!(yield* t.findById(e.userId))) return z(404, { error: "user_not_found" });
504
+ let i = yield* n.take(`${e.userId}:register`);
505
+ if (!i) return z(400, { error: "no_pending_challenge" });
506
+ let a = ne({
507
+ clientDataJSON: e.clientDataJSON,
508
+ authenticatorData: e.authenticatorData,
509
+ expectedChallenge: i,
510
+ expectedOrigin: r.origin,
511
+ rpId: r.rpId
512
+ });
513
+ return a.ok ? (yield* t.insertPasskey({
514
+ id: u("pk"),
515
+ credentialId: a.result.credentialId,
516
+ userId: e.userId,
517
+ publicKey: a.result.publicKey,
518
+ counter: a.result.counter,
519
+ transports: e.transports ?? null
520
+ }).pipe(C.ignore), z(200, {
521
+ ok: !0,
522
+ credentialId: a.result.credentialId
523
+ })) : z(400, { error: a.error });
524
+ }), lt = (e, t, n, r) => C.gen(function* () {
525
+ let i = x(), a = e.userId ? `${e.userId}:assert` : "*:assert";
526
+ yield* n.put(a, i, B);
527
+ let o = e.userId ? (yield* t.listPasskeys(e.userId)).map((e) => ({
528
+ type: "public-key",
529
+ id: e.credentialId
530
+ })) : [];
531
+ return z(200, {
532
+ challenge: i,
533
+ rpId: r.rpId,
534
+ allowCredentials: o,
535
+ userVerification: "preferred",
536
+ timeout: B * 1e3
537
+ });
538
+ }), ut = (e, t, n, r, i) => C.gen(function* () {
539
+ let o = yield* t.findPasskey(e.credentialId);
540
+ if (!o) return z(401, { error: "unknown_credential" });
541
+ let s = e.userId ? `${e.userId}:assert` : "*:assert", c = yield* n.take(s);
542
+ if (!c) return z(400, { error: "no_pending_challenge" });
543
+ let l = te({
544
+ clientDataJSON: e.clientDataJSON,
545
+ authenticatorData: e.authenticatorData,
546
+ signature: e.signature,
547
+ expectedChallenge: c,
548
+ expectedOrigin: r.origin,
549
+ rpId: r.rpId,
550
+ storedPublicKey: o.publicKey,
551
+ storedCounter: o.counter
552
+ });
553
+ if (!l.ok) return z(401, { error: l.error });
554
+ if (!(yield* t.advancePasskeyCounter(e.credentialId, l.result.newCounter)) && l.result.newCounter !== 0) return z(401, { error: "counter_regressed" });
555
+ let d = yield* t.findById(o.userId);
556
+ if (!d) return z(401, { error: "unknown_credential" });
557
+ let f = yield* R(d, i);
558
+ if (f) return f;
559
+ let m = yield* t.listMemberships(d.id), h = u("sess");
560
+ yield* t.insertSession({
561
+ id: h,
562
+ userId: d.id,
563
+ tenantId: d.tenantId,
564
+ expiresAt: new Date(Date.now() + it * 1e3),
565
+ ipAddress: null,
566
+ userAgent: null
567
+ }).pipe(C.ignore);
568
+ let g = p(d, { memberships: m }), _ = g.type === "user" ? {
569
+ ...g,
570
+ metadata: {
571
+ ...g.metadata ?? {},
572
+ sessionId: h
573
+ }
574
+ } : g, v = a(_, P(i), {
575
+ ttlSeconds: it,
576
+ ...i.cookieDomain ? { domain: i.cookieDomain } : {},
577
+ secure: i.cookieSecure ?? !0
578
+ });
579
+ return z(200, {
580
+ ok: !0,
581
+ subject: _
582
+ }, { setCookie: v.setCookie });
583
+ }), dt = 10080 * 60, ft = 500, V = [
584
+ "owner",
585
+ "admin",
586
+ "member",
587
+ "viewer"
588
+ ], pt = ["owner", "admin"], mt = (e, t) => t.indexOf(e), ht = (e, t = V) => {
589
+ let n = mt(e.inviterRole, t);
590
+ if (n < 0) return !1;
591
+ let r = mt(e.requestedRole, t);
592
+ return r < 0 ? n === 0 : r >= n;
593
+ }, gt = (e) => {
594
+ let t = e?.roleRank ?? V;
595
+ return e?.canGrantRole ?? ((e) => ht(e, t));
596
+ }, H = (e, t, n = {}) => ({
597
+ status: e,
598
+ body: JSON.stringify(t),
599
+ contentType: "application/json; charset=utf-8",
600
+ ...n
601
+ }), _t = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, vt = (e) => ({
602
+ id: e.id,
603
+ email: e.email,
604
+ tenantId: e.tenantId,
605
+ role: e.role,
606
+ invitedByUserId: e.invitedByUserId,
607
+ expiresAt: e.expiresAt.toISOString(),
608
+ acceptedAt: e.acceptedAt?.toISOString() ?? null,
609
+ revokedAt: e.revokedAt?.toISOString() ?? null,
610
+ createdAt: e.createdAt.toISOString()
611
+ }), U = (e, t, n, r) => C.gen(function* () {
612
+ let i = yield* n.membershipRole(e, t);
613
+ return i === null || !(r?.inviterRoles ?? pt).includes(i) ? H(403, { error: "not_permitted" }) : { role: i };
614
+ }), W = (e) => "status" in e, yt = (e, t, n) => C.gen(function* () {
615
+ let r = e.email.trim().toLowerCase();
616
+ if (!r || !r.includes("@")) return H(400, { error: "a valid email is required" });
617
+ if (!e.tenantId) return H(400, { error: "tenantId required" });
618
+ if (!e.role) return H(400, { error: "role required" });
619
+ let i = n.invitations, a = yield* U(e.inviterUserId, e.tenantId, t, i);
620
+ if (W(a)) return a;
621
+ if (!gt(i)({
622
+ inviterRole: a.role,
623
+ requestedRole: e.role,
624
+ tenantId: e.tenantId
625
+ })) return H(403, {
626
+ error: "role_not_grantable",
627
+ message: `A '${a.role}' may not invite a '${e.role}'.`
628
+ });
629
+ let o = new Date((n.now ?? Date.now)());
630
+ if ((yield* t.countPendingInvitations(e.tenantId, o)) >= (i?.maxPending ?? 500)) return H(429, { error: "too_many_pending_invitations" });
631
+ yield* t.supersedePendingInvitations(e.tenantId, r, e.inviterUserId, o).pipe(C.ignore);
632
+ let s = i?.ttlSeconds ?? 604800, c = g(s), l = yield* t.insertInvitation({
633
+ id: u("inv"),
634
+ tokenHash: c.tokenHash,
635
+ email: r,
636
+ tenantId: e.tenantId,
637
+ role: e.role,
638
+ invitedByUserId: e.inviterUserId,
639
+ expiresAt: new Date(o.getTime() + s * 1e3)
640
+ });
641
+ if (n.sendEmail) {
642
+ let t = _t(n.appBaseUrl ?? "", i?.callbackPath ?? "/auth/invitations/accept", c.token);
643
+ yield* C.tryPromise(() => n.sendEmail({
644
+ to: r,
645
+ kind: "invitation",
646
+ actionUrl: t,
647
+ subject: "You have been invited",
648
+ text: `You have been invited to join as ${e.role}: ${t}`,
649
+ html: `<p>You have been invited to join as <strong>${e.role}</strong>. <a href="${t}">Accept the invitation</a> — this link expires.</p>`
650
+ })).pipe(C.ignore);
651
+ }
652
+ return H(201, {
653
+ ok: !0,
654
+ invitation: vt(l)
655
+ });
656
+ }), bt = (e, t, n) => C.gen(function* () {
657
+ let r = yield* U(e.callerUserId, e.tenantId, t, n.invitations);
658
+ return W(r) ? r : H(200, { invitations: (yield* t.listInvitations(e.tenantId)).map(vt) });
659
+ }), xt = (e, t, n) => C.gen(function* () {
660
+ if (!e.invitationId) return H(400, { error: "invitationId required" });
661
+ let r = yield* U(e.callerUserId, e.tenantId, t, n.invitations);
662
+ if (W(r)) return r;
663
+ let i = new Date((n.now ?? Date.now)());
664
+ return (yield* t.revokeInvitation(e.invitationId, e.tenantId, e.callerUserId, i)) ? H(200, {
665
+ ok: !0,
666
+ revoked: 1
667
+ }) : H(404, { error: "invitation_not_found" });
668
+ }), St = (e, t, n = {}) => C.gen(function* () {
669
+ if (!e.token) return H(400, { error: "token required" });
670
+ let r = yield* t.findInvitationByHash(h(e.token));
671
+ if (!r) return H(404, { error: "invalid_invitation" });
672
+ let i = new Date((n.now ?? Date.now)());
673
+ return H(200, {
674
+ ok: !0,
675
+ state: r.acceptedAt === null ? r.revokedAt === null ? r.expiresAt.getTime() <= i.getTime() ? "expired" : "pending" : "revoked" : "accepted",
676
+ email: r.email,
677
+ tenantId: r.tenantId,
678
+ role: r.role,
679
+ expiresAt: r.expiresAt.toISOString()
680
+ });
681
+ }), Ct = (e, t, n = {}) => C.gen(function* () {
682
+ if (!e.token) return H(400, { error: "token required" });
683
+ let r = h(e.token), i = new Date((n.now ?? Date.now)()), a = yield* t.findInvitationByHash(r);
684
+ if (!a) return H(401, { error: "invalid_or_expired_invitation" });
685
+ if (!e.userId) return H(401, {
686
+ error: "authentication_required",
687
+ email: a.email
688
+ });
689
+ let o = yield* t.findById(e.userId);
690
+ if (!o) return H(401, {
691
+ error: "authentication_required",
692
+ email: a.email
693
+ });
694
+ if (o.email.toLowerCase() !== a.email.toLowerCase()) return H(403, {
695
+ error: "invitation_email_mismatch",
696
+ message: "This invitation was issued to a different email address. Sign in as that address to accept it."
697
+ });
698
+ let s = yield* t.acceptInvitation(r, o.id, i);
699
+ if (!s) return H(401, { error: "invalid_or_expired_invitation" });
700
+ let c = yield* t.addMembership({
701
+ userId: o.id,
702
+ tenantId: s.tenantId,
703
+ role: s.role
704
+ });
705
+ return yield* t.markEmailVerified(o.id).pipe(C.ignore), H(200, {
706
+ ok: !0,
707
+ membership: {
708
+ tenantId: c.tenantId,
709
+ role: c.role,
710
+ joinedAt: c.joinedAt.toISOString()
711
+ }
712
+ });
713
+ }), wt = (e, t, n) => C.gen(function* () {
714
+ if (!e.token || !e.password) return H(400, { error: "token and password required" });
715
+ if (e.password.length < (n.minPasswordLength ?? 8)) return H(400, { error: "password must be at least 8 characters" });
716
+ let r = h(e.token), i = new Date((n.now ?? Date.now)()), a = yield* t.findInvitationByHash(r);
717
+ if (!a) return H(401, { error: "invalid_or_expired_invitation" });
718
+ if (yield* t.findByEmail(a.email)) return H(409, {
719
+ error: "account_exists",
720
+ message: "An account already exists for this address. Sign in, then accept the invitation."
721
+ });
722
+ let o = yield* n.hashPassword(e.password).pipe(C.orElseSucceed(() => ""));
723
+ if (!o) return H(500, { error: "hash_failed" });
724
+ let s = u("user"), c = yield* t.acceptInvitation(r, s, i);
725
+ if (!c) return H(401, { error: "invalid_or_expired_invitation" });
726
+ let l = yield* t.insert({
727
+ id: s,
728
+ email: c.email,
729
+ passwordHash: o,
730
+ tenantId: c.tenantId
731
+ }).pipe(C.orElseSucceed(() => null));
732
+ return l ? (yield* t.addMembership({
733
+ userId: l.id,
734
+ tenantId: c.tenantId,
735
+ role: c.role
736
+ }), yield* t.markEmailVerified(l.id).pipe(C.ignore), H(201, {
737
+ ok: !0,
738
+ userId: l.id,
739
+ tenantId: c.tenantId,
740
+ role: c.role
741
+ })) : H(409, { error: "account_exists" });
742
+ }), Tt = 900, G = "impersonation", Et = [
743
+ "/mfa/enroll/start",
744
+ "/mfa/enroll/verify",
745
+ "/mfa/unenroll",
746
+ "/mfa/recovery-codes/regenerate",
747
+ "/passkey/register/options",
748
+ "/passkey/register/verify",
749
+ "/sessions/revoke-others",
750
+ "/switch-tenant",
751
+ "/impersonate/start"
752
+ ], Dt = (e) => {
753
+ if (typeof e != "object" || !e) return null;
754
+ let t = e;
755
+ return typeof t.actorUserId != "string" || t.actorUserId.length === 0 || typeof t.grantId != "string" || typeof t.expiresAt != "string" || typeof t.actorSessionId != "string" || typeof t.startedAt != "string" ? null : {
756
+ actorUserId: t.actorUserId,
757
+ actorSessionId: t.actorSessionId,
758
+ grantId: t.grantId,
759
+ startedAt: t.startedAt,
760
+ expiresAt: t.expiresAt,
761
+ ...typeof t.reason == "string" ? { reason: t.reason } : {}
762
+ };
763
+ }, K = (e) => e.type === "user" ? Dt(e.metadata?.[G]) : null, q = (e) => K(e) !== null, Ot = () => (e) => {
764
+ if (e.type !== "user" || e.metadata === void 0) return e;
765
+ let t = Dt(e.metadata[G]), { metadata: n, ...r } = e;
766
+ return {
767
+ ...r,
768
+ metadata: t === null ? { __redacted: "all" } : {
769
+ __redacted: "all-except-impersonation",
770
+ [G]: t
771
+ }
772
+ };
773
+ }, J = (e, t, n = {}) => ({
774
+ status: e,
775
+ body: JSON.stringify(t),
776
+ contentType: "application/json; charset=utf-8",
777
+ ...n
778
+ }), Y = (e, t) => C.suspend(() => {
779
+ let n = e.audit(t);
780
+ return n === void 0 ? C.void : C.isEffect(n) ? n : C.tryPromise({
781
+ try: () => Promise.resolve(n),
782
+ catch: (e) => e
783
+ });
784
+ }).pipe(C.catchAllCause(() => C.void)), kt = (e, t, n) => C.gen(function* () {
785
+ let r = n.impersonation, i = (n.now ?? Date.now)(), a = new Date(i), o = e.actorSubject, s = (t, n, i = {}) => Y(r, {
786
+ kind: "refused",
787
+ at: a,
788
+ actorUserId: i.actorUser?.id ?? (o.type === "user" ? o.id : null),
789
+ actorEmail: i.actorUser?.email ?? null,
790
+ targetUserId: i.target?.id ?? null,
791
+ targetEmail: i.target?.email ?? null,
792
+ tenantId: o.tenantId ?? null,
793
+ reason: e.reason ?? null,
794
+ refusal: n,
795
+ ipAddress: e.ipAddress ?? null,
796
+ userAgent: e.userAgent ?? null
797
+ }).pipe(C.as(J(t, {
798
+ error: n,
799
+ ...i.message === void 0 ? {} : { message: i.message }
800
+ })));
801
+ if (o.type !== "user") return yield* s(401, "unauthenticated");
802
+ if (q(o)) return yield* s(403, "nested_impersonation", { message: "An impersonated session may not start another impersonation. Stop the current one first." });
803
+ if (!e.actorSessionId) return yield* s(400, "no_actor_session");
804
+ let c = yield* t.findById(o.id);
805
+ if (!c) return yield* s(401, "unauthenticated");
806
+ if (!e.targetUserId && !e.targetEmail) return yield* s(400, "target_required", { actorUser: c });
807
+ let l = e.targetUserId ? yield* t.findById(e.targetUserId) : yield* t.findByEmail(e.targetEmail);
808
+ if (!l) return yield* s(404, "target_not_found", { actorUser: c });
809
+ if (l.id === c.id) return yield* s(400, "self_impersonation", {
810
+ actorUser: c,
811
+ target: l
812
+ });
813
+ let d = e.reason?.trim() ?? "";
814
+ if ((r.requireReason ?? !0) && d.length === 0) return yield* s(400, "reason_required", {
815
+ actorUser: c,
816
+ target: l,
817
+ message: "A reason is required to impersonate a user."
818
+ });
819
+ if (!(yield* r.authority({
820
+ actor: o,
821
+ actorUser: c,
822
+ target: l
823
+ }).pipe(C.catchAllCause(() => C.succeed(!1))))) return yield* s(403, "not_permitted", {
824
+ actorUser: c,
825
+ target: l
826
+ });
827
+ if (yield* r.authority({
828
+ actor: p(l),
829
+ actorUser: l,
830
+ target: c
831
+ }).pipe(C.catchAllCause(() => C.succeed(!0)))) return yield* s(403, "target_may_impersonate", {
832
+ actorUser: c,
833
+ target: l,
834
+ message: "This user may themselves impersonate others and cannot be impersonated."
835
+ });
836
+ let f = r.maxDurationSeconds ?? 900, m = Math.max(1, Math.min(e.durationSeconds ?? f, f)), h = new Date(i + m * 1e3), g = u("sess"), _ = yield* t.insertImpersonationGrant({
837
+ id: u("imp"),
838
+ sessionId: g,
839
+ actorUserId: c.id,
840
+ actorSessionId: e.actorSessionId,
841
+ targetUserId: l.id,
842
+ tenantId: l.tenantId,
843
+ reason: d.length > 0 ? d : null,
844
+ expiresAt: h,
845
+ ipAddress: e.ipAddress ?? null,
846
+ userAgent: e.userAgent ?? null
847
+ }), v = {
848
+ actorUserId: c.id,
849
+ actorSessionId: e.actorSessionId,
850
+ grantId: _.id,
851
+ startedAt: _.startedAt.toISOString(),
852
+ expiresAt: h.toISOString(),
853
+ ...d.length > 0 ? { reason: d } : {}
854
+ }, y = yield* n.issuer.issue(l, {
855
+ sessionId: g,
856
+ ttlSeconds: m,
857
+ metadata: { [G]: v },
858
+ ipAddress: e.ipAddress ?? null,
859
+ userAgent: e.userAgent ?? null
860
+ });
861
+ return yield* Y(r, {
862
+ kind: "started",
863
+ at: a,
864
+ actorUserId: c.id,
865
+ actorEmail: c.email,
866
+ targetUserId: l.id,
867
+ targetEmail: l.email,
868
+ tenantId: l.tenantId,
869
+ sessionId: g,
870
+ grantId: _.id,
871
+ reason: d.length > 0 ? d : null,
872
+ expiresAt: h,
873
+ ipAddress: e.ipAddress ?? null,
874
+ userAgent: e.userAgent ?? null
875
+ }), J(200, {
876
+ ok: !0,
877
+ impersonation: v,
878
+ subject: y.subject
879
+ }, { setCookie: y.setCookie });
880
+ }), At = (e, t, n) => C.gen(function* () {
881
+ let r = n.impersonation, i = (n.now ?? Date.now)(), a = new Date(i), o = K(e.subject);
882
+ if (!o || !e.sessionId) return J(400, { error: "not_impersonating" });
883
+ let s = yield* t.findImpersonationGrantBySession(e.sessionId);
884
+ yield* t.endImpersonationGrant(e.sessionId, "stopped", a).pipe(C.ignore), e.subject.type === "user" && (yield* t.revokeSession(e.subject.id, e.sessionId).pipe(C.ignore)), n.invalidateSession?.(e.sessionId);
885
+ let c = yield* t.findById(o.actorUserId), l = yield* t.findSession(o.actorSessionId), u = l ? Math.floor((l.expiresAt.getTime() - i) / 1e3) : 0;
886
+ if (yield* Y(r, {
887
+ kind: "stopped",
888
+ at: a,
889
+ actorUserId: o.actorUserId,
890
+ actorEmail: c?.email ?? null,
891
+ targetUserId: e.subject.type === "user" ? e.subject.id : null,
892
+ targetEmail: null,
893
+ tenantId: s?.tenantId ?? e.subject.tenantId ?? null,
894
+ sessionId: e.sessionId,
895
+ ...s ? { grantId: s.id } : {},
896
+ reason: s?.reason ?? null
897
+ }), !c || !l || u <= 0) return J(200, {
898
+ ok: !0,
899
+ restored: !1
900
+ }, { setCookie: n.issuer.clear() });
901
+ let d = yield* n.issuer.reissue(c, {
902
+ sessionId: o.actorSessionId,
903
+ ttlSeconds: u
904
+ });
905
+ return J(200, {
906
+ ok: !0,
907
+ restored: !0,
908
+ subject: d.subject
909
+ }, { setCookie: d.setCookie });
910
+ }), jt = (e) => {
911
+ for (let t of e) if (Mt(t)) return t.auth.sessionStrategy;
912
+ return null;
913
+ }, Mt = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", Nt = "/auth", Pt = (e) => {
914
+ let t = Buffer.from(e.rawBody).toString("utf8");
915
+ if (!t) return {};
916
+ if ((e.headers["content-type"] ?? "").includes("application/x-www-form-urlencoded")) {
917
+ let e = new URLSearchParams(t), n = {};
918
+ for (let [t, r] of e) n[t] = r;
919
+ return n;
920
+ }
921
+ try {
922
+ let e = JSON.parse(t);
923
+ return typeof e == "object" && e ? e : {};
924
+ } catch {
925
+ return {};
926
+ }
927
+ }, X = (e) => ({
928
+ status: e.status,
929
+ ...e.body === void 0 ? {} : { body: e.body },
930
+ ...e.contentType === void 0 ? {} : { contentType: e.contentType },
931
+ headers: {
932
+ ...e.setCookie === void 0 ? {} : { "set-cookie": e.setCookie },
933
+ ...e.location === void 0 ? {} : { location: e.location }
934
+ }
935
+ }), Z = (e, t) => ({
936
+ status: e,
937
+ body: JSON.stringify(t),
938
+ contentType: "application/json; charset=utf-8"
939
+ }), Q = (e) => typeof e == "string" ? e : void 0, $ = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {}, Ft = (t) => {
940
+ let n = t.prefix ?? Nt, r = t.store, s = t.challengeStore ?? at(), c = {
941
+ ...t.secret === void 0 ? {} : { secret: t.secret },
942
+ ...t.secrets === void 0 ? {} : { secrets: t.secrets },
943
+ defaultTenantId: t.defaultTenantId,
944
+ ...t.cookieDomain === void 0 ? {} : { cookieDomain: t.cookieDomain },
945
+ ...t.cookieSecure === void 0 ? {} : { cookieSecure: t.cookieSecure },
946
+ ...t.successRedirect === void 0 ? {} : { successRedirect: t.successRedirect },
947
+ ...t.sendEmail === void 0 ? {} : { sendEmail: t.sendEmail },
948
+ ...t.appBaseUrl === void 0 ? {} : { appBaseUrl: t.appBaseUrl },
949
+ subjectGuards: [...t.subjectGuards ?? [], ...k(t.emailVerification) ? [ke(t.emailVerification)] : []],
950
+ ...t.lockout === void 0 ? {} : { lockout: t.lockout },
951
+ ...t.now === void 0 ? {} : { now: t.now },
952
+ ...t.emailVerification === void 0 ? {} : { emailVerification: t.emailVerification }
953
+ }, l = {
954
+ ...t.appBaseUrl === void 0 ? {} : { appBaseUrl: t.appBaseUrl },
955
+ ...t.sendEmail === void 0 ? {} : { sendEmail: t.sendEmail },
956
+ ...t.invitations === void 0 ? {} : { invitations: t.invitations },
957
+ ...t.now === void 0 ? {} : { now: t.now }
958
+ };
959
+ t.invitations !== void 0 && ce({
960
+ source: "plugin",
961
+ table: "invitations",
962
+ timeColumn: "createdAt",
963
+ ttlMs: le(process.env.VOLTRO_INVITATIONS_TTL_HOURS, 2160),
964
+ envVar: "VOLTRO_INVITATIONS_TTL_HOURS"
965
+ }), t.impersonation !== void 0 && ce({
966
+ source: "plugin",
967
+ table: "impersonationGrants",
968
+ timeColumn: "startedAt",
969
+ ttlMs: le(process.env.VOLTRO_IMPERSONATION_GRANTS_TTL_HOURS, 24 * 365),
970
+ envVar: "VOLTRO_IMPERSONATION_GRANTS_TTL_HOURS"
971
+ });
972
+ let u = d(r, t.sessionRevocation ?? {}), p = async (e) => {
973
+ let t = P(c), n = o(e.headers.cookie, t);
974
+ if (!n) return null;
975
+ let i = f(n.subject);
976
+ if (i && !await C.runPromise(u.isLive(i))) return null;
977
+ if (n.renew || n.kid !== t.current.kid) {
978
+ let e = Math.max(n.exp - n.iat, 60), o = a(n.subject, t, {
979
+ ttlSeconds: e,
980
+ ...c.cookieDomain ? { domain: c.cookieDomain } : {},
981
+ secure: c.cookieSecure ?? !0
982
+ });
983
+ return i && await C.runPromise(r.touchSession(i, new Date(Date.now() + e * 1e3))), {
984
+ subject: n.subject,
985
+ sessionId: i,
986
+ renewSetCookie: o.setCookie
987
+ };
988
+ }
989
+ return {
990
+ subject: n.subject,
991
+ sessionId: i
992
+ };
993
+ }, h = (e, t) => {
994
+ if (!t) return e;
995
+ let n = e.headers ?? {};
996
+ return n["set-cookie"] ? e : {
997
+ ...e,
998
+ headers: {
999
+ ...n,
1000
+ "set-cookie": t
1001
+ }
1002
+ };
1003
+ }, g = (e) => {
1004
+ let n = e.headers[y];
1005
+ return ee(n, se(e.headers.cookie, v), Ie(t));
1006
+ }, _ = (e, t) => e.path === `${n}${t}`, b = (e) => C.runPromise(e).then(X), x = {
1007
+ issue: (e, t) => L(e, r, c, {
1008
+ sessionId: t.sessionId,
1009
+ ttlSeconds: t.ttlSeconds,
1010
+ metadata: t.metadata,
1011
+ ipAddress: t.ipAddress ?? null,
1012
+ userAgent: t.userAgent ?? null,
1013
+ clearLoginFailures: !1
1014
+ }),
1015
+ reissue: (e, t) => L(e, r, c, {
1016
+ sessionId: t.sessionId,
1017
+ ttlSeconds: t.ttlSeconds,
1018
+ sessionRow: "existing",
1019
+ clearLoginFailures: !1
1020
+ }),
1021
+ clear: () => i({
1022
+ ...c.cookieDomain ? { domain: c.cookieDomain } : {},
1023
+ secure: c.cookieSecure ?? !0
1024
+ })
1025
+ }, te = {
1026
+ method: "*",
1027
+ path: n,
1028
+ handle: async (i) => {
1029
+ let a = i.method === "GET" ? {} : Pt(i);
1030
+ if (i.method === "GET" && _(i, "/csrf")) return X(qe(c));
1031
+ if (i.method === "POST" && _(i, "/sign-in")) return b(ze({
1032
+ email: Q(a.email) ?? "",
1033
+ password: Q(a.password) ?? "",
1034
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter },
1035
+ ...i.remoteAddr ? { ipAddress: i.remoteAddr } : {},
1036
+ ...i.headers["user-agent"] ? { userAgent: i.headers["user-agent"] } : {}
1037
+ }, r, c));
1038
+ if (i.method === "POST" && _(i, "/sign-up")) return b(Ve({
1039
+ email: Q(a.email) ?? "",
1040
+ password: Q(a.password) ?? "",
1041
+ ...$("tenantId", a.tenantId),
1042
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter }
1043
+ }, r, c));
1044
+ if (i.method === "POST" && _(i, "/sign-out")) {
1045
+ let e = await p(i);
1046
+ return e && e.subject.type === "user" && e.sessionId && (await C.runPromise(r.revokeSession(e.subject.id, e.sessionId)), u.invalidate(e.sessionId)), X(He(c));
1047
+ }
1048
+ if (i.method === "POST" && _(i, "/magic-link")) return b(Ye({
1049
+ email: Q(a.email) ?? "",
1050
+ ...$("callbackPath", a.callbackPath)
1051
+ }, r, c));
1052
+ if (i.method === "POST" && _(i, "/magic-link/callback")) return b(Xe({
1053
+ token: Q(a.token) ?? "",
1054
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter }
1055
+ }, r, c));
1056
+ if (i.method === "POST" && _(i, "/password-reset")) return b(Ze({
1057
+ email: Q(a.email) ?? "",
1058
+ ...$("callbackPath", a.callbackPath)
1059
+ }, r, c));
1060
+ if (i.method === "POST" && _(i, "/password-reset/confirm")) return b(Qe({
1061
+ token: Q(a.token) ?? "",
1062
+ newPassword: Q(a.newPassword) ?? ""
1063
+ }, r));
1064
+ if (i.method === "POST" && _(i, "/verify-email")) {
1065
+ let e = Q(a.email);
1066
+ if (e) return b(N({ email: e }, r, c));
1067
+ let t = await p(i);
1068
+ return !t || t.subject.type !== "user" ? Z(400, { error: "email required" }) : b(N({ userId: t.subject.id }, r, c));
1069
+ }
1070
+ if (i.method === "POST" && _(i, "/verify-email/callback")) return b(je({ token: Q(a.token) ?? "" }, r));
1071
+ if (t.invitations && i.method === "POST" && _(i, "/invitations/preview")) return b(St({ token: Q(a.token) ?? "" }, r, l));
1072
+ if (t.invitations && i.method === "POST" && _(i, "/invitations/sign-up")) return b(wt({
1073
+ token: Q(a.token) ?? "",
1074
+ password: Q(a.password) ?? ""
1075
+ }, r, {
1076
+ ...l,
1077
+ hashPassword: e
1078
+ }));
1079
+ if (i.method === "POST" && _(i, "/mfa/verify")) return b(Be({
1080
+ pendingToken: Q(a.pendingToken) ?? "",
1081
+ ...$("code", a.code),
1082
+ ...$("recoveryCode", a.recoveryCode),
1083
+ ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter },
1084
+ ...i.remoteAddr ? { ipAddress: i.remoteAddr } : {},
1085
+ ...i.headers["user-agent"] ? { userAgent: i.headers["user-agent"] } : {}
1086
+ }, r, c));
1087
+ if (t.passkey && i.method === "POST" && _(i, "/passkey/assert/options")) return b(lt({ ...$("userId", a.userId) }, r, s, t.passkey));
1088
+ if (t.passkey && i.method === "POST" && _(i, "/passkey/assert/verify")) return b(ut({
1089
+ credentialId: Q(a.credentialId) ?? "",
1090
+ clientDataJSON: Q(a.clientDataJSON) ?? "",
1091
+ authenticatorData: Q(a.authenticatorData) ?? "",
1092
+ signature: Q(a.signature) ?? "",
1093
+ ...$("userId", a.userId)
1094
+ }, r, s, t.passkey, c));
1095
+ let o = await p(i), d = o?.subject ?? null, f = () => !d || d.type !== "user" ? Z(401, { error: "unauthenticated" }) : { userId: d.id }, m = () => g(i) ? null : Z(403, { error: "csrf" }), v = (e) => b(e).then((e) => h(e, o?.renewSetCookie));
1096
+ if (d && q(d)) {
1097
+ let e = i.path.startsWith(n) ? i.path.slice(n.length) : "";
1098
+ if (Et.includes(e)) return Z(403, {
1099
+ error: "forbidden_while_impersonating",
1100
+ message: "This action changes account credentials or session scope and is refused while impersonating. Stop the impersonation first."
1101
+ });
1102
+ }
1103
+ if (i.method === "GET" && _(i, "/session")) {
1104
+ let e = f();
1105
+ return "status" in e ? e : h(Z(200, {
1106
+ ok: !0,
1107
+ subject: d,
1108
+ renewed: o?.renewSetCookie !== void 0
1109
+ }), o?.renewSetCookie);
1110
+ }
1111
+ if (i.method === "GET" && _(i, "/sessions")) {
1112
+ let e = f();
1113
+ return "status" in e ? e : v($e({ userId: e.userId }, r));
1114
+ }
1115
+ if (i.method === "GET" && _(i, "/memberships")) {
1116
+ let e = f();
1117
+ return "status" in e ? e : v(rt({ userId: e.userId }, r));
1118
+ }
1119
+ if (i.method === "POST" && _(i, "/sessions/revoke")) {
1120
+ let e = f();
1121
+ if ("status" in e) return e;
1122
+ let t = m();
1123
+ if (t) return t;
1124
+ let n = Q(a.sessionId) ?? "";
1125
+ return v(et({
1126
+ userId: e.userId,
1127
+ sessionId: n
1128
+ }, r)).then((e) => (n && u.invalidate(n), e));
1129
+ }
1130
+ if (i.method === "POST" && _(i, "/sessions/revoke-others")) {
1131
+ let e = f();
1132
+ return "status" in e ? e : m() || v(tt({
1133
+ userId: e.userId,
1134
+ keepSessionId: o?.sessionId ?? "__none__"
1135
+ }, r));
1136
+ }
1137
+ if (t.invitations && i.method === "POST" && _(i, "/invitations")) {
1138
+ let e = f();
1139
+ return "status" in e ? e : m() || v(yt({
1140
+ inviterUserId: e.userId,
1141
+ tenantId: Q(a.tenantId) ?? d?.tenantId ?? "",
1142
+ email: Q(a.email) ?? "",
1143
+ role: Q(a.role) ?? ""
1144
+ }, r, l));
1145
+ }
1146
+ if (t.invitations && i.method === "GET" && i.path === `${n}/invitations`) {
1147
+ let e = f();
1148
+ return "status" in e ? e : v(bt({
1149
+ callerUserId: e.userId,
1150
+ tenantId: d?.tenantId ?? ""
1151
+ }, r, l));
1152
+ }
1153
+ if (t.invitations && i.method === "POST" && _(i, "/invitations/revoke")) {
1154
+ let e = f();
1155
+ return "status" in e ? e : m() || v(xt({
1156
+ callerUserId: e.userId,
1157
+ tenantId: Q(a.tenantId) ?? d?.tenantId ?? "",
1158
+ invitationId: Q(a.invitationId) ?? ""
1159
+ }, r, l));
1160
+ }
1161
+ if (t.invitations && i.method === "POST" && _(i, "/invitations/accept")) {
1162
+ let e = f();
1163
+ if (!("status" in e)) {
1164
+ let e = m();
1165
+ if (e) return e;
1166
+ }
1167
+ return v(Ct({
1168
+ token: Q(a.token) ?? "",
1169
+ ..."status" in e ? {} : { userId: e.userId }
1170
+ }, r, l));
1171
+ }
1172
+ if (t.impersonation && i.method === "POST" && _(i, "/impersonate/start")) {
1173
+ let e = f();
1174
+ return "status" in e ? e : m() || b(kt({
1175
+ actorSubject: d,
1176
+ actorSessionId: o?.sessionId ?? "",
1177
+ ...$("targetUserId", a.userId),
1178
+ ...$("targetEmail", a.email),
1179
+ ...$("reason", a.reason),
1180
+ ...typeof a.durationSeconds == "number" ? { durationSeconds: a.durationSeconds } : {},
1181
+ ipAddress: i.remoteAddr ?? null,
1182
+ userAgent: i.headers["user-agent"] ?? null
1183
+ }, r, {
1184
+ impersonation: t.impersonation,
1185
+ issuer: x,
1186
+ ...t.now === void 0 ? {} : { now: t.now },
1187
+ invalidateSession: (e) => u.invalidate(e)
1188
+ }));
1189
+ }
1190
+ if (t.impersonation && i.method === "POST" && _(i, "/impersonate/stop")) {
1191
+ let e = f();
1192
+ return "status" in e ? e : m() || b(At({
1193
+ subject: d,
1194
+ sessionId: o?.sessionId ?? ""
1195
+ }, r, {
1196
+ impersonation: t.impersonation,
1197
+ issuer: x,
1198
+ ...t.now === void 0 ? {} : { now: t.now },
1199
+ invalidateSession: (e) => u.invalidate(e)
1200
+ }));
1201
+ }
1202
+ if (i.method === "POST" && _(i, "/switch-tenant")) {
1203
+ let e = f();
1204
+ return "status" in e ? e : m() || b(nt({
1205
+ userId: e.userId,
1206
+ targetTenantId: Q(a.targetTenantId) ?? "",
1207
+ ...typeof a.clientId == "number" ? { clientId: a.clientId } : {},
1208
+ ...o?.sessionId ? { sessionId: o.sessionId } : {},
1209
+ ...d?.type === "user" && d.metadata ? { metadata: d.metadata } : {}
1210
+ }, r, c, t.rebind));
1211
+ }
1212
+ if (t.mfa && i.method === "POST" && _(i, "/mfa/enroll/start")) {
1213
+ let e = f();
1214
+ if ("status" in e) return e;
1215
+ let n = m();
1216
+ if (n) return n;
1217
+ let i = await C.runPromise(r.findById(e.userId));
1218
+ return v(Ue({
1219
+ userId: e.userId,
1220
+ issuer: t.mfa.issuer,
1221
+ accountName: i?.email ?? e.userId
1222
+ }, r));
1223
+ }
1224
+ if (t.mfa && i.method === "POST" && _(i, "/mfa/enroll/verify")) {
1225
+ let e = f();
1226
+ return "status" in e ? e : m() || v(We({
1227
+ userId: e.userId,
1228
+ code: Q(a.code) ?? ""
1229
+ }, r));
1230
+ }
1231
+ if (t.mfa && i.method === "POST" && _(i, "/mfa/unenroll")) {
1232
+ let e = f();
1233
+ return "status" in e ? e : m() || v(Ke({ userId: e.userId }, r));
1234
+ }
1235
+ if (t.mfa && i.method === "POST" && _(i, "/mfa/recovery-codes/regenerate")) {
1236
+ let e = f();
1237
+ return "status" in e ? e : m() || v(Ge({ userId: e.userId }, r));
1238
+ }
1239
+ if (t.passkey && i.method === "POST" && _(i, "/passkey/register/options")) {
1240
+ let e = f();
1241
+ return "status" in e ? e : m() || v(st({
1242
+ userId: e.userId,
1243
+ userName: Q(a.userName) ?? e.userId
1244
+ }, r, s, t.passkey));
1245
+ }
1246
+ if (t.passkey && i.method === "POST" && _(i, "/passkey/register/verify")) {
1247
+ let e = f();
1248
+ return "status" in e ? e : m() || v(ct({
1249
+ userId: e.userId,
1250
+ credentialId: Q(a.credentialId) ?? "",
1251
+ clientDataJSON: Q(a.clientDataJSON) ?? "",
1252
+ authenticatorData: Q(a.authenticatorData) ?? "",
1253
+ ...$("transports", a.transports)
1254
+ }, r, s, t.passkey));
1255
+ }
1256
+ return !t.passkey && i.path.startsWith(`${n}/passkey/`) ? Z(501, { error: "passkey_not_configured" }) : Z(404, { error: "not_found" });
1257
+ }
1258
+ }, ne = ue({
1259
+ name: t.name ? `@voltro/plugin-auth#${t.name}` : "@voltro/plugin-auth",
1260
+ 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.",
1261
+ permissions: [],
1262
+ declaredEnv: It,
1263
+ httpRoutes: [te],
1264
+ onActivate: (e) => C.sync(() => {
1265
+ e.logger.info("auth routes active", {
1266
+ prefix: n,
1267
+ passkey: t.passkey ? t.passkey.rpId : "disabled",
1268
+ email: t.sendEmail ? "wired" : "unwired",
1269
+ revocationWindowMs: u.ttlMs,
1270
+ emailVerification: t.emailVerification?.policy ?? "off",
1271
+ invitations: t.invitations ? "enabled" : "disabled",
1272
+ impersonation: t.impersonation ? `enabled(max ${t.impersonation.maxDurationSeconds ?? 900}s)` : "disabled"
1273
+ });
1274
+ }),
1275
+ onDeactivate: () => C.void
1276
+ }), S = t.secrets ?? t.secret;
1277
+ return {
1278
+ ...ne,
1279
+ auth: { sessionStrategy: m({
1280
+ ...S === void 0 ? {} : { secret: S },
1281
+ store: r,
1282
+ revocation: u
1283
+ }) }
1284
+ };
1285
+ }, It = [
1286
+ {
1287
+ name: "VOLTRO_SESSION_SECRET_PREVIOUS",
1288
+ required: !1,
1289
+ secret: !0,
1290
+ description: "The session secret being rotated AWAY from. While set, cookies signed with it still verify (and get re-issued under the current secret on the next authenticated /auth response). Set the new secret as VOLTRO_SESSION_SECRET, move the old one here for one session lifetime, then drop it.",
1291
+ example: "the-previous-signing-secret"
1292
+ },
1293
+ {
1294
+ name: "VOLTRO_SESSION_KID",
1295
+ required: !1,
1296
+ secret: !1,
1297
+ description: "Non-secret key-id label stamped into cookies signed with the current secret (default 'k0').",
1298
+ example: "2026-07"
1299
+ },
1300
+ {
1301
+ name: "VOLTRO_SESSION_KID_PREVIOUS",
1302
+ required: !1,
1303
+ secret: !1,
1304
+ description: "Non-secret key-id label for the previous secret (default 'k-previous').",
1305
+ example: "2026-01"
1306
+ },
1307
+ {
1308
+ name: "VOLTRO_INVITATIONS_TTL_HOURS",
1309
+ required: !1,
1310
+ secret: !1,
1311
+ description: "How long an `invitations` row is kept before the retention sweep DELETES it. Default 2160 (90 days). Only armed when `invitations` is configured. Every invitation is dead long before this — the lifetime that decides redeemability is `invitations.ttlSeconds` (7 days by default) — so this bounds the history, not the grant.",
1312
+ example: "2160"
1313
+ },
1314
+ {
1315
+ name: "VOLTRO_IMPERSONATION_GRANTS_TTL_HOURS",
1316
+ required: !1,
1317
+ secret: !1,
1318
+ description: "How long an `impersonationGrants` row is kept before the retention sweep DELETES it. Default 8760 (365 days), matching the audit log — this table answers \"who was acting as whom, when\", which is compliance evidence rather than operational state. Only armed when `impersonation` is configured. Raise it to your audit-retention obligation; lowering it shortens how far back an impersonation can be traced.",
1319
+ example: "8760"
1320
+ },
1321
+ {
1322
+ name: "VOLTRO_AUTH_SCOPE_CACHE_TTL_MS",
1323
+ required: !1,
1324
+ secret: !1,
1325
+ description: "How long a resolved authority (auth.resolveScopes) stays cached, in milliseconds. Default 30000 — the same window as the session-revocation check, so the two per-request store reads expire together. This is the staleness window for a role change: remove a permission and every live session observes it within this long. Set it to 0 to resolve on every request (staleness zero, one store read per request). An explicit ttlMs in code wins over this variable.",
1326
+ example: "30000"
1327
+ }
1328
+ ];
1329
+ //#endregion
1330
+ export { ke as $, qe as A, Qe as B, lt as C, at as D, ct as E, Ue as F, He as G, tt as H, We as I, L as J, Ve as K, Ge as L, $e as M, Xe as N, P as O, Ye as P, k as Q, Ke as R, ot as S, st as T, et as U, Ze as V, ze as W, Ee as X, Te as Y, we as Z, wt as _, G as a, E as at, St as b, Ot as c, be as ct, pt as d, fe as dt, je as et, V as f, Ct as g, ht as h, Et as i, A as it, rt as j, R as k, K as l, xe as lt, dt as m, jt as n, Oe as nt, kt as o, Ce as ot, ft as p, nt as q, Tt as r, M as rt, At as s, Se as st, Ft as t, N as tt, q as u, O as ut, yt as v, ut as w, xt as x, bt as y, Be as z };