@voltro/plugin-auth 0.86.0 → 0.88.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 (55) hide show
  1. package/CHANGELOG.md +5484 -0
  2. package/README.md +83 -0
  3. package/THIRD-PARTY-NOTICES.md +3 -227
  4. package/dist/admission.js +1 -2
  5. package/dist/appUserStore.js +1 -2
  6. package/dist/authExtensions.js +8 -0
  7. package/dist/csrf.js +1 -2
  8. package/dist/emailVerification.js +14 -13
  9. package/dist/guest.d.ts +99 -0
  10. package/dist/guest.js +3 -0
  11. package/dist/guestClient.js +236 -0
  12. package/dist/guestCredential.js +11 -0
  13. package/dist/guestPasswordUpgrade.js +84 -0
  14. package/dist/guestPlugin.js +165 -0
  15. package/dist/guestSchema.js +19 -0
  16. package/dist/guestSecrets.js +9 -0
  17. package/dist/guestService.js +223 -0
  18. package/dist/handlers.js +114 -115
  19. package/dist/impersonation.js +80 -80
  20. package/dist/index.d.ts +265 -15
  21. package/dist/index.js +27 -19
  22. package/dist/invitations.js +24 -25
  23. package/dist/mail.js +1 -2
  24. package/dist/memberships.js +1 -2
  25. package/dist/native/guestStorage.js +40 -0
  26. package/dist/native.d.ts +110 -0
  27. package/dist/native.js +46 -45
  28. package/dist/nativeRoutes.js +62 -74
  29. package/dist/overlayPlugin.js +166 -0
  30. package/dist/passkeyHandlers.js +1 -2
  31. package/dist/password.js +1 -2
  32. package/dist/plugin.d.ts +119 -10
  33. package/dist/plugin.js +100 -80
  34. package/dist/postgresUserStore.js +20 -9
  35. package/dist/revocation.js +41 -25
  36. package/dist/schema.d.ts +41 -1
  37. package/dist/schema.js +100 -100
  38. package/dist/session.js +1 -2
  39. package/dist/sessionFamily.js +14 -0
  40. package/dist/sessionRegistry.js +94 -80
  41. package/dist/signedClaims.js +23 -0
  42. package/dist/strategy.d.ts +13 -9
  43. package/dist/strategy.js +27 -24
  44. package/dist/subjectGuard.js +1 -2
  45. package/dist/tokens.js +1 -2
  46. package/dist/totp.js +1 -2
  47. package/dist/userStore.js +146 -140
  48. package/dist/web/guestStorage.js +46 -0
  49. package/dist/web/passkeyClient.js +1 -2
  50. package/dist/web/requireAuth.js +1 -2
  51. package/dist/web/subjectContext.js +1 -2
  52. package/dist/web.d.ts +94 -0
  53. package/dist/web.js +4 -2
  54. package/dist/webauthn.js +1 -2
  55. package/package.json +9 -4
package/dist/schema.js CHANGED
@@ -1,107 +1,107 @@
1
- import { id as e, integer as t, table as n, text as r, timestamp as i } from "@voltro/database";
2
- //#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,
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";
3
+ // src/schema/index.ts
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
- //#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 };
107
+ 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 };
package/dist/session.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_SESSION_KID as e, buildSetCookie as t, readCookie as n, readSessionValue as r, resolveOptionalSessionSecrets as i, resolveSessionSecret as a, resolveSessionSecrets as o, signSession as s, timingSafeStringEqual as c, verifySessionKeyed as l } from "@voltro/protocol/session";
2
- //#region src/session.ts
2
+ // src/session.ts
3
3
  var u = "voltro:session", d = 604800, f = a, p = o, m = (e) => typeof e == "string" ? e : e.current, h = (t) => {
4
4
  if (typeof t != "string") return t;
5
5
  let n = i(), r = {
@@ -40,5 +40,4 @@ var u = "voltro:session", d = 604800, f = a, p = o, m = (e) => typeof e == "stri
40
40
  ...e.domain ? { domain: e.domain } : {},
41
41
  path: "/"
42
42
  });
43
- //#endregion
44
43
  export { u as SESSION_COOKIE_NAME, b as clearSessionCookie, g as issueSession, _ as readSession, y as readSessionFromHeaders, v as readSessionKeyed, f as resolveSessionSecret, p as resolveSessionSecrets, h as sessionSecretsOf };
@@ -0,0 +1,14 @@
1
+ // 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
+ 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";
5
- //#region src/sessionRegistry.ts
6
- var i = "sessions", a = 604800, o = (e) => ({
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";
6
+ // src/sessionRegistry.ts
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,72 @@ 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
- //#endregion
118
- export { p as dataStoreSessionRegistry, g as issueRegisteredSession, _ as rotateRegisteredSession, m as withSessionId };
132
+ export { C as dataStoreSessionRegistry, E as issueRegisteredSession, y as lockRegisteredSession, D as rotateRegisteredSession, w as withSessionId };
@@ -0,0 +1,23 @@
1
+ import { createHmac as e, timingSafeEqual as t } from "node:crypto";
2
+ // 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
+ 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";
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) => {
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
+ // src/strategy.ts
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 {
@@ -61,5 +65,4 @@ var s = (e) => typeof e.isLive == "function", c = (e) => e.revocation && s(e.rev
61
65
  }
62
66
  };
63
67
  };
64
- //#endregion
65
- export { l as voltroPasswordStrategy };
68
+ export { u as voltroPasswordStrategy };