@voltro/plugin-auth 0.86.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schema.js CHANGED
@@ -1,107 +1,108 @@
1
- import { id as e, integer as t, table as n, text as r, timestamp as i } from "@voltro/database";
1
+ import { authGuestDevicesTable as e, authGuestsTable as t, guestTables as n } from "./guestSchema.js";
2
+ import { id as r, integer as i, table as a, text as o, timestamp as s } from "@voltro/database";
2
3
  //#region src/schema/index.ts
3
- var a = n("users", {
4
- id: e(),
5
- email: r().unique(),
6
- passwordHash: r().nullable(),
7
- tenantId: r(),
8
- mfaSecret: r().nullable(),
9
- mfaEnrolledAt: i().nullable(),
10
- emailVerifiedAt: i().nullable(),
11
- createdAt: i().default("now"),
12
- updatedAt: i().default("now").onUpdate("now")
13
- }).index(["tenantId"]), o = n("sessions", {
14
- id: e(),
15
- familyId: r().nullable(),
16
- rotatedAt: i().nullable(),
17
- userId: r(),
18
- tenantId: r(),
19
- expiresAt: i(),
20
- ipAddress: r().nullable(),
21
- userAgent: r().nullable(),
22
- createdAt: i().default("now"),
23
- lastSeenAt: i().default("now").onUpdate("now")
24
- }).index(["userId"]).index(["tenantId"]).index(["familyId"]), s = n("memberships", {
25
- id: e(),
26
- userId: r(),
27
- tenantId: r(),
28
- role: r(),
29
- joinedAt: i().default("now")
30
- }).unique("byUserTenant", ["userId", "tenantId"]).index(["tenantId"]), c = n("authTokens", {
31
- id: e(),
32
- tokenHash: r().unique(),
33
- userId: r(),
34
- purpose: r(),
35
- expiresAt: i(),
36
- consumedAt: i().nullable(),
37
- createdAt: i().default("now")
38
- }).index(["userId"]).index(["purpose"]), l = n("passkeys", {
39
- id: e(),
40
- credentialId: r().unique(),
41
- userId: r(),
42
- publicKey: r(),
43
- counter: t().default(0),
44
- transports: r().nullable(),
45
- createdAt: i().default("now"),
46
- lastUsedAt: i().nullable()
47
- }).index(["userId"]), u = n("recoveryCodes", {
48
- id: e(),
49
- userId: r(),
50
- codeHash: r(),
51
- consumedAt: i().nullable(),
52
- createdAt: i().default("now")
53
- }).index(["userId"]), d = n("passkeyChallenges", {
54
- id: e(),
55
- key: r().unique(),
56
- challenge: r(),
57
- expiresAt: i(),
58
- createdAt: i().default("now")
59
- }), f = n("loginAttempts", {
60
- id: e(),
61
- email: r().unique(),
62
- failedCount: t().default(0),
63
- windowStartedAt: i().nullable(),
64
- lockedUntil: i().nullable(),
65
- createdAt: i().default("now"),
66
- updatedAt: i().default("now").onUpdate("now")
67
- }), p = n("invitations", {
68
- id: e({ prefix: "inv" }),
69
- tokenHash: r().unique(),
70
- email: r(),
71
- tenantId: r(),
72
- role: r(),
73
- invitedByUserId: r(),
74
- expiresAt: i(),
75
- acceptedAt: i().nullable(),
76
- acceptedByUserId: r().nullable(),
77
- revokedAt: i().nullable(),
78
- revokedByUserId: r().nullable(),
79
- createdAt: i().default("now")
80
- }).index(["tenantId"]).index(["email"]), m = n("impersonationGrants", {
81
- id: e({ prefix: "imp" }),
82
- sessionId: r().unique(),
83
- actorUserId: r(),
84
- actorSessionId: r(),
85
- targetUserId: r(),
86
- tenantId: r(),
87
- reason: r().nullable(),
88
- startedAt: i().default("now"),
89
- expiresAt: i(),
90
- endedAt: i().nullable(),
91
- endedReason: r().nullable(),
92
- ipAddress: r().nullable(),
93
- userAgent: r().nullable()
94
- }).index(["actorUserId"]).index(["targetUserId"]), h = [
95
- a,
96
- o,
97
- s,
4
+ var c = a("users", {
5
+ id: r(),
6
+ email: o().nullable(),
7
+ passwordHash: o().nullable(),
8
+ tenantId: o(),
9
+ mfaSecret: o().nullable(),
10
+ mfaEnrolledAt: s().nullable(),
11
+ emailVerifiedAt: s().nullable(),
12
+ createdAt: s().default("now"),
13
+ updatedAt: s().default("now").onUpdate("now")
14
+ }).index(["tenantId"]).uniqueActive("usersEmailUnique", ["email"], { where: "\"email\" IS NOT NULL" }), l = a("sessions", {
15
+ id: r(),
16
+ familyId: o().nullable(),
17
+ rotatedAt: s().nullable(),
18
+ userId: o(),
19
+ tenantId: o(),
20
+ expiresAt: s(),
21
+ ipAddress: o().nullable(),
22
+ userAgent: o().nullable(),
23
+ createdAt: s().default("now"),
24
+ lastSeenAt: s().default("now").onUpdate("now")
25
+ }).index(["userId"]).index(["tenantId"]).index(["familyId"]), u = a("memberships", {
26
+ id: r(),
27
+ userId: o(),
28
+ tenantId: o(),
29
+ role: o(),
30
+ joinedAt: s().default("now")
31
+ }).unique("byUserTenant", ["userId", "tenantId"]).index(["tenantId"]), d = a("authTokens", {
32
+ id: r(),
33
+ tokenHash: o().unique(),
34
+ userId: o(),
35
+ purpose: o(),
36
+ expiresAt: s(),
37
+ consumedAt: s().nullable(),
38
+ createdAt: s().default("now")
39
+ }).index(["userId"]).index(["purpose"]), f = a("passkeys", {
40
+ id: r(),
41
+ credentialId: o().unique(),
42
+ userId: o(),
43
+ publicKey: o(),
44
+ counter: i().default(0),
45
+ transports: o().nullable(),
46
+ createdAt: s().default("now"),
47
+ lastUsedAt: s().nullable()
48
+ }).index(["userId"]), p = a("recoveryCodes", {
49
+ id: r(),
50
+ userId: o(),
51
+ codeHash: o(),
52
+ consumedAt: s().nullable(),
53
+ createdAt: s().default("now")
54
+ }).index(["userId"]), m = a("passkeyChallenges", {
55
+ id: r(),
56
+ key: o().unique(),
57
+ challenge: o(),
58
+ expiresAt: s(),
59
+ createdAt: s().default("now")
60
+ }), h = a("loginAttempts", {
61
+ id: r(),
62
+ email: o().unique(),
63
+ failedCount: i().default(0),
64
+ windowStartedAt: s().nullable(),
65
+ lockedUntil: s().nullable(),
66
+ createdAt: s().default("now"),
67
+ updatedAt: s().default("now").onUpdate("now")
68
+ }), g = a("invitations", {
69
+ id: r({ prefix: "inv" }),
70
+ tokenHash: o().unique(),
71
+ email: o(),
72
+ tenantId: o(),
73
+ role: o(),
74
+ invitedByUserId: o(),
75
+ expiresAt: s(),
76
+ acceptedAt: s().nullable(),
77
+ acceptedByUserId: o().nullable(),
78
+ revokedAt: s().nullable(),
79
+ revokedByUserId: o().nullable(),
80
+ createdAt: s().default("now")
81
+ }).index(["tenantId"]).index(["email"]), _ = a("impersonationGrants", {
82
+ id: r({ prefix: "imp" }),
83
+ sessionId: o().unique(),
84
+ actorUserId: o(),
85
+ actorSessionId: o(),
86
+ targetUserId: o(),
87
+ tenantId: o(),
88
+ reason: o().nullable(),
89
+ startedAt: s().default("now"),
90
+ expiresAt: s(),
91
+ endedAt: s().nullable(),
92
+ endedReason: o().nullable(),
93
+ ipAddress: o().nullable(),
94
+ userAgent: o().nullable()
95
+ }).index(["actorUserId"]).index(["targetUserId"]), v = [
98
96
  c,
99
97
  l,
100
98
  u,
101
99
  d,
102
100
  f,
103
101
  p,
104
- m
102
+ m,
103
+ h,
104
+ g,
105
+ _
105
106
  ];
106
107
  //#endregion
107
- export { h as authTables, c as authTokensTable, m as impersonationGrantsTable, p as invitationsTable, f as loginAttemptsTable, s as membershipsTable, d as passkeyChallengesTable, l as passkeysTable, u as recoveryCodesTable, o as sessionsTable, a as usersTable };
108
+ export { e as authGuestDevicesTable, t as authGuestsTable, v as authTables, d as authTokensTable, n as guestTables, _ as impersonationGrantsTable, g as invitationsTable, h as loginAttemptsTable, u as membershipsTable, m as passkeyChallengesTable, f as passkeysTable, p as recoveryCodesTable, l as sessionsTable, c as usersTable };
@@ -0,0 +1,15 @@
1
+ //#region src/sessionFamily.ts
2
+ var e = (e) => {
3
+ if (typeof e.id != "string" || e.id.length === 0 || typeof e.userId != "string" || e.userId.length === 0) return null;
4
+ let t = e.familyId ?? e.id;
5
+ return typeof t == "string" && t.length > 0 ? t : null;
6
+ }, t = (t, n) => {
7
+ let r = e(t);
8
+ return r !== null && n != null && n.id === r && n.userId === t.userId && e(n) === n.id;
9
+ }, n = (e) => {
10
+ let t = /* @__PURE__ */ new Map();
11
+ for (let n of e) typeof n.id == "string" && n.id.length !== 0 && t.set(n.id, t.has(n.id) ? null : n);
12
+ return t;
13
+ }, r = (e, t, n, r) => e !== null && typeof t == "string" && t.length > 0 && typeof n == "string" && n.length > 0 && Number.isFinite(r) && e.id === t && e.userId === n && e.rotatedAt == null && e.expiresAt instanceof Date && Number.isFinite(e.expiresAt.getTime()) && e.expiresAt.getTime() > r;
14
+ //#endregion
15
+ export { t as hasOwnedSessionFamily, n as indexSessionFamilies, r as isSessionCredentialLiveFor, e as sessionFamilyId };
@@ -1,48 +1,59 @@
1
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";
2
+ import { hasOwnedSessionFamily as t, indexSessionFamilies as n, sessionFamilyId as r } from "./sessionFamily.js";
3
+ import { newAuthId as i } from "./userStore.js";
4
+ import { Effect as a } from "effect";
5
+ import { and as o, columnRef as s, eq as c, isNull as l, or as u } from "@voltro/database";
5
6
  //#region src/sessionRegistry.ts
6
- var i = "sessions", a = 604800, o = (e) => ({
7
+ var d = "sessions", f = 604800, p = (e) => ({
7
8
  ...e,
8
9
  expiresAt: new Date(e.expiresAt),
9
10
  createdAt: new Date(e.createdAt),
10
11
  lastSeenAt: new Date(e.lastSeenAt),
11
12
  rotatedAt: e.rotatedAt == null ? null : new Date(e.rotatedAt)
12
- }), s = async (e, t, n = !1) => await e.query({
13
- table: i,
13
+ }), m = async (e, t, n = !1) => await e.query({
14
+ table: d,
14
15
  predicate: t,
15
- order: [],
16
+ order: n ? [{
17
+ column: "id",
18
+ direction: "asc"
19
+ }] : [],
16
20
  take: void 0,
17
21
  skip: void 0,
18
22
  projection: void 0,
19
23
  ...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
24
+ }), h = (e, t, n) => t.transaction === "existing" ? n(e) : e.transactional((e) => n(e)), g = (e) => e.familyId ?? e.id, _ = (e) => e.rotatedAt == null, v = async (e, t, n) => {
25
+ let r = (await m(e, c("id", n)))[0];
26
+ if (!r || r.userId !== t) return null;
27
+ let i = g(r), a = (await m(e, c("id", i), !0))[0];
28
+ if (!a || a.userId !== t || g(a) !== i) return null;
29
+ let o = n === a.id ? a : (await m(e, c("id", n), !0))[0];
30
+ return o && o.userId === t && g(o) === i ? {
31
+ root: a,
32
+ current: o
30
33
  } : 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));
34
+ }, y = async (e, t, n) => {
35
+ let r = await v(e, t, n);
36
+ return r && _(r.current) ? p(r.current) : null;
37
+ }, b = async (e, t, n) => {
38
+ if (n.userId !== t || g(n) !== n.id) throw Error("The locked session family requires reconciliation.");
39
+ let r = await m(e, o(c("userId", t), u(c("id", n.id), c("familyId", n.id))), !0), i = 0, a = !1;
40
+ for (let t of r) await e.delete(d, t.id) && (a = !0, _(t) && (i += 1));
39
41
  return {
40
- removed: o,
41
- credentials: a
42
+ removed: a,
43
+ credentials: i
42
44
  };
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 () => {
45
+ }, x = async (e, t, n) => {
46
+ let r = await v(e, t, n);
47
+ return r === null ? {
48
+ removed: !1,
49
+ credentials: 0
50
+ } : b(e, t, r.root);
51
+ }, S = (e, t) => m(e, o(c("userId", t), u(l("familyId"), c("familyId", s("id")))), !0), C = (e, i = {}) => ({
52
+ listSessions: (i) => a.promise(async () => {
53
+ let a = await m(e, c("userId", i)), o = n(a);
54
+ return a.filter((e) => e.userId === i && _(e) && o.get(e.id) === e && t(e, o.get(r(e) ?? ""))).map(p).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime());
55
+ }),
56
+ insertSession: (t) => a.promise(async () => {
46
57
  let n = /* @__PURE__ */ new Date(), r = {
47
58
  ...t,
48
59
  familyId: t.familyId ?? t.id,
@@ -50,69 +61,73 @@ var i = "sessions", a = 604800, o = (e) => ({
50
61
  createdAt: n,
51
62
  lastSeenAt: n
52
63
  };
53
- return await e.insert(i, r), r;
64
+ return await e.insert(d, r), r;
54
65
  }),
55
- findSession: (t) => n.promise(async () => {
56
- let n = (await s(e, r("id", t)))[0];
57
- return n && u(n) ? o(n) : null;
66
+ findSession: (n) => a.promise(async () => {
67
+ let i = await m(e, c("id", n)), a = i.length === 1 ? i[0] : void 0;
68
+ if (!a || a.id !== n || !_(a)) return null;
69
+ let o = r(a);
70
+ if (o === null) return null;
71
+ let s = o === a.id ? [a] : await m(e, c("id", o));
72
+ return s.length === 1 && t(a, s[0]) ? p(a) : null;
58
73
  }),
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,
74
+ touchSession: (t, n) => a.promise(() => h(e, i, async (e) => {
75
+ let r = (await m(e, c("id", t)))[0];
76
+ if (!r) return;
77
+ let i = await v(e, r.userId, t);
78
+ i && _(i.current) && (await e.update(d, t, {
79
+ expiresAt: n,
65
80
  lastSeenAt: /* @__PURE__ */ new Date()
66
- }), n.root.id !== a && new Date(n.root.expiresAt) < o && await e.update(i, n.root.id, { expiresAt: o }));
81
+ }), i.root.id !== t && new Date(i.root.expiresAt) < n && await e.update(d, i.root.id, { expiresAt: n }));
67
82
  })),
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;
83
+ revokeSession: (t, n) => a.promise(() => h(e, i, async (e) => (await x(e, t, n)).removed)),
84
+ revokeAllOtherSessions: (t, n) => a.promise(() => h(e, i, async (e) => {
85
+ let r = await S(e, t), i = (await m(e, o(c("userId", t), c("id", n)), !0))[0], a = i && r.some((e) => e.id === g(i)) ? g(i) : void 0, s = 0;
86
+ for (let n of r) n.id !== a && (s += (await b(e, t, n)).credentials);
87
+ return s;
73
88
  })),
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;
89
+ revokeAllSessions: (t) => a.promise(() => h(e, i, async (e) => {
90
+ let n = await S(e, t), r = 0;
91
+ for (let i of n) r += (await b(e, t, i)).credentials;
92
+ return r;
78
93
  }))
79
- }), m = (e, t) => e.type === "user" ? {
94
+ }), w = (e, t) => e.type === "user" ? {
80
95
  ...e,
81
96
  metadata: {
82
97
  ...e.metadata ?? {},
83
98
  sessionId: t
84
99
  }
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,
100
+ } : e, T = async (t, n, r, o, s) => {
101
+ if (n.type !== "user") throw Error("issueRegisteredSession: only a user subject has a session row");
102
+ let c = i("sess"), l = o.ttlSeconds ?? f, u = (o.now ?? Date.now)();
103
+ await a.runPromise(t.insertSession({
104
+ id: c,
105
+ familyId: s ?? c,
91
106
  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
107
+ userId: n.id,
108
+ tenantId: n.tenantId ?? "",
109
+ expiresAt: new Date(u + l * 1e3),
110
+ ipAddress: o.ipAddress ?? null,
111
+ userAgent: o.userAgent ?? null
97
112
  }));
98
- let { ipAddress: f, userAgent: p, ...h } = s;
113
+ let { ipAddress: d, userAgent: p, ...m } = o;
99
114
  return {
100
- ...e(m(i, l), o, h),
101
- sessionId: l
115
+ ...e(w(n, c), r, m),
116
+ sessionId: c
102
117
  };
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()))
118
+ }, E = async (e, t, n, r = {}) => T(e, t, n, r), D = async (e, t, n, r, i = {}) => t.type === "user" ? h(e, i, async (e) => {
119
+ let a = await v(e, t.id, n), o = (i.now ?? Date.now)();
120
+ if (!a || !_(a.current) || new Date(a.current.expiresAt).getTime() <= o) return null;
121
+ let s = new Date(o + (i.ttlSeconds ?? f) * 1e3);
122
+ await e.update(d, a.current.id, {
123
+ familyId: a.root.id,
124
+ rotatedAt: new Date(o)
125
+ }), await e.update(d, a.root.id, {
126
+ familyId: a.root.id,
127
+ expiresAt: new Date(Math.max(new Date(a.root.expiresAt).getTime(), s.getTime()))
113
128
  });
114
- let { transaction: f, ...m } = o;
115
- return h(p(e, { transaction: "existing" }), t, r, m, s.root.id);
129
+ let { transaction: c, ...l } = i;
130
+ return T(C(e, { transaction: "existing" }), t, r, l, a.root.id);
116
131
  }) : null;
