@voltro/plugin-auth 0.85.0 → 0.86.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,175 @@
1
+ import { newAuthId as e, subjectFromUser as t } from "./userStore.js";
2
+ import { Effect as n } from "effect";
3
+ import { IMPERSONATION_METADATA_KEY as r, rawImpersonationMark as i } from "@voltro/protocol";
4
+ //#region src/impersonation.ts
5
+ var a = 900, o = [
6
+ "/mfa/enroll/start",
7
+ "/mfa/enroll/verify",
8
+ "/mfa/unenroll",
9
+ "/mfa/recovery-codes/regenerate",
10
+ "/passkey/register/options",
11
+ "/passkey/register/verify",
12
+ "/sessions/revoke-others",
13
+ "/switch-tenant",
14
+ "/impersonate/start"
15
+ ], s = (e) => {
16
+ if (typeof e != "object" || !e) return null;
17
+ let t = e;
18
+ 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 : {
19
+ actorUserId: t.actorUserId,
20
+ actorSessionId: t.actorSessionId,
21
+ grantId: t.grantId,
22
+ startedAt: t.startedAt,
23
+ expiresAt: t.expiresAt,
24
+ ...typeof t.reason == "string" ? { reason: t.reason } : {}
25
+ };
26
+ }, c = (e) => e.type === "user" ? s(i(e)) : null, l = (e) => c(e) !== null, u = () => (e) => {
27
+ if (e.type !== "user" || e.metadata === void 0) return e;
28
+ let t = s(e.metadata[r]), { metadata: n, ...i } = e;
29
+ return {
30
+ ...i,
31
+ metadata: t === null ? { __redacted: "all" } : {
32
+ __redacted: "all-except-impersonation",
33
+ [r]: t
34
+ }
35
+ };
36
+ }, d = (e, t, n = {}) => ({
37
+ status: e,
38
+ body: JSON.stringify(t),
39
+ contentType: "application/json; charset=utf-8",
40
+ ...n
41
+ }), f = (e, t) => n.suspend(() => {
42
+ let r = e.audit(t);
43
+ return r === void 0 ? n.void : n.isEffect(r) ? r : n.tryPromise({
44
+ try: () => Promise.resolve(r),
45
+ catch: (e) => e
46
+ });
47
+ }).pipe(n.catchCause(() => n.void)), p = (i, a, o) => n.gen(function* () {
48
+ let s = o.impersonation, c = (o.now ?? Date.now)(), u = new Date(c), p = i.actorSubject, m = (e, t, r = {}) => f(s, {
49
+ kind: "refused",
50
+ at: u,
51
+ actorUserId: r.actorUser?.id ?? (p.type === "user" ? p.id : null),
52
+ actorEmail: r.actorUser?.email ?? null,
53
+ targetUserId: r.target?.id ?? null,
54
+ targetEmail: r.target?.email ?? null,
55
+ tenantId: p.tenantId ?? null,
56
+ reason: i.reason ?? null,
57
+ refusal: t,
58
+ ipAddress: i.ipAddress ?? null,
59
+ userAgent: i.userAgent ?? null
60
+ }).pipe(n.as(d(e, {
61
+ error: t,
62
+ ...r.message === void 0 ? {} : { message: r.message }
63
+ })));
64
+ if (p.type !== "user") return yield* m(401, "unauthenticated");
65
+ if (l(p)) return yield* m(403, "nested_impersonation", { message: "An impersonated session may not start another impersonation. Stop the current one first." });
66
+ if (!i.actorSessionId) return yield* m(400, "no_actor_session");
67
+ let h = yield* a.findById(p.id);
68
+ if (!h) return yield* m(401, "unauthenticated");
69
+ if (!i.targetUserId && !i.targetEmail) return yield* m(400, "target_required", { actorUser: h });
70
+ let g = i.targetUserId ? yield* a.findById(i.targetUserId) : yield* a.findByEmail(i.targetEmail);
71
+ if (!g) return yield* m(404, "target_not_found", { actorUser: h });
72
+ if (g.id === h.id) return yield* m(400, "self_impersonation", {
73
+ actorUser: h,
74
+ target: g
75
+ });
76
+ let _ = i.reason?.trim() ?? "";
77
+ if ((s.requireReason ?? !0) && _.length === 0) return yield* m(400, "reason_required", {
78
+ actorUser: h,
79
+ target: g,
80
+ message: "A reason is required to impersonate a user."
81
+ });
82
+ if (!(yield* s.authority({
83
+ actor: p,
84
+ actorUser: h,
85
+ target: g
86
+ }).pipe(n.catchCause(() => n.succeed(!1))))) return yield* m(403, "not_permitted", {
87
+ actorUser: h,
88
+ target: g
89
+ });
90
+ if (yield* s.authority({
91
+ actor: t(g),
92
+ actorUser: g,
93
+ target: h
94
+ }).pipe(n.catchCause(() => n.succeed(!0)))) return yield* m(403, "target_may_impersonate", {
95
+ actorUser: h,
96
+ target: g,
97
+ message: "This user may themselves impersonate others and cannot be impersonated."
98
+ });
99
+ let v = s.maxDurationSeconds ?? 900, y = Math.max(1, Math.min(i.durationSeconds ?? v, v)), b = new Date(c + y * 1e3), x = e("sess"), S = yield* a.insertImpersonationGrant({
100
+ id: e("imp"),
101
+ sessionId: x,
102
+ actorUserId: h.id,
103
+ actorSessionId: i.actorSessionId,
104
+ targetUserId: g.id,
105
+ tenantId: g.tenantId,
106
+ reason: _.length > 0 ? _ : null,
107
+ expiresAt: b,
108
+ ipAddress: i.ipAddress ?? null,
109
+ userAgent: i.userAgent ?? null
110
+ }), C = {
111
+ actorUserId: h.id,
112
+ actorSessionId: i.actorSessionId,
113
+ grantId: S.id,
114
+ startedAt: S.startedAt.toISOString(),
115
+ expiresAt: b.toISOString(),
116
+ ..._.length > 0 ? { reason: _ } : {}
117
+ }, w = yield* o.issuer.issue(g, {
118
+ sessionId: x,
119
+ ttlSeconds: y,
120
+ metadata: { [r]: C },
121
+ ipAddress: i.ipAddress ?? null,
122
+ userAgent: i.userAgent ?? null
123
+ });
124
+ return yield* f(s, {
125
+ kind: "started",
126
+ at: u,
127
+ actorUserId: h.id,
128
+ actorEmail: h.email,
129
+ targetUserId: g.id,
130
+ targetEmail: g.email,
131
+ tenantId: g.tenantId,
132
+ sessionId: x,
133
+ grantId: S.id,
134
+ reason: _.length > 0 ? _ : null,
135
+ expiresAt: b,
136
+ ipAddress: i.ipAddress ?? null,
137
+ userAgent: i.userAgent ?? null
138
+ }), d(200, {
139
+ ok: !0,
140
+ impersonation: C,
141
+ subject: w.subject
142
+ }, { setCookie: w.setCookie });
143
+ }), m = (e, t, r) => n.gen(function* () {
144
+ let i = r.impersonation, a = (r.now ?? Date.now)(), o = new Date(a), s = c(e.subject);
145
+ if (!s || !e.sessionId) return d(400, { error: "not_impersonating" });
146
+ let l = yield* t.findImpersonationGrantBySession(e.sessionId);
147
+ yield* t.endImpersonationGrant(e.sessionId, "stopped", o).pipe(n.ignore), e.subject.type === "user" && (yield* t.revokeSession(e.subject.id, e.sessionId).pipe(n.ignore)), r.invalidateSession?.(e.sessionId);
148
+ let u = yield* t.findById(s.actorUserId), p = yield* t.findSession(s.actorSessionId), m = p ? Math.floor((p.expiresAt.getTime() - a) / 1e3) : 0;
149
+ if (yield* f(i, {
150
+ kind: "stopped",
151
+ at: o,
152
+ actorUserId: s.actorUserId,
153
+ actorEmail: u?.email ?? null,
154
+ targetUserId: e.subject.type === "user" ? e.subject.id : null,
155
+ targetEmail: null,
156
+ tenantId: l?.tenantId ?? e.subject.tenantId ?? null,
157
+ sessionId: e.sessionId,
158
+ ...l ? { grantId: l.id } : {},
159
+ reason: l?.reason ?? null
160
+ }), !u || !p || m <= 0) return d(200, {
161
+ ok: !0,
162
+ restored: !1
163
+ }, { setCookie: r.issuer.clear() });
164
+ let h = yield* r.issuer.reissue(u, {
165
+ sessionId: s.actorSessionId,
166
+ ttlSeconds: m
167
+ });
168
+ return d(200, {
169
+ ok: !0,
170
+ restored: !0,
171
+ subject: h.subject
172
+ }, { setCookie: h.setCookie });
173
+ });
174
+ //#endregion
175
+ export { a as IMPERSONATION_DEFAULT_DURATION_S, o as IMPERSONATION_FORBIDDEN_SUBPATHS, p as handleImpersonationStart, m as handleImpersonationStop, u as impersonationAuditRedactor, c as impersonationOf, l as isImpersonated };
package/dist/index.d.ts CHANGED
@@ -310,7 +310,7 @@ export declare const dataStoreChallengeStore: (sql: SqlClient) => ChallengeStore
310
310
  * and writes go through the store the framework opened, so there is no second
