@voltro/plugin-auth 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +5987 -0
  2. package/README.md +93 -0
  3. package/THIRD-PARTY-NOTICES.md +1122 -31
  4. package/dist/admission.js +9 -0
  5. package/dist/appUserStore.js +19 -0
  6. package/dist/authExtensions.js +9 -0
  7. package/dist/emailVerification.js +64 -0
  8. package/dist/guest.d.ts +99 -0
  9. package/dist/guest.js +3 -0
  10. package/dist/guestClient.js +237 -0
  11. package/dist/guestCredential.js +12 -0
  12. package/dist/guestPasswordUpgrade.js +85 -0
  13. package/dist/guestPlugin.js +166 -0
  14. package/dist/guestSchema.js +20 -0
  15. package/dist/guestSecrets.js +10 -0
  16. package/dist/guestService.js +224 -0
  17. package/dist/handlers.js +337 -0
  18. package/dist/impersonation.js +176 -0
  19. package/dist/index.d.ts +282 -20
  20. package/dist/index.js +30 -88
  21. package/dist/invitations.js +187 -0
  22. package/dist/mail.js +10 -0
  23. package/dist/memberships.js +8 -0
  24. package/dist/native/guestStorage.js +41 -0
  25. package/dist/native.d.ts +110 -0
  26. package/dist/native.js +45 -43
  27. package/dist/nativeRoutes.js +109 -0
  28. package/dist/overlayPlugin.js +167 -0
  29. package/dist/passkeyHandlers.js +162 -0
  30. package/dist/plugin.d.ts +123 -10
  31. package/dist/plugin.js +519 -5
  32. package/dist/postgresUserStore.js +475 -0
  33. package/dist/revocation.js +50 -0
  34. package/dist/schema.d.ts +45 -6
  35. package/dist/schema.js +99 -96
  36. package/dist/sessionFamily.js +15 -0
  37. package/dist/sessionRegistry.js +133 -0
  38. package/dist/signedClaims.js +24 -0
  39. package/dist/strategy.d.ts +17 -9
  40. package/dist/strategy.js +69 -3
  41. package/dist/subjectGuard.js +11 -0
  42. package/dist/totp.js +55 -0
  43. package/dist/userStore.js +292 -0
  44. package/dist/web/guestStorage.js +47 -0
  45. package/dist/web/passkeyClient.js +78 -0
  46. package/dist/web/requireAuth.js +9 -0
  47. package/dist/web/subjectContext.js +13 -0
  48. package/dist/web.d.ts +94 -0
  49. package/dist/web.js +6 -90
  50. package/package.json +8 -3
  51. package/dist/plugin-CbmcyEJs.js +0 -2018
  52. package/dist/strategy-XaGjSicX.js +0 -353