117
132
  //#endregion
118
- export { p as dataStoreSessionRegistry, g as issueRegisteredSession, _ as rotateRegisteredSession, m as withSessionId };
133
+ export { C as dataStoreSessionRegistry, E as issueRegisteredSession, y as lockRegisteredSession, D as rotateRegisteredSession, w as withSessionId };
@@ -0,0 +1,24 @@
1
+ import { createHmac as e, timingSafeEqual as t } from "node:crypto";
2
+ //#region src/signedClaims.ts
3
+ var n = (t, n, r) => e("sha256", n).update(`${r}:${t}`).digest(), r = (e, t, r) => {
4
+ if (t.length < 32 || typeof e.exp != "number" || !Number.isSafeInteger(e.exp)) throw Error("Auth handoff requires a project signing key and whole-second expiry");
5
+ let i = Buffer.from(JSON.stringify(e), "utf8").toString("base64url");
6
+ if (i.length > 8192) throw Error("Auth handoff exceeds its payload budget");
7
+ return `${i}.${n(i, t, r).toString("base64url")}`;
8
+ }, i = (e, r, i, a = Date.now) => {
9
+ if (!e || e.length > 8256 || r.length < 32) return null;
10
+ let o = e.split("."), [s, c] = o;
11
+ if (o.length !== 2 || !s || !c || !/^[A-Za-z0-9_-]+$/.test(s) || !/^[A-Za-z0-9_-]{43}$/.test(c)) return null;
12
+ let l = Buffer.from(c, "base64url"), u = n(s, r, i);
13
+ if (l.length !== u.length || !t(l, u)) return null;
14
+ try {
15
+ let e = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
16
+ if (typeof e != "object" || !e || Array.isArray(e)) return null;
17
+ let t = e, n = a();
18
+ return !Number.isFinite(n) || typeof t.exp != "number" || !Number.isSafeInteger(t.exp) || t.exp * 1e3 <= n ? null : t;
19
+ } catch {
20
+ return null;
21
+ }
22
+ };
23
+ //#endregion
24
+ export { r as signAuthClaims, i as verifyAuthClaims };
@@ -243,10 +243,10 @@ declare interface SessionRecord {
243
243
  declare type SessionRegistry = SessionStore;
244
244
 
245
245
  declare interface SessionRevocationChecker {
246
- /** Is this session still live? `false` means the row was revoked (or
247
- * is past its `expiresAt`) — the caller rejects the cookie. Verdicts
248
- * are cached for `ttlMs`. */
249
- readonly isLive: (sessionId: string) => Effect.Effect<boolean>;
246
+ /** Is this exact session credential live for the signed subject's user ID?
247
+ * Refuse a missing/rotated/expired row or a different stored owner. Cache
248
+ * verdicts retain native owner identity, never the requested owner. */
249
+ readonly isLiveFor: (sessionId: string, userId: string) => Effect.Effect<boolean>;
250
250
  /** Drop the cached verdict for one session — called inline by
251
251
  * sign-out / revoke handlers so the kill is immediate on THIS
252
252
  * process (other replicas converge within `ttlMs`). */
@@ -276,13 +276,14 @@ declare type SessionSecretInput = string | SessionSecrets;
276
276
 
277
277
  /** Server-side sessions — enumeration + revocation. */
278
278
  declare interface SessionStore {
279
- /** Active sessions for a user (most recent first). */
279
+ /** Unrotated credentials with an existing canonical root owned by the same user, most recent first. */
280
280
  readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
281
281
  /** Record a session on sign-in. */
282
282
  readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
283
- /** Look up one session row by id. The request-time revocation check
284
- * (`makeSessionRevocationChecker`) calls this — a missing row means
285
- * the session was revoked. */
283
+ /** Look up an unrotated credential only while its canonical family root
284
+ * exists and belongs to the same user. A rotated root remains an anchor;
285
+ * its tenant does not replace the credential tenant. Custom stores must
286
+ * enforce this before returning a row to request-time revocation checks. */
286
287
  readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
287
288
  /** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
288
289
  * when the sliding-window renewal re-issues the cookie, so the row
@@ -356,7 +357,7 @@ declare const UserNotFoundError_base: new <A extends Record<string, any> = {}>(a
356
357
 
357
358
  declare interface UserRecord {
358
359
  readonly id: string;
359
- readonly email: string;
360
+ readonly email: string | null;
360
361
  /**
361
362
  * The stored scrypt hash — OPTIONAL, because not every identity has a
362
363
  * password. An SSO-only, magic-link-only, passkey-only, or provider-PAT
@@ -445,6 +446,9 @@ export declare interface VoltroPasswordStrategyOptions {
445
446
  * (stateless); with it, nothing but a registered session authenticates,
446
447
  * and every session can be revoked. */
447
448
  readonly requireSessionId?: boolean;
449
+ /** Verified user identity kinds owned by this carrier. Other verified kinds
450
+ * skip to their own carrier; malformed, forged or expired values still fail. */
451
+ readonly userKinds?: ReadonlyArray<'registered' | 'guest'>;
448
452
  }
449
453
 
450
454
  export { }
package/dist/strategy.js CHANGED
@@ -1,33 +1,34 @@
1
1
  import { sessionSecretsOf as e } from "./session.js";
2
- import { makeSessionRevocationChecker as t } from "./revocation.js";
3
- import { sessionIdOfSubject as n } from "./userStore.js";
4
- import { Effect as r } from "effect";
5
- import { readSessionValue as i, resolveSessionSecret as a, verifySessionKeyed as o } from "@voltro/protocol/session";
2
+ import { makeSessionRevocationChecker as t, validateSessionRevocationOptions as n } from "./revocation.js";
3
+ import { sessionIdOfSubject as r } from "./userStore.js";
4
+ import { Effect as i } from "effect";
5
+ import { readSessionValue as a, resolveSessionSecret as o, verifySessionKeyed as s } from "@voltro/protocol/session";
6
6
  //#region src/strategy.ts
7
- var s = (e) => typeof e.isLive == "function", c = (e) => e.revocation && s(e.revocation) ? e.revocation : e.store ? t(e.store, e.revocation ?? {}) : null, l = (l = {}) => {
8
- let d = l.cookieName ?? "voltro:session", f = c(l), p = l.revocation && !s(l.revocation) ? l.revocation : {}, m = /* @__PURE__ */ new WeakMap(), h = (e) => {
7
+ var c = (e) => typeof e == "object" && !!e && "isLiveFor" in e && typeof e.isLiveFor == "function" && "invalidate" in e && typeof e.invalidate == "function" && typeof e.ttlMs == "number" && Number.isFinite(e.ttlMs) && e.ttlMs >= 0, l = (e) => e.revocation && c(e.revocation) ? e.revocation : (e.revocation !== void 0 && n(e.revocation), e.store ? t(e.store, e.revocation ?? {}) : null), u = (n = {}) => {
8
+ let u = n.cookieName ?? "voltro:session", f = l(n), p = n.revocation && !c(n.revocation) ? n.revocation : {}, m = /* @__PURE__ */ new WeakMap(), h = (e) => {
9
9
  if (f) return f;
10
- let n = l.sessionRegistry;
11
- if (n === void 0) return null;
12
- if (typeof n != "function") return t(n, p);
10
+ let r = n.sessionRegistry;
11
+ if (r === void 0) return null;
12
+ if (typeof r != "function") return t(r, p);
13
13
  if (e === void 0) return null;
14
- let r = m.get(e);
15
- if (r) return r;
16
- let i = t(n(e), p);
17
- return m.set(e, i), i;
14
+ let i = m.get(e);
15
+ if (i) return i;
16
+ let a = t(r(e), p);
17
+ return m.set(e, a), a;
18
18
  };
19
19
  return {
20
20
  id: "voltro-password",
21
21
  resolve: (t) => {
22
- let s = i(t.headers, d);
23
- if (!s) return { kind: "skip" };
24
- let c = e(l.secret ?? a()), f = o(s, c);
22
+ let c = a(t.headers, u);
23
+ if (!c) return { kind: "skip" };
24
+ let l = e(n.secret ?? o()), f = s(c, l);
25
25
  if (!f) return {
26
26
  kind: "failed",
27
27
  reason: "voltro session cookie failed verification"
28
28
  };
29
- let p = u(f.subject, "voltro-password"), m = n(p);
30
- if (!m) return l.requireSessionId === !0 ? {
29
+ if (f.subject.type === "user" && n.userKinds && !n.userKinds.includes(f.subject.kind === "guest" ? "guest" : "registered")) return { kind: "skip" };
30
+ let p = d(f.subject, "voltro-password"), m = r(p);
31
+ if (!m) return n.requireSessionId === !0 ? {
31
32
  kind: "failed",
32
33
  reason: "voltro session cookie carries no session id"
33
34
  } : {
@@ -35,13 +36,16 @@ var s = (e) => typeof e.isLive == "function", c = (e) => e.revocation && s(e.rev
35
36
  subject: p
36
37
  };
37
38
  let g = h(t.store);
38
- return g ? r.runPromise(g.isLive(m)).then((e) => e ? {
39
+ return g ? p.type === "user" ? i.runPromise(g.isLiveFor(m, p.id)).then((e) => e ? {
39
40
  kind: "matched",
40
41
  subject: p
41
42
  } : {
42
43
  kind: "failed",
43
44
  reason: "voltro session revoked"
44
- }) : l.requireSessionId === !0 && l.sessionRegistry !== void 0 ? {
45
+ }) : {
46
+ kind: "failed",
47
+ reason: "registered session requires a user subject"
48
+ } : n.requireSessionId === !0 ? {
45
49
  kind: "failed",
46
50
  reason: "session registry not available yet"
47
51
  } : {
@@ -50,7 +54,7 @@ var s = (e) => typeof e.isLive == "function", c = (e) => e.revocation && s(e.rev
50
54
  };
51
55
  }
52
56
  };
53
- }, u = (e, t) => {
57
+ }, d = (e, t) => {
54
58
  if (e.type === "anonymous") return e;
55
59
  let n = e.metadata ?? {};
56
60
  return {
@@ -62,4 +66,4 @@ var s = (e) => typeof e.isLive == "function", c = (e) => e.revocation && s(e.rev
62
66
  };
63
67
  };
64
68
  //#endregion
65
- export { l as voltroPasswordStrategy };
69
+ export { u as voltroPasswordStrategy };