@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.
- package/CHANGELOG.md +5987 -0
- package/README.md +93 -0
- package/THIRD-PARTY-NOTICES.md +1122 -31
- package/dist/admission.js +9 -0
- package/dist/appUserStore.js +19 -0
- package/dist/authExtensions.js +9 -0
- package/dist/emailVerification.js +64 -0
- package/dist/guest.d.ts +99 -0
- package/dist/guest.js +3 -0
- package/dist/guestClient.js +237 -0
- package/dist/guestCredential.js +12 -0
- package/dist/guestPasswordUpgrade.js +85 -0
- package/dist/guestPlugin.js +166 -0
- package/dist/guestSchema.js +20 -0
- package/dist/guestSecrets.js +10 -0
- package/dist/guestService.js +224 -0
- package/dist/handlers.js +337 -0
- package/dist/impersonation.js +176 -0
- package/dist/index.d.ts +282 -20
- package/dist/index.js +30 -88
- package/dist/invitations.js +187 -0
- package/dist/mail.js +10 -0
- package/dist/memberships.js +8 -0
- package/dist/native/guestStorage.js +41 -0
- package/dist/native.d.ts +110 -0
- package/dist/native.js +45 -43
- package/dist/nativeRoutes.js +109 -0
- package/dist/overlayPlugin.js +167 -0
- package/dist/passkeyHandlers.js +162 -0
- package/dist/plugin.d.ts +123 -10
- package/dist/plugin.js +519 -5
- package/dist/postgresUserStore.js +475 -0
- package/dist/revocation.js +50 -0
- package/dist/schema.d.ts +45 -6
- package/dist/schema.js +99 -96
- package/dist/sessionFamily.js +15 -0
- package/dist/sessionRegistry.js +133 -0
- package/dist/signedClaims.js +24 -0
- package/dist/strategy.d.ts +17 -9
- package/dist/strategy.js +69 -3
- package/dist/subjectGuard.js +11 -0
- package/dist/totp.js +55 -0
- package/dist/userStore.js +292 -0
- package/dist/web/guestStorage.js +47 -0
- package/dist/web/passkeyClient.js +78 -0
- package/dist/web/requireAuth.js +9 -0
- package/dist/web/subjectContext.js +13 -0
- package/dist/web.d.ts +94 -0
- package/dist/web.js +6 -90
- package/package.json +8 -3
- package/dist/plugin-CbmcyEJs.js +0 -2018
- package/dist/strategy-XaGjSicX.js +0 -353
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { UserAlreadyExistsError as e, UserNotFoundError as t, newAuthId as n } from "./userStore.js";
|
|
2
|
+
import { Effect as r } from "effect";
|
|
3
|
+
//#region src/postgresUserStore.ts
|
|
4
|
+
var i = (e) => {
|
|
5
|
+
let t = e;
|
|
6
|
+
for (let e = 0; typeof t == "object" && t && e < 6; e++) {
|
|
7
|
+
let e = t.code;
|
|
8
|
+
if (typeof e == "string") return e;
|
|
9
|
+
let n = t;
|
|
10
|
+
t = n.reason ?? n.cause;
|
|
11
|
+
}
|
|
12
|
+
}, a = (e) => {
|
|
13
|
+
let t = e?.columns ?? {};
|
|
14
|
+
return {
|
|
15
|
+
table: e?.table ?? "users",
|
|
16
|
+
col: {
|
|
17
|
+
id: t.id ?? "id",
|
|
18
|
+
email: t.email ?? "email",
|
|
19
|
+
tenantId: t.tenantId ?? "tenantId",
|
|
20
|
+
createdAt: t.createdAt ?? "createdAt",
|
|
21
|
+
passwordHash: t.passwordHash ?? "passwordHash",
|
|
22
|
+
updatedAt: t.updatedAt ?? "updatedAt",
|
|
23
|
+
mfaSecret: t.mfaSecret ?? "mfaSecret",
|
|
24
|
+
mfaEnrolledAt: t.mfaEnrolledAt ?? "mfaEnrolledAt",
|
|
25
|
+
emailVerifiedAt: t.emailVerifiedAt ?? "emailVerifiedAt",
|
|
26
|
+
deactivatedAt: t.deactivatedAt ?? !1
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}, o = (o, s = {}) => {
|
|
30
|
+
let c = a(s.identity), { col: l } = c, u = (e) => o`${o(l[e])}`, d = [
|
|
31
|
+
"id",
|
|
32
|
+
"email",
|
|
33
|
+
"passwordHash",
|
|
34
|
+
"tenantId",
|
|
35
|
+
"createdAt",
|
|
36
|
+
"mfaSecret",
|
|
37
|
+
"mfaEnrolledAt",
|
|
38
|
+
"emailVerifiedAt",
|
|
39
|
+
"deactivatedAt"
|
|
40
|
+
], f, p = () => f ??= {
|
|
41
|
+
usersTable: o.join(".", !1)(c.table.split(".").map((e) => o`${o(e)}`)),
|
|
42
|
+
selectList: o.csv(d.map((e) => l[e] === !1 ? o`NULL AS ${o(e)}` : o`${u(e)} AS ${o(e)}`))
|
|
43
|
+
}, m = (e, t) => /* @__PURE__ */ Error(`postgresUserStore: ${t} needs the "${e}" column, and identity.columns.${e} is false — this table has none`), h = (e) => l.updatedAt === !1 ? o`` : o`, ${u("updatedAt")} = ${e}`, g = (e) => ({
|
|
44
|
+
id: e.id,
|
|
45
|
+
email: e.email,
|
|
46
|
+
passwordHash: e.passwordHash,
|
|
47
|
+
tenantId: e.tenantId,
|
|
48
|
+
createdAt: e.createdAt,
|
|
49
|
+
mfaSecret: e.mfaSecret,
|
|
50
|
+
mfaEnrolledAt: e.mfaEnrolledAt,
|
|
51
|
+
emailVerifiedAt: e.emailVerifiedAt,
|
|
52
|
+
...l.deactivatedAt === !1 ? {} : { deactivatedAt: e.deactivatedAt ?? null }
|
|
53
|
+
}), _ = (e) => r.gen(function* () {
|
|
54
|
+
let t = yield* o`
|
|
55
|
+
SELECT ${p().selectList} FROM ${p().usersTable}
|
|
56
|
+
WHERE ${u("email")} = ${e.toLowerCase()}
|
|
57
|
+
LIMIT 1
|
|
58
|
+
`;
|
|
59
|
+
return t[0] ? g(t[0]) : null;
|
|
60
|
+
}).pipe(r.orElseSucceed(() => null)), v = (e) => r.gen(function* () {
|
|
61
|
+
let t = yield* o`
|
|
62
|
+
SELECT ${p().selectList} FROM ${p().usersTable}
|
|
63
|
+
WHERE ${u("id")} = ${e}
|
|
64
|
+
LIMIT 1
|
|
65
|
+
`;
|
|
66
|
+
return t[0] ? g(t[0]) : null;
|
|
67
|
+
}).pipe(r.orElseSucceed(() => null)), ee = (t) => r.gen(function* () {
|
|
68
|
+
let n = t.email === null ? null : t.email.toLowerCase(), a = /* @__PURE__ */ new Date(), s = [
|
|
69
|
+
u("id"),
|
|
70
|
+
u("email"),
|
|
71
|
+
u("tenantId"),
|
|
72
|
+
u("createdAt")
|
|
73
|
+
], c = [
|
|
74
|
+
t.id,
|
|
75
|
+
n,
|
|
76
|
+
t.tenantId,
|
|
77
|
+
a
|
|
78
|
+
];
|
|
79
|
+
return l.passwordHash !== !1 && (s.push(u("passwordHash")), c.push(t.passwordHash ?? null)), l.updatedAt !== !1 && (s.push(u("updatedAt")), c.push(a)), yield* o`
|
|
80
|
+
INSERT INTO ${p().usersTable} (${o.csv(s)})
|
|
81
|
+
VALUES ${o.in(c)}
|
|
82
|
+
`.pipe(r.map(() => ({
|
|
83
|
+
...t,
|
|
84
|
+
email: n,
|
|
85
|
+
createdAt: a
|
|
86
|
+
})), r.catch((t) => i(t) === "23505" && n !== null ? r.fail(new e({ email: n })) : r.die(t)));
|
|
87
|
+
}), y = (e) => r.gen(function* () {
|
|
88
|
+
let n = (yield* o`
|
|
89
|
+
SELECT ${p().selectList} FROM ${p().usersTable} WHERE ${u("id")} = ${e} LIMIT 1
|
|
90
|
+
`)[0];
|
|
91
|
+
return n ? g(n) : yield* r.fail(new t({ userId: e }));
|
|
92
|
+
}).pipe(r.catchTag("SqlError", (e) => r.die(e))), b = (e, t) => r.gen(function* () {
|
|
93
|
+
return l.passwordHash === !1 ? yield* r.die(m("passwordHash", "updatePassword")) : (yield* o`
|
|
94
|
+
UPDATE ${p().usersTable} SET ${u("passwordHash")} = ${t}${h(/* @__PURE__ */ new Date())}
|
|
95
|
+
WHERE ${u("id")} = ${e}
|
|
96
|
+
`.pipe(r.catchTag("SqlError", (e) => r.die(e))), yield* y(e));
|
|
97
|
+
}), x = (e, t) => r.gen(function* () {
|
|
98
|
+
return l.mfaSecret === !1 ? yield* r.die(m("mfaSecret", "setMfaSecret")) : l.mfaEnrolledAt === !1 ? yield* r.die(m("mfaEnrolledAt", "setMfaSecret")) : (yield* o`
|
|
99
|
+
UPDATE ${p().usersTable}
|
|
100
|
+
SET ${u("mfaSecret")} = ${t}, ${u("mfaEnrolledAt")} = ${null}
|
|
101
|
+
WHERE ${u("id")} = ${e}
|
|
102
|
+
`.pipe(r.catchTag("SqlError", (e) => r.die(e))), yield* y(e));
|
|
103
|
+
}), S = (e) => r.gen(function* () {
|
|
104
|
+
return l.mfaEnrolledAt === !1 ? yield* r.die(m("mfaEnrolledAt", "markMfaEnrolled")) : (yield* o`
|
|
105
|
+
UPDATE ${p().usersTable}
|
|
106
|
+
SET ${u("mfaEnrolledAt")} = ${/* @__PURE__ */ new Date()}
|
|
107
|
+
WHERE ${u("id")} = ${e}
|
|
108
|
+
`.pipe(r.catchTag("SqlError", (e) => r.die(e))), yield* y(e));
|
|
109
|
+
}), C = (e) => r.gen(function* () {
|
|
110
|
+
return l.mfaSecret === !1 ? yield* r.die(m("mfaSecret", "clearMfa")) : l.mfaEnrolledAt === !1 ? yield* r.die(m("mfaEnrolledAt", "clearMfa")) : (yield* o`
|
|
111
|
+
UPDATE ${p().usersTable}
|
|
112
|
+
SET ${u("mfaSecret")} = ${null}, ${u("mfaEnrolledAt")} = ${null}
|
|
113
|
+
WHERE ${u("id")} = ${e}
|
|
114
|
+
`.pipe(r.catchTag("SqlError", (e) => r.die(e))), yield* y(e));
|
|
115
|
+
}), w = (e) => r.gen(function* () {
|
|
116
|
+
return l.emailVerifiedAt === !1 ? yield* r.die(m("emailVerifiedAt", "markEmailVerified")) : (yield* o`
|
|
117
|
+
UPDATE ${p().usersTable}
|
|
118
|
+
SET ${u("emailVerifiedAt")} = ${/* @__PURE__ */ new Date()}${h(/* @__PURE__ */ new Date())}
|
|
119
|
+
WHERE ${u("id")} = ${e} AND ${u("emailVerifiedAt")} IS NULL
|
|
120
|
+
`.pipe(r.catchTag("SqlError", (e) => r.die(e))), yield* y(e));
|
|
121
|
+
}), T = (e) => r.gen(function* () {
|
|
122
|
+
return (yield* o`
|
|
123
|
+
SELECT "userId", "tenantId", role, "joinedAt"
|
|
124
|
+
FROM memberships WHERE "userId" = ${e}
|
|
125
|
+
ORDER BY "joinedAt" ASC
|
|
126
|
+
`).map((e) => ({ ...e }));
|
|
127
|
+
}).pipe(r.orElseSucceed(() => [])), E = (e) => r.gen(function* () {
|
|
128
|
+
let t = /* @__PURE__ */ new Date();
|
|
129
|
+
yield* o`
|
|
130
|
+
UPDATE memberships SET role = ${e.role}
|
|
131
|
+
WHERE "userId" = ${e.userId} AND "tenantId" = ${e.tenantId}
|
|
132
|
+
`;
|
|
133
|
+
let r = yield* o`
|
|
134
|
+
SELECT "userId", "tenantId", role, "joinedAt"
|
|
135
|
+
FROM memberships
|
|
136
|
+
WHERE "userId" = ${e.userId} AND "tenantId" = ${e.tenantId}
|
|
137
|
+
LIMIT 1
|
|
138
|
+
`;
|
|
139
|
+
return r[0] ? { ...r[0] } : (yield* o`
|
|
140
|
+
INSERT INTO memberships (id, "userId", "tenantId", role, "joinedAt")
|
|
141
|
+
VALUES (${n("mem")}, ${e.userId}, ${e.tenantId}, ${e.role}, ${t})
|
|
142
|
+
`, {
|
|
143
|
+
...e,
|
|
144
|
+
joinedAt: t
|
|
145
|
+
});
|
|
146
|
+
}).pipe(r.catch(() => r.succeed({
|
|
147
|
+
...e,
|
|
148
|
+
joinedAt: /* @__PURE__ */ new Date()
|
|
149
|
+
}))), D = (e, t) => r.gen(function* () {
|
|
150
|
+
return (yield* o`
|
|
151
|
+
SELECT role FROM memberships
|
|
152
|
+
WHERE "userId" = ${e} AND "tenantId" = ${t} LIMIT 1
|
|
153
|
+
`)[0]?.role ?? null;
|
|
154
|
+
}).pipe(r.orElseSucceed(() => null)), O = (e) => r.gen(function* () {
|
|
155
|
+
return (yield* o`
|
|
156
|
+
SELECT credential.id, credential."userId", credential."tenantId", credential."expiresAt", credential."ipAddress", credential."userAgent", credential."createdAt", credential."lastSeenAt", credential."familyId", credential."rotatedAt"
|
|
157
|
+
FROM sessions AS credential
|
|
158
|
+
INNER JOIN sessions AS root
|
|
159
|
+
ON root.id = COALESCE(credential."familyId", credential.id)
|
|
160
|
+
AND root."userId" = credential."userId"
|
|
161
|
+
AND COALESCE(root."familyId", root.id) = root.id
|
|
162
|
+
WHERE credential."userId" = ${e} AND credential."rotatedAt" IS NULL
|
|
163
|
+
AND credential.id <> '' AND credential."userId" <> '' AND root.id <> ''
|
|
164
|
+
ORDER BY credential."createdAt" DESC
|
|
165
|
+
`).map((e) => ({ ...e }));
|
|
166
|
+
}).pipe(r.orElseSucceed(() => [])), k = (e) => r.gen(function* () {
|
|
167
|
+
let t = /* @__PURE__ */ new Date();
|
|
168
|
+
return yield* o`
|
|
169
|
+
INSERT INTO sessions (id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt", "familyId", "rotatedAt")
|
|
170
|
+
VALUES (
|
|
171
|
+
${e.id}, ${e.userId}, ${e.tenantId}, ${e.expiresAt},
|
|
172
|
+
${e.ipAddress ?? null}, ${e.userAgent ?? null}, ${t}, ${t}, ${e.familyId ?? e.id}, ${e.rotatedAt ?? null}
|
|
173
|
+
)
|
|
174
|
+
`, {
|
|
175
|
+
...e,
|
|
176
|
+
familyId: e.familyId ?? e.id,
|
|
177
|
+
rotatedAt: e.rotatedAt ?? null,
|
|
178
|
+
createdAt: t,
|
|
179
|
+
lastSeenAt: t
|
|
180
|
+
};
|
|
181
|
+
}).pipe(r.orDie), A = (e) => r.gen(function* () {
|
|
182
|
+
let t = yield* o`
|
|
183
|
+
SELECT credential.id, credential."userId", credential."tenantId", credential."expiresAt", credential."ipAddress", credential."userAgent", credential."createdAt", credential."lastSeenAt", credential."familyId", credential."rotatedAt"
|
|
184
|
+
FROM sessions AS credential
|
|
185
|
+
INNER JOIN sessions AS root
|
|
186
|
+
ON root.id = COALESCE(credential."familyId", credential.id)
|
|
187
|
+
AND root."userId" = credential."userId"
|
|
188
|
+
AND COALESCE(root."familyId", root.id) = root.id
|
|
189
|
+
WHERE credential.id = ${e} AND credential."rotatedAt" IS NULL
|
|
190
|
+
AND credential.id <> '' AND credential."userId" <> '' AND root.id <> '' LIMIT 1
|
|
191
|
+
`;
|
|
192
|
+
return t[0] ? { ...t[0] } : null;
|
|
193
|
+
}).pipe(r.orElseSucceed(() => null)), j = (e, t) => o.withTransaction(r.gen(function* () {
|
|
194
|
+
let n = (yield* o`SELECT COALESCE("familyId", id) AS family, "userId" FROM sessions WHERE id = ${e}`)[0];
|
|
195
|
+
n && (yield* o`SELECT id FROM sessions WHERE id = ${n.family} AND "userId" = ${n.userId} FOR UPDATE`).length && (yield* o`UPDATE sessions SET "expiresAt" = ${t}, "lastSeenAt" = ${/* @__PURE__ */ new Date()} WHERE id = ${e} AND "userId" = ${n.userId} AND "rotatedAt" IS NULL RETURNING id`).length && (yield* o`UPDATE sessions SET "expiresAt" = GREATEST("expiresAt", ${t}) WHERE id = ${n.family} AND "userId" = ${n.userId}`);
|
|
196
|
+
})).pipe(r.asVoid, r.orDie), M = (e, t) => o.withTransaction(r.gen(function* () {
|
|
197
|
+
let n = (yield* o`SELECT COALESCE("familyId", id) AS family FROM sessions WHERE id = ${t} AND "userId" = ${e}`)[0]?.family;
|
|
198
|
+
return !n || !(yield* o`SELECT id FROM sessions WHERE id = ${n} AND "userId" = ${e} FOR UPDATE`).length ? !1 : (yield* o`DELETE FROM sessions WHERE "userId" = ${e} AND COALESCE("familyId", id) = ${n} RETURNING id`).length > 0;
|
|
199
|
+
})).pipe(r.orDie), N = (e, t) => o.withTransaction(r.gen(function* () {
|
|
200
|
+
let n = (yield* o`SELECT COALESCE("familyId", id) AS family FROM sessions WHERE id = ${t} AND "userId" = ${e}`)[0]?.family ?? "";
|
|
201
|
+
return yield* o`SELECT id FROM sessions WHERE "userId" = ${e} AND id = COALESCE("familyId", id) AND id <> ${n} ORDER BY id FOR UPDATE`, (yield* o`DELETE FROM sessions WHERE "userId" = ${e} AND COALESCE("familyId", id) <> ${n} RETURNING "rotatedAt"`).filter((e) => e.rotatedAt === null).length;
|
|
202
|
+
})).pipe(r.orDie), P = (e) => o.withTransaction(r.gen(function* () {
|
|
203
|
+
return yield* o`SELECT id FROM sessions WHERE "userId" = ${e} AND id = COALESCE("familyId", id) ORDER BY id FOR UPDATE`, (yield* o`DELETE FROM sessions WHERE "userId" = ${e} RETURNING "rotatedAt"`).filter((e) => e.rotatedAt === null).length;
|
|
204
|
+
})).pipe(r.orDie), te = (e) => r.gen(function* () {
|
|
205
|
+
let t = /* @__PURE__ */ new Date();
|
|
206
|
+
return yield* o`
|
|
207
|
+
INSERT INTO "authTokens" (id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt")
|
|
208
|
+
VALUES (${e.id}, ${e.tokenHash}, ${e.userId}, ${e.purpose}, ${e.expiresAt}, ${null}, ${t})
|
|
209
|
+
`, {
|
|
210
|
+
...e,
|
|
211
|
+
consumedAt: null,
|
|
212
|
+
createdAt: t
|
|
213
|
+
};
|
|
214
|
+
}).pipe(r.catch(() => r.succeed({
|
|
215
|
+
...e,
|
|
216
|
+
consumedAt: null,
|
|
217
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
218
|
+
}))), F = (e, t) => r.gen(function* () {
|
|
219
|
+
let n = yield* o`
|
|
220
|
+
UPDATE "authTokens"
|
|
221
|
+
SET "consumedAt" = ${/* @__PURE__ */ new Date()}
|
|
222
|
+
WHERE "tokenHash" = ${e}
|
|
223
|
+
AND purpose = ${t}
|
|
224
|
+
AND "consumedAt" IS NULL
|
|
225
|
+
AND "expiresAt" > ${/* @__PURE__ */ new Date()}
|
|
226
|
+
RETURNING id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt"
|
|
227
|
+
`;
|
|
228
|
+
return n[0] ? { ...n[0] } : null;
|
|
229
|
+
}).pipe(r.orElseSucceed(() => null)), I = (e, t) => r.gen(function* () {
|
|
230
|
+
let n = yield* o`
|
|
231
|
+
SELECT id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt"
|
|
232
|
+
FROM "authTokens"
|
|
233
|
+
WHERE "userId" = ${e} AND purpose = ${t}
|
|
234
|
+
ORDER BY "createdAt" DESC
|
|
235
|
+
LIMIT 1
|
|
236
|
+
`;
|
|
237
|
+
return n[0] ? { ...n[0] } : null;
|
|
238
|
+
}).pipe(r.orElseSucceed(() => null)), L = (e) => r.gen(function* () {
|
|
239
|
+
let t = /* @__PURE__ */ new Date(), n = e.email.toLowerCase();
|
|
240
|
+
return yield* o`
|
|
241
|
+
INSERT INTO invitations (
|
|
242
|
+
id, "tokenHash", email, "tenantId", role, "invitedByUserId",
|
|
243
|
+
"expiresAt", "acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
|
|
244
|
+
) VALUES (
|
|
245
|
+
${e.id}, ${e.tokenHash}, ${n}, ${e.tenantId},
|
|
246
|
+
${e.role}, ${e.invitedByUserId}, ${e.expiresAt},
|
|
247
|
+
${null}, ${null}, ${null}, ${null}, ${t}
|
|
248
|
+
)
|
|
249
|
+
`, {
|
|
250
|
+
...e,
|
|
251
|
+
email: n,
|
|
252
|
+
acceptedAt: null,
|
|
253
|
+
acceptedByUserId: null,
|
|
254
|
+
revokedAt: null,
|
|
255
|
+
revokedByUserId: null,
|
|
256
|
+
createdAt: t
|
|
257
|
+
};
|
|
258
|
+
}).pipe(r.catch((e) => r.die(e))), R = (e) => r.gen(function* () {
|
|
259
|
+
let t = yield* o`
|
|
260
|
+
SELECT id, "tokenHash", email, "tenantId", role, "invitedByUserId", "expiresAt",
|
|
261
|
+
"acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
|
|
262
|
+
FROM invitations WHERE "tokenHash" = ${e} LIMIT 1
|
|
263
|
+
`;
|
|
264
|
+
return t[0] ? { ...t[0] } : null;
|
|
265
|
+
}).pipe(r.orElseSucceed(() => null)), z = (e, t, n) => r.gen(function* () {
|
|
266
|
+
let r = yield* o`
|
|
267
|
+
UPDATE invitations
|
|
268
|
+
SET "acceptedAt" = ${n}, "acceptedByUserId" = ${t}
|
|
269
|
+
WHERE "tokenHash" = ${e}
|
|
270
|
+
AND "acceptedAt" IS NULL
|
|
271
|
+
AND "revokedAt" IS NULL
|
|
272
|
+
AND "expiresAt" > ${n}
|
|
273
|
+
RETURNING id, "tokenHash", email, "tenantId", role, "invitedByUserId", "expiresAt",
|
|
274
|
+
"acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
|
|
275
|
+
`;
|
|
276
|
+
return r[0] ? { ...r[0] } : null;
|
|
277
|
+
}).pipe(r.catch((e) => r.die(e))), B = (e, t, n, i) => r.gen(function* () {
|
|
278
|
+
return (yield* o`
|
|
279
|
+
UPDATE invitations
|
|
280
|
+
SET "revokedAt" = ${i}, "revokedByUserId" = ${n}
|
|
281
|
+
WHERE id = ${e}
|
|
282
|
+
AND "tenantId" = ${t}
|
|
283
|
+
AND "acceptedAt" IS NULL
|
|
284
|
+
AND "revokedAt" IS NULL
|
|
285
|
+
RETURNING id
|
|
286
|
+
`).length > 0;
|
|
287
|
+
}).pipe(r.catch((e) => r.die(e))), V = (e) => r.gen(function* () {
|
|
288
|
+
return (yield* o`
|
|
289
|
+
SELECT id, "tokenHash", email, "tenantId", role, "invitedByUserId", "expiresAt",
|
|
290
|
+
"acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
|
|
291
|
+
FROM invitations WHERE "tenantId" = ${e}
|
|
292
|
+
ORDER BY "createdAt" DESC
|
|
293
|
+
`).map((e) => ({ ...e }));
|
|
294
|
+
}).pipe(r.orElseSucceed(() => [])), H = (e, t) => r.gen(function* () {
|
|
295
|
+
return (yield* o`
|
|
296
|
+
SELECT COUNT(*)::int AS n FROM invitations
|
|
297
|
+
WHERE "tenantId" = ${e}
|
|
298
|
+
AND "acceptedAt" IS NULL AND "revokedAt" IS NULL AND "expiresAt" > ${t}
|
|
299
|
+
`)[0]?.n ?? 0;
|
|
300
|
+
}).pipe(r.catch((e) => r.die(e))), U = (e, t, n, i) => r.gen(function* () {
|
|
301
|
+
return (yield* o`
|
|
302
|
+
UPDATE invitations
|
|
303
|
+
SET "revokedAt" = ${i}, "revokedByUserId" = ${n}
|
|
304
|
+
WHERE "tenantId" = ${e}
|
|
305
|
+
AND email = ${t.toLowerCase()}
|
|
306
|
+
AND "acceptedAt" IS NULL
|
|
307
|
+
AND "revokedAt" IS NULL
|
|
308
|
+
RETURNING id
|
|
309
|
+
`).length;
|
|
310
|
+
}).pipe(r.catch((e) => r.die(e))), W = (e) => r.gen(function* () {
|
|
311
|
+
let t = /* @__PURE__ */ new Date();
|
|
312
|
+
return yield* o`
|
|
313
|
+
INSERT INTO "impersonationGrants" (
|
|
314
|
+
id, "sessionId", "actorUserId", "actorSessionId", "targetUserId", "tenantId",
|
|
315
|
+
reason, "startedAt", "expiresAt", "endedAt", "endedReason", "ipAddress", "userAgent"
|
|
316
|
+
) VALUES (
|
|
317
|
+
${e.id}, ${e.sessionId}, ${e.actorUserId}, ${e.actorSessionId},
|
|
318
|
+
${e.targetUserId}, ${e.tenantId}, ${e.reason ?? null}, ${t},
|
|
319
|
+
${e.expiresAt}, ${null}, ${null}, ${e.ipAddress ?? null}, ${e.userAgent ?? null}
|
|
320
|
+
)
|
|
321
|
+
`, {
|
|
322
|
+
...e,
|
|
323
|
+
startedAt: t,
|
|
324
|
+
endedAt: null,
|
|
325
|
+
endedReason: null
|
|
326
|
+
};
|
|
327
|
+
}).pipe(r.catch((e) => r.die(e))), G = (e) => r.gen(function* () {
|
|
328
|
+
let t = yield* o`
|
|
329
|
+
SELECT id, "sessionId", "actorUserId", "actorSessionId", "targetUserId", "tenantId",
|
|
330
|
+
reason, "startedAt", "expiresAt", "endedAt", "endedReason", "ipAddress", "userAgent"
|
|
331
|
+
FROM "impersonationGrants" WHERE "sessionId" = ${e} LIMIT 1
|
|
332
|
+
`;
|
|
333
|
+
return t[0] ? { ...t[0] } : null;
|
|
334
|
+
}).pipe(r.orElseSucceed(() => null)), K = (e, t, n) => r.gen(function* () {
|
|
335
|
+
return (yield* o`
|
|
336
|
+
UPDATE "impersonationGrants"
|
|
337
|
+
SET "endedAt" = ${n}, "endedReason" = ${t}
|
|
338
|
+
WHERE "sessionId" = ${e} AND "endedAt" IS NULL
|
|
339
|
+
RETURNING id
|
|
340
|
+
`).length > 0;
|
|
341
|
+
}).pipe(r.catch((e) => r.die(e))), q = (e) => r.gen(function* () {
|
|
342
|
+
return (yield* o`
|
|
343
|
+
SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
|
|
344
|
+
FROM passkeys WHERE "userId" = ${e}
|
|
345
|
+
`).map((e) => ({ ...e }));
|
|
346
|
+
}).pipe(r.orElseSucceed(() => [])), J = (e) => r.gen(function* () {
|
|
347
|
+
let t = yield* o`
|
|
348
|
+
SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
|
|
349
|
+
FROM passkeys WHERE "credentialId" = ${e} LIMIT 1
|
|
350
|
+
`;
|
|
351
|
+
return t[0] ? { ...t[0] } : null;
|
|
352
|
+
}).pipe(r.orElseSucceed(() => null)), Y = (e) => r.gen(function* () {
|
|
353
|
+
let t = /* @__PURE__ */ new Date();
|
|
354
|
+
return yield* o`
|
|
355
|
+
INSERT INTO passkeys (id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt")
|
|
356
|
+
VALUES (
|
|
357
|
+
${e.id}, ${e.credentialId}, ${e.userId}, ${e.publicKey},
|
|
358
|
+
${e.counter}, ${e.transports ?? null}, ${t}, ${null}
|
|
359
|
+
)
|
|
360
|
+
`, {
|
|
361
|
+
...e,
|
|
362
|
+
createdAt: t,
|
|
363
|
+
lastUsedAt: null
|
|
364
|
+
};
|
|
365
|
+
}).pipe(r.catch(() => r.succeed({
|
|
366
|
+
...e,
|
|
367
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
368
|
+
lastUsedAt: null
|
|
369
|
+
}))), X = (e, t) => r.gen(function* () {
|
|
370
|
+
return (yield* o`
|
|
371
|
+
UPDATE passkeys
|
|
372
|
+
SET counter = ${t}, "lastUsedAt" = ${/* @__PURE__ */ new Date()}
|
|
373
|
+
WHERE "credentialId" = ${e}
|
|
374
|
+
AND counter < ${t}
|
|
375
|
+
RETURNING id
|
|
376
|
+
`).length > 0;
|
|
377
|
+
}).pipe(r.orElseSucceed(() => !1)), Z = (e, t) => r.gen(function* () {
|
|
378
|
+
let r = /* @__PURE__ */ new Date();
|
|
379
|
+
yield* o`DELETE FROM "recoveryCodes" WHERE "userId" = ${e}`;
|
|
380
|
+
for (let i of t) yield* o`
|
|
381
|
+
INSERT INTO "recoveryCodes" (id, "userId", "codeHash", "consumedAt", "createdAt")
|
|
382
|
+
VALUES (${n("rc")}, ${e}, ${i}, ${null}, ${r})
|
|
383
|
+
`;
|
|
384
|
+
}).pipe(r.asVoid, r.orElseSucceed(() => void 0)), Q = (e, t) => r.gen(function* () {
|
|
385
|
+
return (yield* o`
|
|
386
|
+
UPDATE "recoveryCodes"
|
|
387
|
+
SET "consumedAt" = ${/* @__PURE__ */ new Date()}
|
|
388
|
+
WHERE "userId" = ${e}
|
|
389
|
+
AND "codeHash" = ${t}
|
|
390
|
+
AND "consumedAt" IS NULL
|
|
391
|
+
RETURNING id
|
|
392
|
+
`).length > 0;
|
|
393
|
+
}).pipe(r.orElseSucceed(() => !1)), ne = (e) => r.gen(function* () {
|
|
394
|
+
return (yield* o`
|
|
395
|
+
SELECT COUNT(*)::int AS n FROM "recoveryCodes"
|
|
396
|
+
WHERE "userId" = ${e} AND "consumedAt" IS NULL
|
|
397
|
+
`)[0]?.n ?? 0;
|
|
398
|
+
}).pipe(r.orElseSucceed(() => 0)), $ = (e, t) => r.catch((n) => r.logWarning(`[auth] lockout ${e} failed — brute-force protection is not counting`).pipe(r.annotateLogs("cause", String(n)), r.as(t)));
|
|
399
|
+
return {
|
|
400
|
+
findByEmail: _,
|
|
401
|
+
findById: v,
|
|
402
|
+
insert: ee,
|
|
403
|
+
updatePassword: b,
|
|
404
|
+
setMfaSecret: x,
|
|
405
|
+
markMfaEnrolled: S,
|
|
406
|
+
clearMfa: C,
|
|
407
|
+
markEmailVerified: w,
|
|
408
|
+
listMemberships: T,
|
|
409
|
+
addMembership: E,
|
|
410
|
+
membershipRole: D,
|
|
411
|
+
listSessions: O,
|
|
412
|
+
insertSession: k,
|
|
413
|
+
findSession: A,
|
|
414
|
+
touchSession: j,
|
|
415
|
+
revokeSession: M,
|
|
416
|
+
revokeAllOtherSessions: N,
|
|
417
|
+
revokeAllSessions: P,
|
|
418
|
+
insertToken: te,
|
|
419
|
+
consumeToken: F,
|
|
420
|
+
latestToken: I,
|
|
421
|
+
insertInvitation: L,
|
|
422
|
+
findInvitationByHash: R,
|
|
423
|
+
acceptInvitation: z,
|
|
424
|
+
revokeInvitation: B,
|
|
425
|
+
listInvitations: V,
|
|
426
|
+
countPendingInvitations: H,
|
|
427
|
+
supersedePendingInvitations: U,
|
|
428
|
+
insertImpersonationGrant: W,
|
|
429
|
+
findImpersonationGrantBySession: G,
|
|
430
|
+
endImpersonationGrant: K,
|
|
431
|
+
replaceRecoveryCodes: Z,
|
|
432
|
+
consumeRecoveryCode: Q,
|
|
433
|
+
countRecoveryCodes: ne,
|
|
434
|
+
listPasskeys: q,
|
|
435
|
+
findPasskey: J,
|
|
436
|
+
insertPasskey: Y,
|
|
437
|
+
advancePasskeyCounter: X,
|
|
438
|
+
isLockedOut: (e, t) => r.gen(function* () {
|
|
439
|
+
let n = (yield* o`
|
|
440
|
+
SELECT "lockedUntil" FROM "loginAttempts"
|
|
441
|
+
WHERE email = ${e.toLowerCase()} LIMIT 1
|
|
442
|
+
`)[0]?.lockedUntil ?? null;
|
|
443
|
+
return n != null && t < n.getTime() ? {
|
|
444
|
+
locked: !0,
|
|
445
|
+
retryAfterSeconds: Math.ceil((n.getTime() - t) / 1e3)
|
|
446
|
+
} : {
|
|
447
|
+
locked: !1,
|
|
448
|
+
retryAfterSeconds: 0
|
|
449
|
+
};
|
|
450
|
+
}).pipe($("read", {
|
|
451
|
+
locked: !1,
|
|
452
|
+
retryAfterSeconds: 0
|
|
453
|
+
})),
|
|
454
|
+
recordLoginFailure: (e, t, i) => r.gen(function* () {
|
|
455
|
+
let r = e.toLowerCase(), a = (yield* o`
|
|
456
|
+
SELECT "failedCount", "windowStartedAt", "lockedUntil"
|
|
457
|
+
FROM "loginAttempts" WHERE email = ${r} LIMIT 1
|
|
458
|
+
`)[0], s = a?.windowStartedAt?.getTime() ?? null, c = a?.lockedUntil?.getTime() ?? null, l = s == null || t - s >= i.windowMs, u = !a || l || c != null && t >= c, d = u ? 1 : a.failedCount + 1, f = u ? new Date(t) : new Date(s), p = d >= i.maxAttempts ? new Date(t + i.lockMs) : null, m = new Date(t);
|
|
459
|
+
yield* o`
|
|
460
|
+
INSERT INTO "loginAttempts" (id, email, "failedCount", "windowStartedAt", "lockedUntil", "createdAt", "updatedAt")
|
|
461
|
+
VALUES (${n("la")}, ${r}, ${d}, ${f}, ${p}, ${m}, ${m})
|
|
462
|
+
ON CONFLICT (email) DO UPDATE SET
|
|
463
|
+
"failedCount" = ${d},
|
|
464
|
+
"windowStartedAt" = ${f},
|
|
465
|
+
"lockedUntil" = ${p},
|
|
466
|
+
"updatedAt" = ${m}
|
|
467
|
+
`;
|
|
468
|
+
}).pipe(r.asVoid, $("record", void 0)),
|
|
469
|
+
clearLoginFailures: (e) => r.gen(function* () {
|
|
470
|
+
yield* o`DELETE FROM "loginAttempts" WHERE email = ${e.toLowerCase()}`;
|
|
471
|
+
}).pipe(r.asVoid, $("clear", void 0))
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
//#endregion
|
|
475
|
+
export { o as postgresUserStore };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isSessionCredentialLiveFor as e } from "./sessionFamily.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
//#region src/revocation.ts
|
|
4
|
+
var n = 3e4, r = 1e4, i = (e) => {
|
|
5
|
+
if (typeof e != "object" || !e) throw TypeError("Session revocation options must be an object.");
|
|
6
|
+
if ("isLive" in e || "isLiveFor" in e || "invalidate" in e || Reflect.ownKeys(e).some((e) => typeof e != "string" || ![
|
|
7
|
+
"ttlMs",
|
|
8
|
+
"maxEntries",
|
|
9
|
+
"now"
|
|
10
|
+
].includes(e))) throw TypeError("Supply revocation options or a checker implementing isLiveFor(sessionId, userId), invalidate(sessionId) and ttlMs.");
|
|
11
|
+
if (e.ttlMs !== void 0 && (!Number.isFinite(e.ttlMs) || e.ttlMs < 0)) throw TypeError("Session revocation ttlMs must be finite and nonnegative.");
|
|
12
|
+
if (e.maxEntries !== void 0 && (!Number.isSafeInteger(e.maxEntries) || e.maxEntries < 0)) throw TypeError("Session revocation maxEntries must be a nonnegative safe integer.");
|
|
13
|
+
if (e.now !== void 0 && typeof e.now != "function") throw TypeError("Session revocation now must be a clock function.");
|
|
14
|
+
}, a = (a, o = {}) => {
|
|
15
|
+
i(o);
|
|
16
|
+
let s = o.ttlMs ?? n, c = o.maxEntries ?? r, l = o.now ?? Date.now, u = /* @__PURE__ */ new Map(), d = () => {
|
|
17
|
+
if (u.size <= c) return;
|
|
18
|
+
let e = l();
|
|
19
|
+
for (let [t, n] of u) n.freshUntil <= e && u.delete(t);
|
|
20
|
+
for (let e of u.keys()) {
|
|
21
|
+
if (u.size <= c) break;
|
|
22
|
+
u.delete(e);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
ttlMs: s,
|
|
27
|
+
invalidate: (e) => {
|
|
28
|
+
u.delete(e);
|
|
29
|
+
},
|
|
30
|
+
isLiveFor: (n, r) => t.gen(function* () {
|
|
31
|
+
if (typeof n != "string" || n.length === 0 || typeof r != "string" || r.length === 0) return !1;
|
|
32
|
+
let t = l();
|
|
33
|
+
if (!Number.isFinite(t)) return !1;
|
|
34
|
+
if (s > 0) {
|
|
35
|
+
let e = u.get(n);
|
|
36
|
+
if (e && e.freshUntil > t) return e.live && e.userId === r;
|
|
37
|
+
}
|
|
38
|
+
let i = yield* a.findSession(n), o = l();
|
|
39
|
+
if (!Number.isFinite(o)) return !1;
|
|
40
|
+
let f = i !== null && i.id === n && typeof i.userId == "string" && i.userId.length > 0 ? i.userId : null, p = i?.expiresAt instanceof Date ? i.expiresAt.getTime() : NaN, m = f !== null && e(i, n, f, o);
|
|
41
|
+
return s > 0 && c > 0 && (u.set(n, {
|
|
42
|
+
userId: f,
|
|
43
|
+
live: m,
|
|
44
|
+
freshUntil: m ? Math.min(o + s, p) : o + s
|
|
45
|
+
}), d()), m && f === r;
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { a as makeSessionRevocationChecker, i as validateSessionRevocationOptions };
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
+
import { ColumnBuilder } from '@voltro/database';
|
|
2
|
+
import { FieldDefinitions } from '@voltro/database';
|
|
3
|
+
import { Table } from '@voltro/database';
|
|
1
4
|
import { TableLike } from '@voltro/database';
|
|
2
5
|
|
|
3
|
-
/**
|
|
6
|
+
/** Device credentials never appear in SQL, logs, exports or session claims. */
|
|
7
|
+
export declare const authGuestDevicesTable: Table<"authGuestDevices", FieldDefinitions<{
|
|
8
|
+
readonly id: ColumnBuilder<string, "id", boolean>;
|
|
9
|
+
readonly guestId: ColumnBuilder<string, "text", boolean>;
|
|
10
|
+
readonly tenantId: ColumnBuilder<string, "text", boolean>;
|
|
11
|
+
readonly secretHash: ColumnBuilder<string, "text", boolean>;
|
|
12
|
+
readonly generation: ColumnBuilder<number, "integer", boolean>;
|
|
13
|
+
readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
14
|
+
readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
15
|
+
}>, true, never>;
|
|
16
|
+
|
|
17
|
+
/** Guest identity is independent of the password user's email record. */
|
|
18
|
+
export declare const authGuestsTable: Table<"authGuests", FieldDefinitions<{
|
|
19
|
+
readonly id: ColumnBuilder<string, "id", boolean>;
|
|
20
|
+
readonly tenantId: ColumnBuilder<string, "text", boolean>;
|
|
21
|
+
readonly upgradeable: ColumnBuilder<boolean, "boolean", boolean>;
|
|
22
|
+
readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
23
|
+
readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
24
|
+
readonly lastSeenAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
25
|
+
}>, true, never>;
|
|
26
|
+
|
|
4
27
|
export declare const authTables: readonly [TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike];
|
|
5
28
|
|
|
6
29
|
/**
|
|
@@ -12,6 +35,23 @@ export declare const authTables: readonly [TableLike, TableLike, TableLike, Tabl
|
|
|
12
35
|
*/
|
|
13
36
|
export declare const authTokensTable: TableLike;
|
|
14
37
|
|
|
38
|
+
export declare const guestTables: readonly [ Table<"authGuests", FieldDefinitions<{
|
|
39
|
+
readonly id: ColumnBuilder<string, "id", boolean>;
|
|
40
|
+
readonly tenantId: ColumnBuilder<string, "text", boolean>;
|
|
41
|
+
readonly upgradeable: ColumnBuilder<boolean, "boolean", boolean>;
|
|
42
|
+
readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
43
|
+
readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
44
|
+
readonly lastSeenAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
45
|
+
}>, true, never>, Table<"authGuestDevices", FieldDefinitions<{
|
|
46
|
+
readonly id: ColumnBuilder<string, "id", boolean>;
|
|
47
|
+
readonly guestId: ColumnBuilder<string, "text", boolean>;
|
|
48
|
+
readonly tenantId: ColumnBuilder<string, "text", boolean>;
|
|
49
|
+
readonly secretHash: ColumnBuilder<string, "text", boolean>;
|
|
50
|
+
readonly generation: ColumnBuilder<number, "integer", boolean>;
|
|
51
|
+
readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
52
|
+
readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
53
|
+
}>, true, never>];
|
|
54
|
+
|
|
15
55
|
/**
|
|
16
56
|
* Impersonation grants — one row per "support agent A is acting as user B",
|
|
17
57
|
* written when the impersonated session is minted and closed when it ends.
|
|
@@ -108,11 +148,10 @@ export declare const passkeysTable: TableLike;
|
|
|
108
148
|
export declare const recoveryCodesTable: TableLike;
|
|
109
149
|
|
|
110
150
|
/**
|
|
111
|
-
* Sessions table
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* wire that up themselves.
|
|
151
|
+
* Sessions table used by the auth handlers and custom registered sessions.
|
|
152
|
+
* Rotation retains inactive aliases in a device family so sign-out using an
|
|
153
|
+
* earlier cookie also revokes its current successor. Only unrotated rows
|
|
154
|
+
* authenticate or appear in device lists.
|
|
116
155
|
*/
|
|
117
156
|
export declare const sessionsTable: TableLike;
|
|
118
157
|
|