@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,463 @@
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.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" ? 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 id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt", "familyId", "rotatedAt"
157
+ FROM sessions WHERE "userId" = ${e} AND "rotatedAt" IS NULL
158
+ ORDER BY "createdAt" DESC
159
+ `).map((e) => ({ ...e }));
160
+ }).pipe(r.orElseSucceed(() => [])), k = (e) => r.gen(function* () {
161
+ let t = /* @__PURE__ */ new Date();
162
+ return yield* o`
163
+ INSERT INTO sessions (id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt", "familyId", "rotatedAt")
164
+ VALUES (
165
+ ${e.id}, ${e.userId}, ${e.tenantId}, ${e.expiresAt},
166
+ ${e.ipAddress ?? null}, ${e.userAgent ?? null}, ${t}, ${t}, ${e.familyId ?? e.id}, ${e.rotatedAt ?? null}
167
+ )
168
+ `, {
169
+ ...e,
170
+ familyId: e.familyId ?? e.id,
171
+ rotatedAt: e.rotatedAt ?? null,
172
+ createdAt: t,
173
+ lastSeenAt: t
174
+ };
175
+ }).pipe(r.orDie), A = (e) => r.gen(function* () {
176
+ let t = yield* o`
177
+ SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt", "familyId", "rotatedAt"
178
+ FROM sessions WHERE id = ${e} AND "rotatedAt" IS NULL LIMIT 1
179
+ `;
180
+ return t[0] ? { ...t[0] } : null;
181
+ }).pipe(r.orElseSucceed(() => null)), j = (e, t) => o.withTransaction(r.gen(function* () {
182
+ let n = (yield* o`SELECT COALESCE("familyId", id) AS family, "userId" FROM sessions WHERE id = ${e}`)[0];
183
+ 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}`);
184
+ })).pipe(r.asVoid, r.orDie), M = (e, t) => o.withTransaction(r.gen(function* () {
185
+ let n = (yield* o`SELECT COALESCE("familyId", id) AS family FROM sessions WHERE id = ${t} AND "userId" = ${e}`)[0]?.family;
186
+ 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;
187
+ })).pipe(r.orDie), N = (e, t) => o.withTransaction(r.gen(function* () {
188
+ let n = (yield* o`SELECT COALESCE("familyId", id) AS family FROM sessions WHERE id = ${t} AND "userId" = ${e}`)[0]?.family ?? "";
189
+ 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;
190
+ })).pipe(r.orDie), P = (e) => o.withTransaction(r.gen(function* () {
191
+ 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;
192
+ })).pipe(r.orDie), te = (e) => r.gen(function* () {
193
+ let t = /* @__PURE__ */ new Date();
194
+ return yield* o`
195
+ INSERT INTO "authTokens" (id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt")
196
+ VALUES (${e.id}, ${e.tokenHash}, ${e.userId}, ${e.purpose}, ${e.expiresAt}, ${null}, ${t})
197
+ `, {
198
+ ...e,
199
+ consumedAt: null,
200
+ createdAt: t
201
+ };
202
+ }).pipe(r.catch(() => r.succeed({
203
+ ...e,
204
+ consumedAt: null,
205
+ createdAt: /* @__PURE__ */ new Date()
206
+ }))), F = (e, t) => r.gen(function* () {
207
+ let n = yield* o`
208
+ UPDATE "authTokens"
209
+ SET "consumedAt" = ${/* @__PURE__ */ new Date()}
210
+ WHERE "tokenHash" = ${e}
211
+ AND purpose = ${t}
212
+ AND "consumedAt" IS NULL
213
+ AND "expiresAt" > ${/* @__PURE__ */ new Date()}
214
+ RETURNING id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt"
215
+ `;
216
+ return n[0] ? { ...n[0] } : null;
217
+ }).pipe(r.orElseSucceed(() => null)), I = (e, t) => r.gen(function* () {
218
+ let n = yield* o`
219
+ SELECT id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt"
220
+ FROM "authTokens"
221
+ WHERE "userId" = ${e} AND purpose = ${t}
222
+ ORDER BY "createdAt" DESC
223
+ LIMIT 1
224
+ `;
225
+ return n[0] ? { ...n[0] } : null;
226
+ }).pipe(r.orElseSucceed(() => null)), L = (e) => r.gen(function* () {
227
+ let t = /* @__PURE__ */ new Date(), n = e.email.toLowerCase();
228
+ return yield* o`
229
+ INSERT INTO invitations (
230
+ id, "tokenHash", email, "tenantId", role, "invitedByUserId",
231
+ "expiresAt", "acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
232
+ ) VALUES (
233
+ ${e.id}, ${e.tokenHash}, ${n}, ${e.tenantId},
234
+ ${e.role}, ${e.invitedByUserId}, ${e.expiresAt},
235
+ ${null}, ${null}, ${null}, ${null}, ${t}
236
+ )
237
+ `, {
238
+ ...e,
239
+ email: n,
240
+ acceptedAt: null,
241
+ acceptedByUserId: null,
242
+ revokedAt: null,
243
+ revokedByUserId: null,
244
+ createdAt: t
245
+ };
246
+ }).pipe(r.catch((e) => r.die(e))), R = (e) => r.gen(function* () {
247
+ let t = yield* o`
248
+ SELECT id, "tokenHash", email, "tenantId", role, "invitedByUserId", "expiresAt",
249
+ "acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
250
+ FROM invitations WHERE "tokenHash" = ${e} LIMIT 1
251
+ `;
252
+ return t[0] ? { ...t[0] } : null;
253
+ }).pipe(r.orElseSucceed(() => null)), z = (e, t, n) => r.gen(function* () {
254
+ let r = yield* o`
255
+ UPDATE invitations
256
+ SET "acceptedAt" = ${n}, "acceptedByUserId" = ${t}
257
+ WHERE "tokenHash" = ${e}
258
+ AND "acceptedAt" IS NULL
259
+ AND "revokedAt" IS NULL
260
+ AND "expiresAt" > ${n}
261
+ RETURNING id, "tokenHash", email, "tenantId", role, "invitedByUserId", "expiresAt",
262
+ "acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
263
+ `;
264
+ return r[0] ? { ...r[0] } : null;
265
+ }).pipe(r.catch((e) => r.die(e))), B = (e, t, n, i) => r.gen(function* () {
266
+ return (yield* o`
267
+ UPDATE invitations
268
+ SET "revokedAt" = ${i}, "revokedByUserId" = ${n}
269
+ WHERE id = ${e}
270
+ AND "tenantId" = ${t}
271
+ AND "acceptedAt" IS NULL
272
+ AND "revokedAt" IS NULL
273
+ RETURNING id
274
+ `).length > 0;
275
+ }).pipe(r.catch((e) => r.die(e))), V = (e) => r.gen(function* () {
276
+ return (yield* o`
277
+ SELECT id, "tokenHash", email, "tenantId", role, "invitedByUserId", "expiresAt",
278
+ "acceptedAt", "acceptedByUserId", "revokedAt", "revokedByUserId", "createdAt"
279
+ FROM invitations WHERE "tenantId" = ${e}
280
+ ORDER BY "createdAt" DESC
281
+ `).map((e) => ({ ...e }));
282
+ }).pipe(r.orElseSucceed(() => [])), H = (e, t) => r.gen(function* () {
283
+ return (yield* o`
284
+ SELECT COUNT(*)::int AS n FROM invitations
285
+ WHERE "tenantId" = ${e}
286
+ AND "acceptedAt" IS NULL AND "revokedAt" IS NULL AND "expiresAt" > ${t}
287
+ `)[0]?.n ?? 0;
288
+ }).pipe(r.catch((e) => r.die(e))), U = (e, t, n, i) => r.gen(function* () {
289
+ return (yield* o`
290
+ UPDATE invitations
291
+ SET "revokedAt" = ${i}, "revokedByUserId" = ${n}
292
+ WHERE "tenantId" = ${e}
293
+ AND email = ${t.toLowerCase()}
294
+ AND "acceptedAt" IS NULL
295
+ AND "revokedAt" IS NULL
296
+ RETURNING id
297
+ `).length;
298
+ }).pipe(r.catch((e) => r.die(e))), W = (e) => r.gen(function* () {
299
+ let t = /* @__PURE__ */ new Date();
300
+ return yield* o`
301
+ INSERT INTO "impersonationGrants" (
302
+ id, "sessionId", "actorUserId", "actorSessionId", "targetUserId", "tenantId",
303
+ reason, "startedAt", "expiresAt", "endedAt", "endedReason", "ipAddress", "userAgent"
304
+ ) VALUES (
305
+ ${e.id}, ${e.sessionId}, ${e.actorUserId}, ${e.actorSessionId},
306
+ ${e.targetUserId}, ${e.tenantId}, ${e.reason ?? null}, ${t},
307
+ ${e.expiresAt}, ${null}, ${null}, ${e.ipAddress ?? null}, ${e.userAgent ?? null}
308
+ )
309
+ `, {
310
+ ...e,
311
+ startedAt: t,
312
+ endedAt: null,
313
+ endedReason: null
314
+ };
315
+ }).pipe(r.catch((e) => r.die(e))), G = (e) => r.gen(function* () {
316
+ let t = yield* o`
317
+ SELECT id, "sessionId", "actorUserId", "actorSessionId", "targetUserId", "tenantId",
318
+ reason, "startedAt", "expiresAt", "endedAt", "endedReason", "ipAddress", "userAgent"
319
+ FROM "impersonationGrants" WHERE "sessionId" = ${e} LIMIT 1
320
+ `;
321
+ return t[0] ? { ...t[0] } : null;
322
+ }).pipe(r.orElseSucceed(() => null)), K = (e, t, n) => r.gen(function* () {
323
+ return (yield* o`
324
+ UPDATE "impersonationGrants"
325
+ SET "endedAt" = ${n}, "endedReason" = ${t}
326
+ WHERE "sessionId" = ${e} AND "endedAt" IS NULL
327
+ RETURNING id
328
+ `).length > 0;
329
+ }).pipe(r.catch((e) => r.die(e))), q = (e) => r.gen(function* () {
330
+ return (yield* o`
331
+ SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
332
+ FROM passkeys WHERE "userId" = ${e}
333
+ `).map((e) => ({ ...e }));
334
+ }).pipe(r.orElseSucceed(() => [])), J = (e) => r.gen(function* () {
335
+ let t = yield* o`
336
+ SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
337
+ FROM passkeys WHERE "credentialId" = ${e} LIMIT 1
338
+ `;
339
+ return t[0] ? { ...t[0] } : null;
340
+ }).pipe(r.orElseSucceed(() => null)), Y = (e) => r.gen(function* () {
341
+ let t = /* @__PURE__ */ new Date();
342
+ return yield* o`
343
+ INSERT INTO passkeys (id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt")
344
+ VALUES (
345
+ ${e.id}, ${e.credentialId}, ${e.userId}, ${e.publicKey},
346
+ ${e.counter}, ${e.transports ?? null}, ${t}, ${null}
347
+ )
348
+ `, {
349
+ ...e,
350
+ createdAt: t,
351
+ lastUsedAt: null
352
+ };
353
+ }).pipe(r.catch(() => r.succeed({
354
+ ...e,
355
+ createdAt: /* @__PURE__ */ new Date(),
356
+ lastUsedAt: null
357
+ }))), X = (e, t) => r.gen(function* () {
358
+ return (yield* o`
359
+ UPDATE passkeys
360
+ SET counter = ${t}, "lastUsedAt" = ${/* @__PURE__ */ new Date()}
361
+ WHERE "credentialId" = ${e}
362
+ AND counter < ${t}
363
+ RETURNING id
364
+ `).length > 0;
365
+ }).pipe(r.orElseSucceed(() => !1)), Z = (e, t) => r.gen(function* () {
366
+ let r = /* @__PURE__ */ new Date();
367
+ yield* o`DELETE FROM "recoveryCodes" WHERE "userId" = ${e}`;
368
+ for (let i of t) yield* o`
369
+ INSERT INTO "recoveryCodes" (id, "userId", "codeHash", "consumedAt", "createdAt")
370
+ VALUES (${n("rc")}, ${e}, ${i}, ${null}, ${r})
371
+ `;
372
+ }).pipe(r.asVoid, r.orElseSucceed(() => void 0)), Q = (e, t) => r.gen(function* () {
373
+ return (yield* o`
374
+ UPDATE "recoveryCodes"
375
+ SET "consumedAt" = ${/* @__PURE__ */ new Date()}
376
+ WHERE "userId" = ${e}
377
+ AND "codeHash" = ${t}
378
+ AND "consumedAt" IS NULL
379
+ RETURNING id
380
+ `).length > 0;
381
+ }).pipe(r.orElseSucceed(() => !1)), ne = (e) => r.gen(function* () {
382
+ return (yield* o`
383
+ SELECT COUNT(*)::int AS n FROM "recoveryCodes"
384
+ WHERE "userId" = ${e} AND "consumedAt" IS NULL
385
+ `)[0]?.n ?? 0;
386
+ }).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)));
387
+ return {
388
+ findByEmail: _,
389
+ findById: v,
390
+ insert: ee,
391
+ updatePassword: b,
392
+ setMfaSecret: x,
393
+ markMfaEnrolled: S,
394
+ clearMfa: C,
395
+ markEmailVerified: w,
396
+ listMemberships: T,
397
+ addMembership: E,
398
+ membershipRole: D,
399
+ listSessions: O,
400
+ insertSession: k,
401
+ findSession: A,
402
+ touchSession: j,
403
+ revokeSession: M,
404
+ revokeAllOtherSessions: N,
405
+ revokeAllSessions: P,
406
+ insertToken: te,
407
+ consumeToken: F,
408
+ latestToken: I,
409
+ insertInvitation: L,
410
+ findInvitationByHash: R,
411
+ acceptInvitation: z,
412
+ revokeInvitation: B,
413
+ listInvitations: V,
414
+ countPendingInvitations: H,
415
+ supersedePendingInvitations: U,
416
+ insertImpersonationGrant: W,
417
+ findImpersonationGrantBySession: G,
418
+ endImpersonationGrant: K,
419
+ replaceRecoveryCodes: Z,
420
+ consumeRecoveryCode: Q,
421
+ countRecoveryCodes: ne,
422
+ listPasskeys: q,
423
+ findPasskey: J,
424
+ insertPasskey: Y,
425
+ advancePasskeyCounter: X,
426
+ isLockedOut: (e, t) => r.gen(function* () {
427
+ let n = (yield* o`
428
+ SELECT "lockedUntil" FROM "loginAttempts"
429
+ WHERE email = ${e.toLowerCase()} LIMIT 1
430
+ `)[0]?.lockedUntil ?? null;
431
+ return n != null && t < n.getTime() ? {
432
+ locked: !0,
433
+ retryAfterSeconds: Math.ceil((n.getTime() - t) / 1e3)
434
+ } : {
435
+ locked: !1,
436
+ retryAfterSeconds: 0
437
+ };
438
+ }).pipe($("read", {
439
+ locked: !1,
440
+ retryAfterSeconds: 0
441
+ })),
442
+ recordLoginFailure: (e, t, i) => r.gen(function* () {
443
+ let r = e.toLowerCase(), a = (yield* o`
444
+ SELECT "failedCount", "windowStartedAt", "lockedUntil"
445
+ FROM "loginAttempts" WHERE email = ${r} LIMIT 1
446
+ `)[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);
447
+ yield* o`
448
+ INSERT INTO "loginAttempts" (id, email, "failedCount", "windowStartedAt", "lockedUntil", "createdAt", "updatedAt")
449
+ VALUES (${n("la")}, ${r}, ${d}, ${f}, ${p}, ${m}, ${m})
450
+ ON CONFLICT (email) DO UPDATE SET
451
+ "failedCount" = ${d},
452
+ "windowStartedAt" = ${f},
453
+ "lockedUntil" = ${p},
454
+ "updatedAt" = ${m}
455
+ `;
456
+ }).pipe(r.asVoid, $("record", void 0)),
457
+ clearLoginFailures: (e) => r.gen(function* () {
458
+ yield* o`DELETE FROM "loginAttempts" WHERE email = ${e.toLowerCase()}`;
459
+ }).pipe(r.asVoid, $("clear", void 0))
460
+ };
461
+ };
462
+ //#endregion
463
+ export { o as postgresUserStore };
@@ -0,0 +1,33 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/revocation.ts
3
+ var t = 3e4, n = 1e4, r = (r, i = {}) => {
4
+ let a = i.ttlMs ?? t, o = i.maxEntries ?? n, s = i.now ?? Date.now, c = /* @__PURE__ */ new Map(), l = () => {
5
+ if (c.size <= o) return;
6
+ let e = s();
7
+ for (let [t, n] of c) n.freshUntil <= e && c.delete(t);
8
+ for (let e of c.keys()) {
9
+ if (c.size <= o) break;
10
+ c.delete(e);
11
+ }
12
+ }, u = (e) => e !== null && e.expiresAt.getTime() > s();
13
+ return {
14
+ ttlMs: a,
15
+ invalidate: (e) => {
16
+ c.delete(e);
17
+ },
18
+ isLive: (t) => e.gen(function* () {
19
+ let e = s();
20
+ if (a > 0) {
21
+ let n = c.get(t);
22
+ if (n && n.freshUntil > e) return n.live;
23
+ }
24
+ let n = yield* r.findSession(t), i = u(n);
25
+ return a > 0 && (c.set(t, {
26
+ live: i,
27
+ freshUntil: e + a
28
+ }), l()), i;
29
+ })
30
+ };
31
+ };
32
+ //#endregion
33
+ export { r as makeSessionRevocationChecker };
package/dist/schema.d.ts CHANGED
@@ -108,11 +108,10 @@ export declare const passkeysTable: TableLike;
108
108
  export declare const recoveryCodesTable: TableLike;
