@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.
- package/CHANGELOG.md +666 -0
- package/README.md +10 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- package/dist/admission.js +9 -0
- package/dist/appUserStore.js +19 -0
- package/dist/emailVerification.js +62 -0
- package/dist/handlers.js +337 -0
- package/dist/impersonation.js +175 -0
- package/dist/index.d.ts +18 -6
- package/dist/index.js +22 -88
- package/dist/invitations.js +187 -0
- package/dist/mail.js +10 -0
- package/dist/memberships.js +8 -0
- package/dist/nativeRoutes.js +120 -0
- package/dist/passkeyHandlers.js +162 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.js +498 -5
- package/dist/postgresUserStore.js +463 -0
- package/dist/revocation.js +33 -0
- package/dist/schema.d.ts +4 -5
- package/dist/schema.js +3 -1
- package/dist/sessionRegistry.js +118 -0
- package/dist/strategy.d.ts +4 -0
- package/dist/strategy.js +65 -3
- package/dist/subjectGuard.js +11 -0
- package/dist/totp.js +55 -0
- package/dist/userStore.js +285 -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.js +4 -90
- package/package.json +3 -3
- package/dist/plugin-CbmcyEJs.js +0 -2018
- package/dist/strategy-XaGjSicX.js +0 -353
|
@@ -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
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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 };
|
package/dist/strategy.d.ts
CHANGED
|
@@ -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;
|