@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,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
- /** Tables the plugin contributes, as an array. */
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. Active sessions enumerated server-side so apps can
112
- * implement "sign out other devices" + server-side revocation.
113
- * `handleSignIn` / `handleSignOut` do NOT write here (cookies are
114
- * self-describing HMAC-signed); apps that need device management
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