@@ -0,0 +1,337 @@
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, resolveSessionSecret as o, resolveSessionSecrets as s, sessionSecretsOf as c } from "./session.js";
3
+ import { newAuthId as l, subjectFromUser as u } from "./userStore.js";
4
+ import { CSRF_COOKIE_NAME as d, issueCsrfToken as f } from "./csrf.js";
5
+ import { decideAdmission as p } from "./admission.js";
6
+ import { generateRecoveryCodes as m, generateTotpSecret as h, otpauthUrl as g, verifyTotpCode as _ } from "./totp.js";
7
+ import { MFA_PENDING_TTL_S as v, hashToken as y, mintToken as b } from "./tokens.js";
8
+ import { runSubjectGuards as x } from "./subjectGuard.js";
9
+ import { EMAIL_VERIFIED_METADATA_KEY as S, emailVerificationActive as C, sendEmailVerification as w, userEmailVerified as T } from "./emailVerification.js";
10
+ import { Effect as E } from "effect";
11
+ import { buildSetCookie as D } from "@voltro/protocol/session";
12
+ import { writeTenantSelection as O } from "@voltro/database";
13
+ import { selectSubjectTenant as k } from "@voltro/protocol";
14
+ //#region src/handlers.ts
15
+ var A = 5, j = 900, M = 900, N = (e) => e.email ?? `user:${e.id}`, P = (e) => ({
16
+ maxAttempts: e.lockout?.maxAttempts ?? A,
17
+ windowMs: (e.lockout?.windowSeconds ?? j) * 1e3,
18
+ lockMs: (e.lockout?.lockSeconds ?? M) * 1e3
19
+ }), F = (e) => e.secrets ?? (e.secret === void 0 ? s() : c(e.secret)), I = (e) => e.secret ?? o(), L = (e, t, n = {}) => ({
20
+ status: e,
21
+ body: JSON.stringify(t),
22
+ contentType: "application/json; charset=utf-8",
23
+ ...n
24
+ }), R = (e, t = {}) => ({
25
+ status: 302,
26
+ body: "",
27
+ contentType: "text/plain",
28
+ location: e,
29
+ ...t
30
+ }), z = 604800, B = (e, t) => e.type === "user" ? {
31
+ ...e,
32
+ metadata: {
33
+ ...e.metadata ?? {},
34
+ sessionId: t
35
+ }
36
+ } : e, V = (e, t, n, r = {}) => E.gen(function* () {
37
+ r.clearLoginFailures !== !1 && (yield* t.clearLoginFailures(N(e))), n.ensureActor && (yield* n.ensureActor(e));
38
+ let i = yield* t.listMemberships(e.id), o = r.sessionId ?? l("sess"), s = r.ttlSeconds ?? z;
39
+ r.sessionRow !== "existing" && (yield* t.insertSession({
40
+ id: o,
41
+ userId: e.id,
42
+ tenantId: e.tenantId,
43
+ expiresAt: new Date(Date.now() + s * 1e3),
44
+ ipAddress: r.ipAddress ?? null,
45
+ userAgent: r.userAgent ?? null
46
+ }).pipe(E.ignore));
47
+ let c = { ...r.metadata };
48
+ C(n.emailVerification) && (c[S] = T(e, n.emailVerification));
49
+ let d = B(u(e, {
50
+ memberships: i,
51
+ ...Object.keys(c).length > 0 ? { metadata: c } : {}
52
+ }), o), f = a(d, F(n), {
53
+ ttlSeconds: s,
54
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
55
+ secure: n.cookieSecure ?? !0
56
+ });
57
+ return {
58
+ setCookie: f.setCookie,
59
+ value: f.value,
60
+ expiresAt: Math.floor(Date.now() / 1e3) + s,
61
+ subject: d
62
+ };
63
+ }), H = (e, t) => E.gen(function* () {
64
+ let n = t.subjectGuards ?? [];
65
+ if (n.length === 0) return null;
66
+ let r = yield* x(n, e);
67
+ return r.ok ? null : L(403, {
68
+ error: r.code,
69
+ message: r.message
70
+ });
71
+ }), U = (r, i, a) => E.gen(function* () {
72
+ if (!r.email || !r.password) return L(400, { error: "email and password required" });
73
+ let o = yield* i.findByEmail(r.email), s = (a.now ?? Date.now)(), c = yield* i.isLockedOut(r.email, s);
74
+ if (c.locked) return L(429, {
75
+ error: "account_locked",
76
+ retryAfterSeconds: c.retryAfterSeconds
77
+ });
78
+ let u = o?.passwordHash, d = typeof u == "string" && u.length > 0 ? yield* n(r.password, u) : yield* E.gen(function* () {
79
+ let n = yield* e("decoy-not-a-real-password").pipe(E.orElseSucceed(() => ""));
80
+ return yield* t(r.password, n), { valid: !1 };
81
+ });
82
+ if (!o || !d.valid) return yield* i.recordLoginFailure(r.email, s, P(a)), L(401, { error: "invalid credentials" });
83
+ let f = yield* H(o, a);
84
+ if (f) return f;
85
+ if (d.rehash && (yield* i.updatePassword(o.id, d.rehash).pipe(E.ignore)), o.mfaEnrolledAt) {
86
+ let e = b("mfa-pending", 300);
87
+ return yield* i.insertToken({
88
+ id: l("tok"),
89
+ tokenHash: e.tokenHash,
90
+ userId: o.id,
91
+ purpose: "mfa-pending",
92
+ expiresAt: e.expiresAt
93
+ }).pipe(E.ignore), L(200, {
94
+ ok: !0,
95
+ mfaRequired: !0,
96
+ pendingToken: e.token,
97
+ expiresInSeconds: 300
98
+ });
99
+ }
100
+ let { setCookie: p, subject: m } = yield* V(o, i, a, {
101
+ ipAddress: r.ipAddress ?? null,
102
+ userAgent: r.userAgent ?? null
103
+ });
104
+ return r.redirectAfter ? R(a.successRedirect ?? "/", { setCookie: p }) : L(200, {
105
+ ok: !0,
106
+ subject: m
107
+ }, { setCookie: p });
108
+ }), W = (e, t, n) => E.gen(function* () {
109
+ if (!e.pendingToken) return L(400, { error: "pending token required" });
110
+ if (!e.code && !e.recoveryCode) return L(400, { error: "code or recoveryCode required" });
111
+ let r = yield* t.consumeToken(y(e.pendingToken), "mfa-pending");
112
+ if (!r) return L(401, { error: "invalid or expired challenge" });
113
+ let i = yield* t.findById(r.userId);
114
+ if (!i || !i.mfaEnrolledAt || !i.mfaSecret) return L(401, { error: "mfa not enrolled" });
115
+ let a = (n.now ?? Date.now)(), o = yield* t.isLockedOut(N(i), a);
116
+ if (o.locked) return L(429, {
117
+ error: "account_locked",
118
+ retryAfterSeconds: o.retryAfterSeconds
119
+ });
120
+ let s = yield* H(i, n);
121
+ if (s) return s;
122
+ if (!(e.recoveryCode ? yield* t.consumeRecoveryCode(i.id, y(e.recoveryCode.trim())) : yield* _(i.mfaSecret, e.code ?? "").pipe(E.map(() => !0), E.catchTag("TotpVerifyError", () => E.succeed(!1))))) return yield* t.recordLoginFailure(N(i), a, P(n)), L(401, { error: "invalid_code" });
123
+ let { setCookie: c, subject: l } = yield* V(i, t, n, {
124
+ ipAddress: e.ipAddress ?? null,
125
+ userAgent: e.userAgent ?? null
126
+ });
127
+ return e.redirectAfter ? R(n.successRedirect ?? "/", { setCookie: c }) : L(200, {
128
+ ok: !0,
129
+ subject: l
130
+ }, { setCookie: c });
131
+ }), G = (t, n, r) => E.gen(function* () {
132
+ if (!t.email || !t.password) return L(400, { error: "email and password required" });
133
+ if (t.password.length < 8) return L(400, { error: "password must be at least 8 characters" });
134
+ if (yield* n.findByEmail(t.email)) return L(409, { error: "email already registered" });
135
+ let i = t.tenantId ?? r.defaultTenantId, a = yield* p(r.admit, {
136
+ email: t.email,
137
+ tenantId: i,
138
+ via: "sign-up"
139
+ });
140
+ if (a) return L(403, {
141
+ error: "not_admitted",
142
+ reason: a.reason,
143
+ message: a.message
144
+ });
145
+ let o = yield* e(t.password), s = yield* n.insert({
146
+ id: l("user"),
147
+ email: t.email,
148
+ passwordHash: o,
149
+ tenantId: i
150
+ });
151
+ yield* n.addMembership({
152
+ userId: s.id,
153
+ tenantId: i,
154
+ role: "owner"
155
+ }).pipe(E.ignore), C(r.emailVerification) && (r.emailVerification?.sendOnSignUp ?? !0) && (yield* w(s, n, r).pipe(E.ignore));
156
+ let { setCookie: c, subject: u } = yield* V(s, n, r);
157
+ return t.redirectAfter ? R(r.successRedirect ?? "/", { setCookie: c }) : L(200, {
158
+ ok: !0,
159
+ subject: u
160
+ }, { setCookie: c });
161
+ }).pipe(E.catch((e) => E.succeed(L(500, {
162
+ error: "signup_failed",
163
+ cause: String(e)
164
+ })))), K = (e) => ({
165
+ status: 302,
166
+ body: "",
167
+ location: e.successRedirect ?? "/",
168
+ setCookie: i({
169
+ ...e.cookieDomain ? { domain: e.cookieDomain } : {},
170
+ secure: e.cookieSecure ?? !0
171
+ })
172
+ }), q = (e, t) => E.gen(function* () {
173
+ let n = yield* t.findById(e.userId);
174
+ if (!n) return L(404, { error: "user_not_found" });
175
+ let r = h();
176
+ return yield* t.setMfaSecret(n.id, r).pipe(E.catch(() => E.succeed(n))), L(200, {
177
+ ok: !0,
178
+ secret: r,
179
+ otpauthUrl: g({
180
+ issuer: e.issuer,
181
+ accountName: e.accountName,
182
+ secret: r
183
+ })
184
+ });
185
+ }), J = (e, t) => E.gen(function* () {
186
+ let n = yield* t.findById(e.userId);
187
+ if (!n) return L(404, { error: "user_not_found" });
188
+ if (!n.mfaSecret) return L(400, { error: "no_enrollment_in_progress" });
189
+ if (n.mfaEnrolledAt) return L(409, { error: "already_enrolled" });
190
+ if (!(yield* _(n.mfaSecret, e.code).pipe(E.map(() => !0), E.catchTag("TotpVerifyError", () => E.succeed(!1))))) return L(401, { error: "invalid_code" });
191
+ yield* t.markMfaEnrolled(n.id).pipe(E.catch(() => E.succeed(n)));
192
+ let r = m();
193
+ return yield* t.replaceRecoveryCodes(n.id, r.map((e) => y(e))).pipe(E.ignore), L(200, {
194
+ ok: !0,
195
+ enrolled: !0,
196
+ recoveryCodes: r
197
+ });
198
+ }), Y = (e, t) => E.gen(function* () {
199
+ let n = yield* t.findById(e.userId);
200
+ if (!n) return L(404, { error: "user_not_found" });
201
+ if (!n.mfaEnrolledAt) return L(400, { error: "mfa_not_enrolled" });
202
+ let r = m();
203
+ return yield* t.replaceRecoveryCodes(n.id, r.map((e) => y(e))).pipe(E.ignore), L(200, {
204
+ ok: !0,
205
+ recoveryCodes: r
206
+ });
207
+ }), X = (e, t) => E.gen(function* () {
208
+ let n = yield* t.findById(e.userId);
209
+ return n ? (yield* t.clearMfa(n.id).pipe(E.catch(() => E.succeed(n))), yield* t.replaceRecoveryCodes(n.id, []).pipe(E.ignore), L(200, {
210
+ ok: !0,
211
+ enrolled: !1
212
+ })) : L(404, { error: "user_not_found" });
213
+ }), Z = (e) => {
214
+ let t = f(I(e)), n = D(d, t, {
215
+ httpOnly: !1,
216
+ sameSite: "lax",
217
+ secure: e.cookieSecure ?? !0,
218
+ ...e.cookieDomain ? { domain: e.cookieDomain } : {},
219
+ path: "/"
220
+ });
221
+ return L(200, { csrfToken: t }, { setCookie: n });
222
+ }, Q = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, $ = (e, t, n) => E.gen(function* () {
223
+ if (!e.email) return L(400, { error: "email required" });
224
+ let r = yield* t.findByEmail(e.email);
225
+ if (r) {
226
+ let i = b("magic-link");
227
+ yield* t.insertToken({
228
+ id: l("tok"),
229
+ tokenHash: i.tokenHash,
230
+ userId: r.id,
231
+ purpose: "magic-link",
232
+ expiresAt: i.expiresAt
233
+ }).pipe(E.ignore);
234
+ let a = Q(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/magic-link/callback", i.token);
235
+ n.sendEmail && (yield* E.tryPromise(() => n.sendEmail({
236
+ to: r.email ?? e.email,
237
+ kind: "magic-link",
238
+ actionUrl: a,
239
+ subject: "Your sign-in link",
240
+ text: `Sign in: ${a}`,
241
+ html: `<p><a href="${a}">Sign in</a> — this link expires shortly.</p>`
242
+ })).pipe(E.ignore));
243
+ }
244
+ return L(202, { ok: !0 });
245
+ }), ee = (e, t, n) => E.gen(function* () {
246
+ if (!e.token) return L(400, { error: "token required" });
247
+ let r = yield* t.consumeToken(y(e.token), "magic-link");
248
+ if (!r) return L(401, { error: "invalid or expired token" });
249
+ let i = yield* t.findById(r.userId);
250
+ if (!i) return L(401, { error: "invalid or expired token" });
251
+ let a = yield* t.markEmailVerified(i.id).pipe(E.orElseSucceed(() => i)), o = yield* H(a, n);
252
+ if (o) return o;
253
+ let { setCookie: s, subject: c } = yield* V(a, t, n);
254
+ return e.redirectAfter ? R(n.successRedirect ?? "/", { setCookie: s }) : L(200, {
255
+ ok: !0,
256
+ subject: c
257
+ }, { setCookie: s });
258
+ }), te = (e, t, n) => E.gen(function* () {
259
+ if (!e.email) return L(400, { error: "email required" });
260
+ let r = yield* t.findByEmail(e.email);
261
+ if (r) {
262
+ let i = b("password-reset");
263
+ yield* t.insertToken({
264
+ id: l("tok"),
265
+ tokenHash: i.tokenHash,
266
+ userId: r.id,
267
+ purpose: "password-reset",
268
+ expiresAt: i.expiresAt
269
+ }).pipe(E.ignore);
270
+ let a = Q(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/password-reset/callback", i.token);
271
+ n.sendEmail && (yield* E.tryPromise(() => n.sendEmail({
272
+ to: r.email ?? e.email,
273
+ kind: "password-reset",
274
+ actionUrl: a,
275
+ subject: "Reset your password",
276
+ text: `Reset your password: ${a}`,
277
+ html: `<p><a href="${a}">Reset your password</a> — this link expires shortly.</p>`
278
+ })).pipe(E.ignore));
279
+ }
280
+ return L(202, { ok: !0 });
281
+ }), ne = (t, n) => E.gen(function* () {
282
+ if (!t.token || !t.newPassword) return L(400, { error: "token and newPassword required" });
283
+ if (t.newPassword.length < 8) return L(400, { error: "password must be at least 8 characters" });
284
+ let r = yield* n.consumeToken(y(t.token), "password-reset");
285
+ if (!r) return L(401, { error: "invalid or expired token" });
286
+ let i = yield* e(t.newPassword).pipe(E.orElseSucceed(() => ""));
287
+ return i ? (yield* n.updatePassword(r.userId, i).pipe(E.ignore), yield* n.markEmailVerified(r.userId).pipe(E.ignore), yield* n.revokeAllSessions(r.userId).pipe(E.ignore), L(200, { ok: !0 })) : L(500, { error: "hash_failed" });
288
+ }), re = (e, t) => E.gen(function* () {
289
+ return L(200, { sessions: (yield* t.listSessions(e.userId)).map((e) => ({
290
+ id: e.id,
291
+ tenantId: e.tenantId,
292
+ ipAddress: e.ipAddress ?? null,
293
+ userAgent: e.userAgent ?? null,
294
+ createdAt: e.createdAt.toISOString(),
295
+ lastSeenAt: e.lastSeenAt.toISOString(),
296
+ expiresAt: e.expiresAt.toISOString()
297
+ })) });
298
+ }), ie = (e, t) => E.gen(function* () {
299
+ return (yield* t.revokeSession(e.userId, e.sessionId)) ? L(200, {
300
+ ok: !0,
301
+ revoked: 1
302
+ }) : L(404, { error: "session_not_found" });
303
+ }), ae = (e, t) => E.gen(function* () {
304
+ return L(200, {
305
+ ok: !0,
306
+ revoked: yield* t.revokeAllOtherSessions(e.userId, e.keepSessionId)
307
+ });
308
+ }), oe = (e, t, n, i, o) => E.gen(function* () {
309
+ let s = yield* t.findById(e.userId);
310
+ if (!s) return L(404, { error: "user_not_found" });
311
+ let c = yield* t.listMemberships(s.id), l = c.find((t) => t.tenantId === e.targetTenantId)?.role ?? null;
312
+ if (l === null) return L(403, { error: "not a member of the target tenant" });
313
+ let d = e.metadata === void 0 ? void 0 : Object.fromEntries(Object.entries(e.metadata).filter(([e]) => e !== "memberships")), f = k(u(s, { ...d === void 0 ? {} : { metadata: d } }), e.targetTenantId, c);
314
+ o !== void 0 && f.type === "user" && (yield* E.promise(() => O(o, {
315
+ type: f.type,
316
+ id: f.id
317
+ }, e.targetTenantId)));
318
+ let p = e.sessionId ? B(f, e.sessionId) : f, m = a(p, F(n), {
319
+ ttlSeconds: z,
320
+ ...n.cookieDomain ? { domain: n.cookieDomain } : {},
321
+ secure: n.cookieSecure ?? !0
322
+ });
323
+ return e.clientId !== void 0 && i && i(e.clientId, { cookies: { [r]: m.value } }), L(200, {
324
+ ok: !0,
325
+ tenantId: e.targetTenantId,
326
+ role: l,
327
+ subject: p
328
+ }, { setCookie: m.setCookie });
329
+ }), se = (e, t) => E.gen(function* () {
330
+ return L(200, { memberships: (yield* t.listMemberships(e.userId)).map((e) => ({
331
+ tenantId: e.tenantId,
332
+ role: e.role,
333
+ joinedAt: e.joinedAt.toISOString()
334
+ })) });
335
+ });
336
+ //#endregion
337
+ export { v as MFA_PENDING_TTL_S, F as configSessionSecrets, I as configSigningKey, H as guardSubject, Z as handleCsrf, se as handleListMemberships, re as handleListSessions, ee as handleMagicLinkConsume, $ as handleMagicLinkRequest, q as handleMfaEnrollStart, J as handleMfaEnrollVerify, Y as handleMfaRegenerateRecoveryCodes, X as handleMfaUnenroll, W as handleMfaVerify, ne as handlePasswordResetConfirm, te as handlePasswordResetRequest, ae as handleRevokeAllOtherSessions, ie as handleRevokeSession, U as handleSignIn, K as handleSignOut, G as handleSignUp, oe as handleSwitchTenant, V as issueUserSession };
@@ -0,0 +1,176 @@
1
+ import { isSessionCredentialLiveFor as e } from "./sessionFamily.js";
2
+ import { newAuthId as t, subjectFromUser as n } from "./userStore.js";
3
+ import { Effect as r } from "effect";
4
+ import { IMPERSONATION_METADATA_KEY as i, rawImpersonationMark as a } from "@voltro/protocol";
5
+ //#region src/impersonation.ts
6
+ var o = 900, s = [
7
+ "/mfa/enroll/start",
8
+ "/mfa/enroll/verify",
9
+ "/mfa/unenroll",
10
+ "/mfa/recovery-codes/regenerate",
11
+ "/passkey/register/options",
12
+ "/passkey/register/verify",
13
+ "/sessions/revoke-others",
14
+ "/switch-tenant",
15
+ "/impersonate/start"
16
+ ], c = (e) => {
17
+ if (typeof e != "object" || !e) return null;
18
+ let t = e;
19
+ 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 : {
20
+ actorUserId: t.actorUserId,
21
+ actorSessionId: t.actorSessionId,
22
+ grantId: t.grantId,
23
+ startedAt: t.startedAt,
24
+ expiresAt: t.expiresAt,
25
+ ...typeof t.reason == "string" ? { reason: t.reason } : {}
26
+ };
27
+ }, l = (e) => e.type === "user" ? c(a(e)) : null, u = (e) => l(e) !== null, d = () => (e) => {
28
+ if (e.type !== "user" || e.metadata === void 0) return e;
29
+ let t = c(e.metadata[i]), { metadata: n, ...r } = e;
30
+ return {
31
+ ...r,
32
+ metadata: t === null ? { __redacted: "all" } : {
33
+ __redacted: "all-except-impersonation",
34
+ [i]: t
35
+ }
36
+ };
37
+ }, f = (e, t, n = {}) => ({
38
+ status: e,
39
+ body: JSON.stringify(t),
40
+ contentType: "application/json; charset=utf-8",
41
+ ...n
42
+ }), p = (e, t) => r.suspend(() => {
43
+ let n = e.audit(t);
44
+ return n === void 0 ? r.void : r.isEffect(n) ? n : r.tryPromise({
45
+ try: () => Promise.resolve(n),
46
+ catch: (e) => e
47
+ });
48
+ }).pipe(r.catchCause(() => r.void)), m = (e, a, o) => r.gen(function* () {
49
+ let s = o.impersonation, c = (o.now ?? Date.now)(), l = new Date(c), d = e.actorSubject, m = (t, n, i = {}) => p(s, {
50
+ kind: "refused",
51
+ at: l,
52
+ actorUserId: i.actorUser?.id ?? (d.type === "user" ? d.id : null),
53
+ actorEmail: i.actorUser?.email ?? null,
54
+ targetUserId: i.target?.id ?? null,
55
+ targetEmail: i.target?.email ?? null,
56
+ tenantId: d.tenantId ?? null,
57
+ reason: e.reason ?? null,
58
+ refusal: n,
59
+ ipAddress: e.ipAddress ?? null,
60
+ userAgent: e.userAgent ?? null
61
+ }).pipe(r.as(f(t, {
62
+ error: n,
63
+ ...i.message === void 0 ? {} : { message: i.message }
64
+ })));
65
+ if (d.type !== "user") return yield* m(401, "unauthenticated");
66
+ if (u(d)) return yield* m(403, "nested_impersonation", { message: "An impersonated session may not start another impersonation. Stop the current one first." });
67
+ if (!e.actorSessionId) return yield* m(400, "no_actor_session");
68
+ let h = yield* a.findById(d.id);
69
+ if (!h) return yield* m(401, "unauthenticated");
70
+ if (!e.targetUserId && !e.targetEmail) return yield* m(400, "target_required", { actorUser: h });
71
+ let g = e.targetUserId ? yield* a.findById(e.targetUserId) : yield* a.findByEmail(e.targetEmail);
72
+ if (!g) return yield* m(404, "target_not_found", { actorUser: h });
73
+ if (g.id === h.id) return yield* m(400, "self_impersonation", {
74
+ actorUser: h,
75
+ target: g
76
+ });
77
+ let _ = e.reason?.trim() ?? "";
78
+ if ((s.requireReason ?? !0) && _.length === 0) return yield* m(400, "reason_required", {
79
+ actorUser: h,
80
+ target: g,
81
+ message: "A reason is required to impersonate a user."
82
+ });
83
+ if (!(yield* s.authority({
84
+ actor: d,
85
+ actorUser: h,
86
+ target: g
87
+ }).pipe(r.catchCause(() => r.succeed(!1))))) return yield* m(403, "not_permitted", {
88
+ actorUser: h,
89
+ target: g
90
+ });
91
+ if (yield* s.authority({
92
+ actor: n(g),
93
+ actorUser: g,
94
+ target: h
95
+ }).pipe(r.catchCause(() => r.succeed(!0)))) return yield* m(403, "target_may_impersonate", {
96
+ actorUser: h,
97
+ target: g,
98
+ message: "This user may themselves impersonate others and cannot be impersonated."
99
+ });
100
+ let v = s.maxDurationSeconds ?? 900, y = Math.max(1, Math.min(e.durationSeconds ?? v, v)), b = new Date(c + y * 1e3), x = t("sess"), S = yield* a.insertImpersonationGrant({
101
+ id: t("imp"),
102
+ sessionId: x,
103
+ actorUserId: h.id,
104
+ actorSessionId: e.actorSessionId,
105
+ targetUserId: g.id,
106
+ tenantId: g.tenantId,
107
+ reason: _.length > 0 ? _ : null,
108
+ expiresAt: b,
109
+ ipAddress: e.ipAddress ?? null,
110
+ userAgent: e.userAgent ?? null
111
+ }), C = {
112
+ actorUserId: h.id,
113
+ actorSessionId: e.actorSessionId,
114
+ grantId: S.id,
115
+ startedAt: S.startedAt.toISOString(),
116
+ expiresAt: b.toISOString(),
117
+ ..._.length > 0 ? { reason: _ } : {}
118
+ }, w = yield* o.issuer.issue(g, {
119
+ sessionId: x,
120
+ ttlSeconds: y,
121
+ metadata: { [i]: C },
122
+ ipAddress: e.ipAddress ?? null,
123
+ userAgent: e.userAgent ?? null
124
+ });
125
+ return yield* p(s, {
126
+ kind: "started",
127
+ at: l,
128
+ actorUserId: h.id,
129
+ actorEmail: h.email,
130
+ targetUserId: g.id,
131
+ targetEmail: g.email,
132
+ tenantId: g.tenantId,
133
+ sessionId: x,
134
+ grantId: S.id,
135
+ reason: _.length > 0 ? _ : null,
136
+ expiresAt: b,
137
+ ipAddress: e.ipAddress ?? null,
138
+ userAgent: e.userAgent ?? null
139
+ }), f(200, {
140
+ ok: !0,
141
+ impersonation: C,
142
+ subject: w.subject
143
+ }, { setCookie: w.setCookie });
144
+ }), h = (t, n, i) => r.gen(function* () {
145
+ let a = i.impersonation, o = (i.now ?? Date.now)(), s = new Date(o), c = l(t.subject);
146
+ if (!c || !t.sessionId) return f(400, { error: "not_impersonating" });
147
+ let u = yield* n.findImpersonationGrantBySession(t.sessionId);
148
+ yield* n.endImpersonationGrant(t.sessionId, "stopped", s).pipe(r.ignore), t.subject.type === "user" && (yield* n.revokeSession(t.subject.id, t.sessionId).pipe(r.ignore)), i.invalidateSession?.(t.sessionId);
149
+ let d = yield* n.findById(c.actorUserId), m = yield* n.findSession(c.actorSessionId), h = m ? Math.floor((m.expiresAt.getTime() - o) / 1e3) : 0;
150
+ if (yield* p(a, {
151
+ kind: "stopped",
152
+ at: s,
153
+ actorUserId: c.actorUserId,
154
+ actorEmail: d?.email ?? null,
155
+ targetUserId: t.subject.type === "user" ? t.subject.id : null,
156
+ targetEmail: null,
157
+ tenantId: u?.tenantId ?? t.subject.tenantId ?? null,
158
+ sessionId: t.sessionId,
159
+ ...u ? { grantId: u.id } : {},
160
+ reason: u?.reason ?? null
161
+ }), !d || !e(m, c.actorSessionId, d.id, o) || !Number.isSafeInteger(h) || h <= 0) return f(200, {
162
+ ok: !0,
163
+ restored: !1
164
+ }, { setCookie: i.issuer.clear() });
165
+ let g = yield* i.issuer.reissue(d, {
166
+ sessionId: c.actorSessionId,
167
+ ttlSeconds: h
168
+ });
169
+ return f(200, {
170
+ ok: !0,
171
+ restored: !0,
172
+ subject: g.subject
173
+ }, { setCookie: g.setCookie });
174
+ });
175
+ //#endregion
176
+ export { o as IMPERSONATION_DEFAULT_DURATION_S, s as IMPERSONATION_FORBIDDEN_SUBPATHS, m as handleImpersonationStart, h as handleImpersonationStop, d as impersonationAuditRedactor, l as impersonationOf, u as isImpersonated };