311
311
  * pool and no second schema.
312
312
  */
313
- export declare const dataStoreSessionRegistry: (store: DataStore) => SessionRegistry;
313
+ export declare const dataStoreSessionRegistry: (store: DataStore, options?: SessionRegistryStoreOptions) => SessionRegistry;
314
314
 
315
315
  /** Default set of roles allowed to invite at all. */
316
316
  export declare const DEFAULT_INVITER_ROLES: ReadonlyArray<string>;
@@ -1234,11 +1234,6 @@ export declare interface IssuedSession {
1234
1234
  readonly setCookie: string;
1235
1235
  }
1236
1236
 
1237
- /**
1238
- * `issueSession` with a row behind it: writes the session to the registry,
1239
- * stamps its id into the cookie and signs. A cookie issued here can be killed
1240
- * before its `exp`; one issued with plain `issueSession` cannot.
1241
- */
1242
1237
  export declare const issueRegisteredSession: (registry: Pick<SessionRegistry, "insertSession">, subject: Subject | SubjectIdentity, secret: SessionSecretInput, options?: IssueRegisteredSessionOptions) => Promise<IssuedRegisteredSession>;
1243
1238
 
1244
1239
  export declare interface IssueRegisteredSessionOptions extends IssueSessionOptions {
@@ -1799,6 +1794,9 @@ export declare interface RecoveryCodeRecord {
1799
1794
  readonly createdAt: Date;
1800
1795
  }
1801
1796
 
1797
+ export declare interface RegisteredSessionRotationOptions extends IssueRegisteredSessionOptions, SessionRegistryStoreOptions {
1798
+ }
1799
+
1802
1800
  export declare type RegistrationOutcome = {
1803
1801
  readonly ok: true;
1804
1802
  readonly result: RegistrationResult;
@@ -1846,6 +1844,11 @@ declare interface RevokeSessionInput {
1846
1844
  readonly sessionId: string;
1847
1845
  }
1848
1846
 
1847
+ /** Rotate one live credential atomically. Retained inactive aliases let
1848
+ * sign-out using an earlier signed cookie revoke this device's successor.
1849
+ * The caller must separately validate current account and tenant authority. */
1850
+ export declare const rotateRegisteredSession: (store: DataStore, subject: Subject | SubjectIdentity, presentedSessionId: string, secret: SessionSecretInput, options?: RegisteredSessionRotationOptions) => Promise<IssuedRegisteredSession | null>;
1851
+
1849
1852
  /**
1850
1853
  * Run the guards in order against `user`, short-circuiting on the FIRST
1851
1854
  * rejection. With no guards (or all allowing) it resolves `{ ok: true }`.
@@ -1891,6 +1894,10 @@ export declare const sessionIdOfSubject: (subject: Subject) => string | null;
1891
1894
  /** A server-side session row — enumerated so apps can list active
1892
1895
  * devices + revoke them. Written on sign-in; deleted on revoke. */
1893
1896
  export declare interface SessionRecord {
1897
+ /** Stable device lineage. The original row is its transaction lock anchor. */
1898
+ readonly familyId?: string | null;
1899
+ /** A replaced credential remains an inert revocation alias until the device ends. */
1900
+ readonly rotatedAt?: Date | null;
1894
1901
  readonly id: string;
1895
1902
  readonly userId: string;
1896
1903
  readonly tenantId: string;
@@ -1905,6 +1912,11 @@ export declare interface SessionRecord {
1905
1912
  * `UserStore`, usable without the rest of it. */
1906
1913
  export declare type SessionRegistry = SessionStore;
1907
1914
 
1915
+ export declare interface SessionRegistryStoreOptions {
1916
+ /** Borrow an already owned transaction; otherwise registry writes open one. */
1917
+ readonly transaction?: 'existing';
1918
+ }
1919
+
1908
1920
  export declare interface SessionRevocationChecker {
1909
1921
  /** Is this session still live? `false` means the row was revoked (or
1910
1922
  * is past its `expiresAt`) — the caller rejects the cookie. Verdicts
package/dist/index.js CHANGED
@@ -1,89 +1,23 @@
1
1
  import { PasswordEmptyError as e, PasswordHashError as t, hashPassword as n, needsRehash as r, parseScryptParams as i, verifyPassword as a, verifyPasswordWithRehash as o } from "./password.js";
2
- import { SESSION_COOKIE_NAME as ee, clearSessionCookie as te, issueSession as s, readSession as c, readSessionFromHeaders as ne, readSessionKeyed as re, resolveSessionSecret as ie, resolveSessionSecrets as ae, sessionSecretsOf as oe } from "./session.js";
3
- import { a as l, c as se, i as u, l as d, n as f, o as p, r as m, s as h, t as g } from "./strategy-XaGjSicX.js";
4
- import { $ as _, A as v, B as y, C as b, D as x, E as S, F as C, G as w, H as T, I as E, J as D, K as O, L as k, M as A, N as j, O as M, P as N, Q as P, R as F, S as I, T as L, U as R, V as z, W as B, X as V, Y as H, Z as U, _ as W, _t as G, a as K, at as ce, b as le, c as ue, ct as de, d as fe, dt as pe, et as me, f as he, ft as ge, g as _e, gt as ve, h as ye, ht as be, i as xe, it as Se, j as Ce, k as we, l as Te, lt as Ee, m as De, mt as Oe, n as ke, nt as Ae, o as je, ot as Me, p as Ne, pt as Pe, q as Fe, r as Ie, rt as Le, s as Re, st as ze, t as Be, tt as Ve, u as He, ut as Ue, v as We, vt as Ge, w as Ke, x as qe, y as Je, z as Ye } from "./plugin-CbmcyEJs.js";
5
- import { EMAIL_VERIFY_TTL_S as Xe, MAGIC_LINK_TTL_S as Ze, MFA_PENDING_TTL_S as Qe, NATIVE_CODE_TTL_S as $e, PASSWORD_RESET_TTL_S as et, hashToken as tt, mintSecret as nt, mintToken as rt } from "./tokens.js";
6
- import { CSRF_COOKIE_NAME as it, CSRF_HEADER_NAME as at, isCsrfTokenWellFormed as ot, issueCsrfToken as st, verifyCsrf as q } from "./csrf.js";
7
- import { generateChallenge as ct, verifyAssertion as lt, verifyRegistration as ut } from "./webauthn.js";
8
- import { Effect as J } from "effect";
9
- import { eq as Y } from "@voltro/database";
10
- //#region src/sessionRegistry.ts
11
- var X = "sessions", dt = 604800, Z = (e) => ({
12
- ...e,
13
- expiresAt: new Date(e.expiresAt),
14
- createdAt: new Date(e.createdAt),
15
- lastSeenAt: new Date(e.lastSeenAt)
16
- }), Q = async (e, t) => await e.query({
17
- table: X,
18
- predicate: t,
19
- order: [],
20
- take: void 0,
21
- skip: void 0,
22
- projection: void 0
23
- }), ft = (e) => ({
24
- listSessions: (t) => J.promise(async () => (await Q(e, Y("userId", t))).map(Z).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime())),
25
- insertSession: (t) => J.promise(async () => {
26
- let n = /* @__PURE__ */ new Date(), r = {
27
- ...t,
28
- createdAt: n,
29
- lastSeenAt: n
30
- };
31
- return await e.insert(X, r), r;
32
- }),
33
- findSession: (t) => J.promise(async () => {
34
- let n = (await Q(e, Y("id", t)))[0];
35
- return n === void 0 ? null : Z(n);
36
- }),
37
- touchSession: (t, n) => J.promise(async () => {
38
- await e.update(X, t, {
39
- expiresAt: n,
40
- lastSeenAt: /* @__PURE__ */ new Date()
41
- });
42
- }),
43
- revokeSession: (t, n) => J.promise(async () => {
44
- let r = (await Q(e, Y("id", n)))[0];
45
- return r === void 0 || r.userId !== t ? !1 : e.delete(X, n);
46
- }),
47
- revokeAllOtherSessions: (t, n) => J.promise(async () => {
48
- let r = 0;
49
- for (let i of await Q(e, Y("userId", t))) i.id !== n && await e.delete(X, i.id) && (r += 1);
50
- return r;
51
- }),
52
- revokeAllSessions: (t) => J.promise(async () => {
53
- let n = 0;
54
- for (let r of await Q(e, Y("userId", t))) await e.delete(X, r.id) && (n += 1);
55
- return n;
56
- })
57
- }), $ = (e, t) => e.type === "user" ? {
58
- ...e,
59
- metadata: {
60
- ...e.metadata ?? {},
61
- sessionId: t
62
- }
63
- } : e, pt = async (e, t, n, r = {}) => {
64
- if (t.type !== "user") throw Error("issueRegisteredSession: only a user subject has a session row");
65
- let i = l("sess"), a = r.ttlSeconds ?? dt, o = (r.now ?? Date.now)();
66
- await J.runPromise(e.insertSession({
67
- id: i,
68
- userId: t.id,
69
- tenantId: t.tenantId ?? "",
70
- expiresAt: new Date(o + a * 1e3),
71
- ipAddress: r.ipAddress ?? null,
72
- userAgent: r.userAgent ?? null
73
- }));
74
- let { ipAddress: ee, userAgent: te, ...c } = r;
75
- return {
76
- ...s($(t, i), n, c),
77
- sessionId: i
78
- };
79
- }, mt = (e) => async (t) => t.type === "user" ? (await J.runPromise(e.listMemberships(t.id))).map((e) => ({
80
- tenantId: e.tenantId,
81
- role: e.role
82
- })) : [], ht = (e) => (t) => J.runPromise(e.send({
83
- to: t.to,
84
- subject: t.subject,
85
- html: t.html,
86
- text: t.text
87
- }).pipe(J.asVoid, J.orElseSucceed(() => void 0)));
88
- //#endregion
89
- export { it as CSRF_COOKIE_NAME, at as CSRF_HEADER_NAME, _e as DEFAULT_INVITER_ROLES, W as DEFAULT_ROLE_RANK, me as EMAIL_NOT_VERIFIED_CODE, Ve as EMAIL_VERIFIED_METADATA_KEY, Ae as EMAIL_VERIFY_RESEND_COOLDOWN_S, Xe as EMAIL_VERIFY_TTL_S, Te as IMPERSONATION_DEFAULT_DURATION_S, He as IMPERSONATION_FORBIDDEN_SUBPATHS, We as INVITATION_MAX_PENDING, Je as INVITATION_TTL_S, Ze as MAGIC_LINK_TTL_S, Qe as MFA_PENDING_TTL_S, $e as NATIVE_CODE_TTL_S, Ie as NATIVE_PENDING_COOKIE, et as PASSWORD_RESET_TTL_S, e as PasswordEmptyError, t as PasswordHashError, ee as SESSION_COOKIE_NAME, Ue as TotpVerifyError, f as UserAlreadyExistsError, m as UserNotFoundError, G as appUserStore, mt as authMemberships, Be as authRoutesPlugin, te as clearSessionCookie, j as configSessionSecrets, x as dataStoreChallengeStore, ft as dataStoreSessionRegistry, le as defaultCanGrantRole, Le as emailVerificationActive, Se as emailVerificationGuard, ct as generateChallenge, pe as generateRecoveryCodes, ge as generateTotpCode, Pe as generateTotpSecret, ke as getAuthSessionStrategy, N as guardSubject, qe as handleAcceptInvitation, I as handleAcceptInvitationWithSignUp, b as handleCreateInvitation, C as handleCsrf, ce as handleEmailVerifyConfirm, Me as handleEmailVerifyRequest, fe as handleImpersonationStart, he as handleImpersonationStop, Ke as handleListInvitations, E as handleListMemberships, k as handleListSessions, F as handleMagicLinkConsume, Ye as handleMagicLinkRequest, y as handleMfaEnrollStart, z as handleMfaEnrollVerify, T as handleMfaRegenerateRecoveryCodes, R as handleMfaUnenroll, B as handleMfaVerify, xe as handleNativeAuthorize, K as handleNativeComplete, je as handleNativeExchange, M as handlePasskeyAssertOptions, we as handlePasskeyAssertVerify, v as handlePasskeyRegisterOptions, Ce as handlePasskeyRegisterVerify, w as handlePasswordResetConfirm, O as handlePasswordResetRequest, L as handlePreviewInvitation, Fe as handleRevokeAllOtherSessions, S as handleRevokeInvitation, D as handleRevokeSession, H as handleSignIn, V as handleSignOut, U as handleSignUp, P as handleSwitchTenant, n as hashPassword, tt as hashToken, Ne as impersonationAuditRedactor, De as impersonationOf, Re as isAllowedRedirectUri, ot as isCsrfTokenWellFormed, ze as isEmailVerified, ye as isImpersonated, st as issueCsrfToken, pt as issueRegisteredSession, s as issueSession, _ as issueUserSession, ht as mailSender, d as makeSessionRevocationChecker, A as memoryChallengeStore, u as memoryUserStore, nt as mintSecret, rt as mintToken, ue as nativeLoginRedirect, r as needsRehash, l as newAuthId, Oe as otpauthUrl, i as parseScryptParams, Ge as postgresUserStore, c as readSession, ne as readSessionFromHeaders, re as readSessionKeyed, ie as resolveSessionSecret, ae as resolveSessionSecrets, ve as runSubjectGuards, de as sendEmailVerification, p as sessionIdOfSubject, oe as sessionSecretsOf, h as subjectFromUser, se as subjectMemberships, Ee as userEmailVerified, lt as verifyAssertion, q as verifyCsrf, a as verifyPassword, o as verifyPasswordWithRehash, ut as verifyRegistration, be as verifyTotpCode, g as voltroPasswordStrategy, $ as withSessionId };
2
+ import { SESSION_COOKIE_NAME as s, clearSessionCookie as c, issueSession as l, readSession as u, readSessionFromHeaders as d, readSessionKeyed as f, resolveSessionSecret as p, resolveSessionSecrets as m, sessionSecretsOf as h } from "./session.js";
3
+ import { makeSessionRevocationChecker as g } from "./revocation.js";
4
+ import { UserAlreadyExistsError as _, UserNotFoundError as v, memoryUserStore as y, newAuthId as b, sessionIdOfSubject as x, subjectFromUser as S, subjectMemberships as C } from "./userStore.js";
5
+ import { dataStoreSessionRegistry as w, issueRegisteredSession as T, rotateRegisteredSession as E, withSessionId as D } from "./sessionRegistry.js";
6
+ import { authMemberships as O } from "./memberships.js";
7
+ import { postgresUserStore as k } from "./postgresUserStore.js";
8
+ import { appUserStore as A } from "./appUserStore.js";
9
+ import { runSubjectGuards as j } from "./subjectGuard.js";
10
+ import { TotpVerifyError as M, generateRecoveryCodes as N, generateTotpCode as P, generateTotpSecret as F, otpauthUrl as I, verifyTotpCode as L } from "./totp.js";
11
+ import { EMAIL_VERIFY_TTL_S as R, MAGIC_LINK_TTL_S as z, MFA_PENDING_TTL_S as B, NATIVE_CODE_TTL_S as V, PASSWORD_RESET_TTL_S as H, hashToken as U, mintSecret as W, mintToken as G } from "./tokens.js";
12
+ import { CSRF_COOKIE_NAME as K, CSRF_HEADER_NAME as q, isCsrfTokenWellFormed as J, issueCsrfToken as Y, verifyCsrf as X } from "./csrf.js";
13
+ import { EMAIL_NOT_VERIFIED_CODE as Z, EMAIL_VERIFIED_METADATA_KEY as Q, EMAIL_VERIFY_RESEND_COOLDOWN_S as $, emailVerificationActive as ee, emailVerificationGuard as te, handleEmailVerifyConfirm as ne, handleEmailVerifyRequest as re, isEmailVerified as ie, sendEmailVerification as ae, userEmailVerified as oe } from "./emailVerification.js";
14
+ import { configSessionSecrets as se, guardSubject as ce, handleCsrf as le, handleListMemberships as ue, handleListSessions as de, handleMagicLinkConsume as fe, handleMagicLinkRequest as pe, handleMfaEnrollStart as me, handleMfaEnrollVerify as he, handleMfaRegenerateRecoveryCodes as ge, handleMfaUnenroll as _e, handleMfaVerify as ve, handlePasswordResetConfirm as ye, handlePasswordResetRequest as be, handleRevokeAllOtherSessions as xe, handleRevokeSession as Se, handleSignIn as Ce, handleSignOut as we, handleSignUp as Te, handleSwitchTenant as Ee, issueUserSession as De } from "./handlers.js";
15
+ import { generateChallenge as Oe, verifyAssertion as ke, verifyRegistration as Ae } from "./webauthn.js";
16
+ import { dataStoreChallengeStore as je, handlePasskeyAssertOptions as Me, handlePasskeyAssertVerify as Ne, handlePasskeyRegisterOptions as Pe, handlePasskeyRegisterVerify as Fe, memoryChallengeStore as Ie } from "./passkeyHandlers.js";
17
+ import { DEFAULT_INVITER_ROLES as Le, DEFAULT_ROLE_RANK as Re, INVITATION_MAX_PENDING as ze, INVITATION_TTL_S as Be, defaultCanGrantRole as Ve, handleAcceptInvitation as He, handleAcceptInvitationWithSignUp as Ue, handleCreateInvitation as We, handleListInvitations as Ge, handlePreviewInvitation as Ke, handleRevokeInvitation as qe } from "./invitations.js";
18
+ import { IMPERSONATION_DEFAULT_DURATION_S as Je, IMPERSONATION_FORBIDDEN_SUBPATHS as Ye, handleImpersonationStart as Xe, handleImpersonationStop as Ze, impersonationAuditRedactor as Qe, impersonationOf as $e, isImpersonated as et } from "./impersonation.js";
19
+ import { NATIVE_PENDING_COOKIE as tt, handleNativeAuthorize as nt, handleNativeComplete as rt, handleNativeExchange as it, isAllowedRedirectUri as at, nativeLoginRedirect as ot } from "./nativeRoutes.js";
20
+ import { voltroPasswordStrategy as st } from "./strategy.js";
21
+ import { authRoutesPlugin as ct, getAuthSessionStrategy as lt } from "./plugin.js";
22
+ import { mailSender as ut } from "./mail.js";
23
+ export { K as CSRF_COOKIE_NAME, q as CSRF_HEADER_NAME, Le as DEFAULT_INVITER_ROLES, Re as DEFAULT_ROLE_RANK, Z as EMAIL_NOT_VERIFIED_CODE, Q as EMAIL_VERIFIED_METADATA_KEY, $ as EMAIL_VERIFY_RESEND_COOLDOWN_S, R as EMAIL_VERIFY_TTL_S, Je as IMPERSONATION_DEFAULT_DURATION_S, Ye as IMPERSONATION_FORBIDDEN_SUBPATHS, ze as INVITATION_MAX_PENDING, Be as INVITATION_TTL_S, z as MAGIC_LINK_TTL_S, B as MFA_PENDING_TTL_S, V as NATIVE_CODE_TTL_S, tt as NATIVE_PENDING_COOKIE, H as PASSWORD_RESET_TTL_S, e as PasswordEmptyError, t as PasswordHashError, s as SESSION_COOKIE_NAME, M as TotpVerifyError, _ as UserAlreadyExistsError, v as UserNotFoundError, A as appUserStore, O as authMemberships, ct as authRoutesPlugin, c as clearSessionCookie, se as configSessionSecrets, je as dataStoreChallengeStore, w as dataStoreSessionRegistry, Ve as defaultCanGrantRole, ee as emailVerificationActive, te as emailVerificationGuard, Oe as generateChallenge, N as generateRecoveryCodes, P as generateTotpCode, F as generateTotpSecret, lt as getAuthSessionStrategy, ce as guardSubject, He as handleAcceptInvitation, Ue as handleAcceptInvitationWithSignUp, We as handleCreateInvitation, le as handleCsrf, ne as handleEmailVerifyConfirm, re as handleEmailVerifyRequest, Xe as handleImpersonationStart, Ze as handleImpersonationStop, Ge as handleListInvitations, ue as handleListMemberships, de as handleListSessions, fe as handleMagicLinkConsume, pe as handleMagicLinkRequest, me as handleMfaEnrollStart, he as handleMfaEnrollVerify, ge as handleMfaRegenerateRecoveryCodes, _e as handleMfaUnenroll, ve as handleMfaVerify, nt as handleNativeAuthorize, rt as handleNativeComplete, it as handleNativeExchange, Me as handlePasskeyAssertOptions, Ne as handlePasskeyAssertVerify, Pe as handlePasskeyRegisterOptions, Fe as handlePasskeyRegisterVerify, ye as handlePasswordResetConfirm, be as handlePasswordResetRequest, Ke as handlePreviewInvitation, xe as handleRevokeAllOtherSessions, qe as handleRevokeInvitation, Se as handleRevokeSession, Ce as handleSignIn, we as handleSignOut, Te as handleSignUp, Ee as handleSwitchTenant, n as hashPassword, U as hashToken, Qe as impersonationAuditRedactor, $e as impersonationOf, at as isAllowedRedirectUri, J as isCsrfTokenWellFormed, ie as isEmailVerified, et as isImpersonated, Y as issueCsrfToken, T as issueRegisteredSession, l as issueSession, De as issueUserSession, ut as mailSender, g as makeSessionRevocationChecker, Ie as memoryChallengeStore, y as memoryUserStore, W as mintSecret, G as mintToken, ot as nativeLoginRedirect, r as needsRehash, b as newAuthId, I as otpauthUrl, i as parseScryptParams, k as postgresUserStore, u as readSession, d as readSessionFromHeaders, f as readSessionKeyed, p as resolveSessionSecret, m as resolveSessionSecrets, E as rotateRegisteredSession, j as runSubjectGuards, ae as sendEmailVerification, x as sessionIdOfSubject, h as sessionSecretsOf, S as subjectFromUser, C as subjectMemberships, oe as userEmailVerified, ke as verifyAssertion, X as verifyCsrf, a as verifyPassword, o as verifyPasswordWithRehash, Ae as verifyRegistration, L as verifyTotpCode, st as voltroPasswordStrategy, D as withSessionId };
@@ -0,0 +1,187 @@
1
+ import { newAuthId as e } from "./userStore.js";
2
+ import { hashToken as t, mintSecret as n } from "./tokens.js";
3
+ import { decideAdmission as r } from "./admission.js";
4
+ import { Effect as i } from "effect";
5
+ //#region src/invitations.ts
6
+ var a = 604800, o = 500, s = [
7
+ "owner",
8
+ "admin",
9
+ "member",
10
+ "viewer"
11
+ ], c = ["owner", "admin"], l = (e, t) => t.indexOf(e), u = (e, t = s) => {
12
+ let n = l(e.inviterRole, t);
13
+ if (n < 0) return !1;
14
+ let r = l(e.requestedRole, t);
15
+ return r < 0 ? n === 0 : r >= n;
16
+ }, d = (e) => {
17
+ let t = e?.roleRank ?? s;
18
+ return e?.canGrantRole ?? ((e) => u(e, t));
19
+ }, f = (e, t, n = {}) => ({
20
+ status: e,
21
+ body: JSON.stringify(t),
22
+ contentType: "application/json; charset=utf-8",
23
+ ...n
24
+ }), p = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, m = (e) => ({
25
+ id: e.id,
26
+ email: e.email,
27
+ tenantId: e.tenantId,
28
+ role: e.role,
29
+ invitedByUserId: e.invitedByUserId,
30
+ expiresAt: e.expiresAt.toISOString(),
31
+ acceptedAt: e.acceptedAt?.toISOString() ?? null,
32
+ revokedAt: e.revokedAt?.toISOString() ?? null,
33
+ createdAt: e.createdAt.toISOString()
34
+ }), h = (e, t, n, r) => i.gen(function* () {
35
+ let i = yield* n.membershipRole(e, t);
36
+ return i === null || !(r?.inviterRoles ?? c).includes(i) ? f(403, { error: "not_permitted" }) : { role: i };
37
+ }), g = (e) => "status" in e, _ = (t, r, a) => i.gen(function* () {
38
+ let o = t.email.trim().toLowerCase();
39
+ if (!o || !o.includes("@")) return f(400, { error: "a valid email is required" });
40
+ if (!t.tenantId) return f(400, { error: "tenantId required" });
41
+ if (!t.role) return f(400, { error: "role required" });
42
+ let s = a.invitations, c = yield* h(t.inviterUserId, t.tenantId, r, s);
43
+ if (g(c)) return c;
44
+ if (!d(s)({
45
+ inviterRole: c.role,
46
+ requestedRole: t.role,
47
+ tenantId: t.tenantId
48
+ })) return f(403, {
49
+ error: "role_not_grantable",
50
+ message: `A '${c.role}' may not invite a '${t.role}'.`
51
+ });
52
+ let l = new Date((a.now ?? Date.now)());
53
+ if ((yield* r.countPendingInvitations(t.tenantId, l)) >= (s?.maxPending ?? 500)) return f(429, { error: "too_many_pending_invitations" });
54
+ yield* r.supersedePendingInvitations(t.tenantId, o, t.inviterUserId, l).pipe(i.ignore);
55
+ let u = s?.ttlSeconds ?? 604800, _ = n(u), v = yield* r.insertInvitation({
56
+ id: e("inv"),
57
+ tokenHash: _.tokenHash,
58
+ email: o,
59
+ tenantId: t.tenantId,
60
+ role: t.role,
61
+ invitedByUserId: t.inviterUserId,
62
+ expiresAt: new Date(l.getTime() + u * 1e3)
63
+ });
64
+ if (a.sendEmail) {
65
+ let e = p(a.appBaseUrl ?? "", s?.callbackPath ?? "/auth/invitations/accept", _.token);
66
+ yield* i.tryPromise(() => a.sendEmail({
67
+ to: o,
68
+ kind: "invitation",
69
+ actionUrl: e,
70
+ subject: "You have been invited",
71
+ text: `You have been invited to join as ${t.role}: ${e}`,
72
+ html: `<p>You have been invited to join as <strong>${t.role}</strong>. <a href="${e}">Accept the invitation</a> — this link expires.</p>`
73
+ })).pipe(i.ignore);
74
+ }
75
+ return f(201, {
76
+ ok: !0,
77
+ invitation: m(v)
78
+ });
79
+ }), v = (e, t, n) => i.gen(function* () {
80
+ let r = yield* h(e.callerUserId, e.tenantId, t, n.invitations);
81
+ return g(r) ? r : f(200, { invitations: (yield* t.listInvitations(e.tenantId)).map(m) });
82
+ }), y = (e, t, n) => i.gen(function* () {
83
+ if (!e.invitationId) return f(400, { error: "invitationId required" });
84
+ let r = yield* h(e.callerUserId, e.tenantId, t, n.invitations);
85
+ if (g(r)) return r;
86
+ let i = new Date((n.now ?? Date.now)());
87
+ return (yield* t.revokeInvitation(e.invitationId, e.tenantId, e.callerUserId, i)) ? f(200, {
88
+ ok: !0,
89
+ revoked: 1
90
+ }) : f(404, { error: "invitation_not_found" });
91
+ }), b = (e, n, r = {}) => i.gen(function* () {
92
+ if (!e.token) return f(400, { error: "token required" });
93
+ let i = yield* n.findInvitationByHash(t(e.token));
94
+ if (!i) return f(404, { error: "invalid_invitation" });
95
+ let a = new Date((r.now ?? Date.now)());
96
+ return f(200, {
97
+ ok: !0,
98
+ state: i.acceptedAt === null ? i.revokedAt === null ? i.expiresAt.getTime() <= a.getTime() ? "expired" : "pending" : "revoked" : "accepted",
99
+ email: i.email,
100
+ tenantId: i.tenantId,
101
+ role: i.role,
102
+ expiresAt: i.expiresAt.toISOString()
103
+ });
104
+ }), x = (e, n, a = {}) => i.gen(function* () {
105
+ if (!e.token) return f(400, { error: "token required" });
106
+ let o = t(e.token), s = new Date((a.now ?? Date.now)()), c = yield* n.findInvitationByHash(o);
107
+ if (!c) return f(401, { error: "invalid_or_expired_invitation" });
108
+ if (!e.userId) return f(401, {
109
+ error: "authentication_required",
110
+ email: c.email
111
+ });
112
+ let l = yield* n.findById(e.userId);
113
+ if (!l) return f(401, {
114
+ error: "authentication_required",
115
+ email: c.email
116
+ });
117
+ if (l.email.toLowerCase() !== c.email.toLowerCase()) return f(403, {
118
+ error: "invitation_email_mismatch",
119
+ message: "This invitation was issued to a different email address. Sign in as that address to accept it."
120
+ });
121
+ let u = yield* r(a.admit, {
122
+ email: c.email,
123
+ tenantId: c.tenantId,
124
+ via: "invitation"
125
+ });
126
+ if (u) return f(403, {
127
+ error: "not_admitted",
128
+ reason: u.reason,
129
+ message: u.message
130
+ });
131
+ let d = yield* n.acceptInvitation(o, l.id, s);
132
+ if (!d) return f(401, { error: "invalid_or_expired_invitation" });
133
+ let p = yield* n.addMembership({
134
+ userId: l.id,
135
+ tenantId: d.tenantId,
136
+ role: d.role
137
+ });
138
+ return yield* n.markEmailVerified(l.id).pipe(i.ignore), f(200, {
139
+ ok: !0,
140
+ membership: {
141
+ tenantId: p.tenantId,
142
+ role: p.role,
143
+ joinedAt: p.joinedAt.toISOString()
144
+ }
145
+ });
146
+ }), S = (n, a, o) => i.gen(function* () {
147
+ if (!n.token || !n.password) return f(400, { error: "token and password required" });
148
+ if (n.password.length < (o.minPasswordLength ?? 8)) return f(400, { error: "password must be at least 8 characters" });
149
+ let s = t(n.token), c = new Date((o.now ?? Date.now)()), l = yield* a.findInvitationByHash(s);
150
+ if (!l) return f(401, { error: "invalid_or_expired_invitation" });
151
+ if (yield* a.findByEmail(l.email)) return f(409, {
152
+ error: "account_exists",
153
+ message: "An account already exists for this address. Sign in, then accept the invitation."
154
+ });
155
+ let u = yield* o.hashPassword(n.password).pipe(i.orElseSucceed(() => ""));
156
+ if (!u) return f(500, { error: "hash_failed" });
157
+ let d = yield* r(o.admit, {
158
+ email: l.email,
159
+ tenantId: l.tenantId,
160
+ via: "invitation"
161
+ });
162
+ if (d) return f(403, {
163
+ error: "not_admitted",
164
+ reason: d.reason,
165
+ message: d.message
166
+ });
167
+ let p = e("user"), m = yield* a.acceptInvitation(s, p, c);
168
+ if (!m) return f(401, { error: "invalid_or_expired_invitation" });
169
+ let h = yield* a.insert({
170
+ id: p,
171
+ email: m.email,
172
+ passwordHash: u,
173
+ tenantId: m.tenantId
174
+ }).pipe(i.orElseSucceed(() => null));
175
+ return h ? (yield* a.addMembership({
176
+ userId: h.id,
177
+ tenantId: m.tenantId,
178
+ role: m.role
179
+ }), yield* a.markEmailVerified(h.id).pipe(i.ignore), f(201, {
180
+ ok: !0,
181
+ userId: h.id,
182
+ tenantId: m.tenantId,
183
+ role: m.role
184
+ })) : f(409, { error: "account_exists" });
185
+ });
186
+ //#endregion
187
+ export { c as DEFAULT_INVITER_ROLES, s as DEFAULT_ROLE_RANK, o as INVITATION_MAX_PENDING, a as INVITATION_TTL_S, u as defaultCanGrantRole, x as handleAcceptInvitation, S as handleAcceptInvitationWithSignUp, _ as handleCreateInvitation, v as handleListInvitations, b as handlePreviewInvitation, y as handleRevokeInvitation };
package/dist/mail.js ADDED
@@ -0,0 +1,10 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/mail.ts
3
+ var t = (t) => (n) => e.runPromise(t.send({
4
+ to: n.to,
5
+ subject: n.subject,
6
+ html: n.html,
7
+ text: n.text
8
+ }).pipe(e.asVoid, e.orElseSucceed(() => void 0)));
9
+ //#endregion
10
+ export { t as mailSender };
@@ -0,0 +1,8 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/memberships.ts
3
+ var t = (t) => async (n) => n.type === "user" ? (await e.runPromise(t.listMemberships(n.id))).map((e) => ({
4
+ tenantId: e.tenantId,
5
+ role: e.role
6
+ })) : [];
7
+ //#endregion
8
+ export { t as authMemberships };