109
109
 
110
110
  /**
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.
111
+ * Sessions table used by the auth handlers and custom registered sessions.
112
+ * Rotation retains inactive aliases in a device family so sign-out using an
113
+ * earlier cookie also revokes its current successor. Only unrotated rows
114
+ * authenticate or appear in device lists.
116
115
  */
117
116
  export declare const sessionsTable: TableLike;
118
117
 
package/dist/schema.js CHANGED
@@ -12,6 +12,8 @@ var a = n("users", {
12
12
  updatedAt: i().default("now").onUpdate("now")
13
13
  }).index(["tenantId"]), o = n("sessions", {
14
14
  id: e(),
15
+ familyId: r().nullable(),
16
+ rotatedAt: i().nullable(),
15
17
  userId: r(),
16
18
  tenantId: r(),
17
19
  expiresAt: i(),
@@ -19,7 +21,7 @@ var a = n("users", {
19
21
  userAgent: r().nullable(),
20
22
  createdAt: i().default("now"),
21
23
  lastSeenAt: i().default("now").onUpdate("now")
22
- }).index(["userId"]).index(["tenantId"]), s = n("memberships", {
24
+ }).index(["userId"]).index(["tenantId"]).index(["familyId"]), s = n("memberships", {
23
25
  id: e(),
24
26
  userId: r(),
25
27
  tenantId: r(),
@@ -0,0 +1,118 @@
1
+ import { issueSession as e } from "./session.js";
2
+ import { newAuthId as t } from "./userStore.js";
3
+ import { Effect as n } from "effect";
4
+ import { eq as r } from "@voltro/database";
5
+ //#region src/sessionRegistry.ts
6
+ var i = "sessions", a = 604800, o = (e) => ({
7
+ ...e,
8
+ expiresAt: new Date(e.expiresAt),
9
+ createdAt: new Date(e.createdAt),
10
+ lastSeenAt: new Date(e.lastSeenAt),
11
+ rotatedAt: e.rotatedAt == null ? null : new Date(e.rotatedAt)
12
+ }), s = async (e, t, n = !1) => await e.query({
13
+ table: i,
14
+ predicate: t,
15
+ order: [],
16
+ take: void 0,
17
+ skip: void 0,
18
+ projection: void 0,
19
+ ...n ? { lock: "update" } : {}
20
+ }), c = (e, t, n) => t.transaction === "existing" ? n(e) : e.transactional((e) => n(e)), l = (e) => e.familyId ?? e.id, u = (e) => e.rotatedAt == null, d = async (e, t, n) => {
21
+ let i = (await s(e, r("id", n)))[0];
22
+ if (!i || i.userId !== t) return null;
23
+ let a = l(i), o = (await s(e, r("id", a), !0))[0];
24
+ if (!o || o.userId !== t || l(o) !== a) return null;
25
+ let c = (await s(e, r("userId", t))).filter((e) => l(e) === a), u = c.find((e) => e.id === n);
26
+ return u ? {
27
+ root: o,
28
+ current: u,
29
+ rows: c
30
+ } : null;
31
+ }, f = async (e, t, n) => {
32
+ let r = await d(e, t, n);
33
+ if (!r) return {
34
+ removed: !1,
35
+ credentials: 0
36
+ };
37
+ let a = 0, o = !1;
38
+ for (let t of r.rows) await e.delete(i, t.id) && (o = !0, u(t) && (a += 1));
39
+ return {
40
+ removed: o,
41
+ credentials: a
42
+ };
43
+ }, p = (e, t = {}) => ({
44
+ listSessions: (t) => n.promise(async () => (await s(e, r("userId", t))).filter(u).map(o).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime())),
45
+ insertSession: (t) => n.promise(async () => {
46
+ let n = /* @__PURE__ */ new Date(), r = {
47
+ ...t,
48
+ familyId: t.familyId ?? t.id,
49
+ rotatedAt: t.rotatedAt ?? null,
50
+ createdAt: n,
51
+ lastSeenAt: n
52
+ };
53
+ return await e.insert(i, r), r;
54
+ }),
55
+ findSession: (t) => n.promise(async () => {
56
+ let n = (await s(e, r("id", t)))[0];
57
+ return n && u(n) ? o(n) : null;
58
+ }),
59
+ touchSession: (a, o) => n.promise(() => c(e, t, async (e) => {
60
+ let t = (await s(e, r("id", a)))[0];
61
+ if (!t) return;
62
+ let n = await d(e, t.userId, a);
63
+ n && u(n.current) && (await e.update(i, a, {
64
+ expiresAt: o,
65
+ lastSeenAt: /* @__PURE__ */ new Date()
66
+ }), n.root.id !== a && new Date(n.root.expiresAt) < o && await e.update(i, n.root.id, { expiresAt: o }));
67
+ })),
68
+ revokeSession: (r, i) => n.promise(() => c(e, t, async (e) => (await f(e, r, i)).removed)),
69
+ revokeAllOtherSessions: (i, a) => n.promise(() => c(e, t, async (e) => {
70
+ let t = await s(e, r("userId", i)), n = t.find((e) => e.id === a), o = n ? l(n) : void 0, c = [...new Set(t.map(l))].filter((e) => e !== o).sort(), u = 0;
71
+ for (let t of c) u += (await f(e, i, t)).credentials;
72
+ return u;
73
+ })),
74
+ revokeAllSessions: (i) => n.promise(() => c(e, t, async (e) => {
75
+ let t = [...new Set((await s(e, r("userId", i))).map(l))].sort(), n = 0;
76
+ for (let r of t) n += (await f(e, i, r)).credentials;
77
+ return n;
78
+ }))
79
+ }), m = (e, t) => e.type === "user" ? {
80
+ ...e,
81
+ metadata: {
82
+ ...e.metadata ?? {},
83
+ sessionId: t
84
+ }
85
+ } : e, h = async (r, i, o, s, c) => {
86
+ if (i.type !== "user") throw Error("issueRegisteredSession: only a user subject has a session row");
87
+ let l = t("sess"), u = s.ttlSeconds ?? a, d = (s.now ?? Date.now)();
88
+ await n.runPromise(r.insertSession({
89
+ id: l,
90
+ familyId: c ?? l,
91
+ rotatedAt: null,
92
+ userId: i.id,
93
+ tenantId: i.tenantId ?? "",
94
+ expiresAt: new Date(d + u * 1e3),
95
+ ipAddress: s.ipAddress ?? null,
96
+ userAgent: s.userAgent ?? null
97
+ }));
98
+ let { ipAddress: f, userAgent: p, ...h } = s;
99
+ return {
100
+ ...e(m(i, l), o, h),
101
+ sessionId: l
102
+ };
103
+ }, g = async (e, t, n, r = {}) => h(e, t, n, r), _ = async (e, t, n, r, o = {}) => t.type === "user" ? c(e, o, async (e) => {
104
+ let s = await d(e, t.id, n), c = (o.now ?? Date.now)();
105
+ if (!s || !u(s.current) || new Date(s.current.expiresAt).getTime() <= c) return null;
106
+ let l = new Date(c + (o.ttlSeconds ?? a) * 1e3);
107
+ await e.update(i, s.current.id, {
108
+ familyId: s.root.id,
109
+ rotatedAt: new Date(c)
110
+ }), await e.update(i, s.root.id, {
111
+ familyId: s.root.id,
112
+ expiresAt: new Date(Math.max(new Date(s.root.expiresAt).getTime(), l.getTime()))
113
+ });
114
+ let { transaction: f, ...m } = o;
115
+ return h(p(e, { transaction: "existing" }), t, r, m, s.root.id);
116
+ }) : null;
117
+ //#endregion
118
+ export { p as dataStoreSessionRegistry, g as issueRegisteredSession, _ as rotateRegisteredSession, m as withSessionId };
@@ -224,6 +224,10 @@ declare interface PasskeyStore {
224
224
  /** A server-side session row — enumerated so apps can list active
225
225
  * devices + revoke them. Written on sign-in; deleted on revoke. */
226
226
  declare interface SessionRecord {
227
+ /** Stable device lineage. The original row is its transaction lock anchor. */
228
+ readonly familyId?: string | null;
229
+ /** A replaced credential remains an inert revocation alias until the device ends. */
230
+ readonly rotatedAt?: Date | null;
227
231
  readonly id: string;
228
232
  readonly userId: string;
229
233
  readonly tenantId: string;