@voltro/plugin-auth 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +5987 -0
  2. package/README.md +93 -0
  3. package/THIRD-PARTY-NOTICES.md +1122 -31
  4. package/dist/admission.js +9 -0
  5. package/dist/appUserStore.js +19 -0
  6. package/dist/authExtensions.js +9 -0
  7. package/dist/emailVerification.js +64 -0
  8. package/dist/guest.d.ts +99 -0
  9. package/dist/guest.js +3 -0
  10. package/dist/guestClient.js +237 -0
  11. package/dist/guestCredential.js +12 -0
  12. package/dist/guestPasswordUpgrade.js +85 -0
  13. package/dist/guestPlugin.js +166 -0
  14. package/dist/guestSchema.js +20 -0
  15. package/dist/guestSecrets.js +10 -0
  16. package/dist/guestService.js +224 -0
  17. package/dist/handlers.js +337 -0
  18. package/dist/impersonation.js +176 -0
  19. package/dist/index.d.ts +282 -20
  20. package/dist/index.js +30 -88
  21. package/dist/invitations.js +187 -0
  22. package/dist/mail.js +10 -0
  23. package/dist/memberships.js +8 -0
  24. package/dist/native/guestStorage.js +41 -0
  25. package/dist/native.d.ts +110 -0
  26. package/dist/native.js +45 -43
  27. package/dist/nativeRoutes.js +109 -0
  28. package/dist/overlayPlugin.js +167 -0
  29. package/dist/passkeyHandlers.js +162 -0
  30. package/dist/plugin.d.ts +123 -10
  31. package/dist/plugin.js +519 -5
  32. package/dist/postgresUserStore.js +475 -0
  33. package/dist/revocation.js +50 -0
  34. package/dist/schema.d.ts +45 -6
  35. package/dist/schema.js +99 -96
  36. package/dist/sessionFamily.js +15 -0
  37. package/dist/sessionRegistry.js +133 -0
  38. package/dist/signedClaims.js +24 -0
  39. package/dist/strategy.d.ts +17 -9
  40. package/dist/strategy.js +69 -3
  41. package/dist/subjectGuard.js +11 -0
  42. package/dist/totp.js +55 -0
  43. package/dist/userStore.js +292 -0
  44. package/dist/web/guestStorage.js +47 -0
  45. package/dist/web/passkeyClient.js +78 -0
  46. package/dist/web/requireAuth.js +9 -0
  47. package/dist/web/subjectContext.js +13 -0
  48. package/dist/web.d.ts +94 -0
  49. package/dist/web.js +6 -90
  50. package/package.json +8 -3
  51. package/dist/plugin-CbmcyEJs.js +0 -2018
  52. package/dist/strategy-XaGjSicX.js +0 -353
package/dist/schema.js CHANGED
@@ -1,105 +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
- userId: r(),
16
- tenantId: r(),
17
- expiresAt: i(),
18
- ipAddress: r().nullable(),
19
- userAgent: r().nullable(),
20
- createdAt: i().default("now"),
21
- lastSeenAt: i().default("now").onUpdate("now")
22
- }).index(["userId"]).index(["tenantId"]), s = n("memberships", {
23
- id: e(),
24
- userId: r(),
25
- tenantId: r(),
26
- role: r(),
27
- joinedAt: i().default("now")
28
- }).unique("byUserTenant", ["userId", "tenantId"]).index(["tenantId"]), c = n("authTokens", {
29
- id: e(),
30
- tokenHash: r().unique(),
31
- userId: r(),
32
- purpose: r(),
33
- expiresAt: i(),
34
- consumedAt: i().nullable(),
35
- createdAt: i().default("now")
36
- }).index(["userId"]).index(["purpose"]), l = n("passkeys", {
37
- id: e(),
38
- credentialId: r().unique(),
39
- userId: r(),
40
- publicKey: r(),
41
- counter: t().default(0),
42
- transports: r().nullable(),
43
- createdAt: i().default("now"),
44
- lastUsedAt: i().nullable()
45
- }).index(["userId"]), u = n("recoveryCodes", {
46
- id: e(),
47
- userId: r(),
48
- codeHash: r(),
49
- consumedAt: i().nullable(),
50
- createdAt: i().default("now")
51
- }).index(["userId"]), d = n("passkeyChallenges", {
52
- id: e(),
53
- key: r().unique(),
54
- challenge: r(),
55
- expiresAt: i(),
56
- createdAt: i().default("now")
57
- }), f = n("loginAttempts", {
58
- id: e(),
59
- email: r().unique(),
60
- failedCount: t().default(0),
61
- windowStartedAt: i().nullable(),
62
- lockedUntil: i().nullable(),
63
- createdAt: i().default("now"),
64
- updatedAt: i().default("now").onUpdate("now")
65
- }), p = n("invitations", {
66
- id: e({ prefix: "inv" }),
67
- tokenHash: r().unique(),
68
- email: r(),
69
- tenantId: r(),
70
- role: r(),
71
- invitedByUserId: r(),
72
- expiresAt: i(),
73
- acceptedAt: i().nullable(),
74
- acceptedByUserId: r().nullable(),
75
- revokedAt: i().nullable(),
76
- revokedByUserId: r().nullable(),
77
- createdAt: i().default("now")
78
- }).index(["tenantId"]).index(["email"]), m = n("impersonationGrants", {
79
- id: e({ prefix: "imp" }),
80
- sessionId: r().unique(),
81
- actorUserId: r(),
82
- actorSessionId: r(),
83
- targetUserId: r(),
84
- tenantId: r(),
85
- reason: r().nullable(),
86
- startedAt: i().default("now"),
87
- expiresAt: i(),
88
- endedAt: i().nullable(),
89
- endedReason: r().nullable(),
90
- ipAddress: r().nullable(),
91
- userAgent: r().nullable()
92
- }).index(["actorUserId"]).index(["targetUserId"]), h = [
93
- a,
94
- o,
95
- 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 = [
96
96
  c,
97
97
  l,
98
98
  u,
99
99
  d,
100
100
  f,
101
101
  p,
102
- m
102
+ m,
103
+ h,
104
+ g,
105
+ _
103
106
  ];
104
107
  //#endregion
105
- 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 };
@@ -0,0 +1,133 @@
1
+ import { issueSession as e } from "./session.js";
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
+ //#region src/sessionRegistry.ts
7
+ var d = "sessions", f = 604800, p = (e) => ({
8
+ ...e,
9
+ expiresAt: new Date(e.expiresAt),
10
+ createdAt: new Date(e.createdAt),
11
+ lastSeenAt: new Date(e.lastSeenAt),
12
+ rotatedAt: e.rotatedAt == null ? null : new Date(e.rotatedAt)
13
+ }), m = async (e, t, n = !1) => await e.query({
14
+ table: d,
15
+ predicate: t,
16
+ order: n ? [{
17
+ column: "id",
18
+ direction: "asc"
19
+ }] : [],
20
+ take: void 0,
21
+ skip: void 0,
22
+ projection: void 0,
23
+ ...n ? { lock: "update" } : {}
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
33
+ } : null;
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));
41
+ return {
42
+ removed: a,
43
+ credentials: i
44
+ };
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 () => {
57
+ let n = /* @__PURE__ */ new Date(), r = {
58
+ ...t,
59
+ familyId: t.familyId ?? t.id,
60
+ rotatedAt: t.rotatedAt ?? null,
61
+ createdAt: n,
62
+ lastSeenAt: n
63
+ };
64
+ return await e.insert(d, r), r;
65
+ }),
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;
73
+ }),
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,
80
+ lastSeenAt: /* @__PURE__ */ new Date()
81
+ }), i.root.id !== t && new Date(i.root.expiresAt) < n && await e.update(d, i.root.id, { expiresAt: n }));
82
+ })),
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;
88
+ })),
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;
93
+ }))
94
+ }), w = (e, t) => e.type === "user" ? {
95
+ ...e,
96
+ metadata: {
97
+ ...e.metadata ?? {},
98
+ sessionId: t
99
+ }
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,
106
+ rotatedAt: 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
112
+ }));
113
+ let { ipAddress: d, userAgent: p, ...m } = o;
114
+ return {
115
+ ...e(w(n, c), r, m),
116
+ sessionId: c
117
+ };
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()))
128
+ });
129
+ let { transaction: c, ...l } = i;
130
+ return T(C(e, { transaction: "existing" }), t, r, l, a.root.id);
131
+ }) : null;
132
+ //#endregion
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 };
@@ -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;
@@ -239,10 +243,10 @@ declare interface SessionRecord {
239
243
  declare type SessionRegistry = SessionStore;
240
244
 
241
245
  declare interface SessionRevocationChecker {
242
- /** Is this session still live? `false` means the row was revoked (or
243
- * is past its `expiresAt`) — the caller rejects the cookie. Verdicts
244
- * are cached for `ttlMs`. */
245
- 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>;
246
250
  /** Drop the cached verdict for one session — called inline by
247
251
  * sign-out / revoke handlers so the kill is immediate on THIS
248
252
  * process (other replicas converge within `ttlMs`). */
@@ -272,13 +276,14 @@ declare type SessionSecretInput = string | SessionSecrets;
272
276
 
273
277
  /** Server-side sessions — enumeration + revocation. */
274
278
  declare interface SessionStore {
275
- /** Active sessions for a user (most recent first). */
279
+ /** Unrotated credentials with an existing canonical root owned by the same user, most recent first. */
276
280
  readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
277
281
  /** Record a session on sign-in. */
278
282
  readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
279
- /** Look up one session row by id. The request-time revocation check
280
- * (`makeSessionRevocationChecker`) calls this — a missing row means
281
- * 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. */
282
287
  readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
283
288
  /** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
284
289
  * when the sliding-window renewal re-issues the cookie, so the row
@@ -352,7 +357,7 @@ declare const UserNotFoundError_base: new <A extends Record<string, any> = {}>(a
352
357
 
353
358
  declare interface UserRecord {
354
359
  readonly id: string;
355
- readonly email: string;
360
+ readonly email: string | null;
356
361
  /**
357
362
  * The stored scrypt hash — OPTIONAL, because not every identity has a
358
363
  * password. An SSO-only, magic-link-only, passkey-only, or provider-PAT
@@ -441,6 +446,9 @@ export declare interface VoltroPasswordStrategyOptions {
441
446
  * (stateless); with it, nothing but a registered session authenticates,
442
447
  * and every session can be revoked. */
443
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'>;
444
452
  }
445
453
 
446
454
  export { }
package/dist/strategy.js CHANGED
@@ -1,3 +1,69 @@
1
- import "./session.js";
2
- import { t as e } from "./strategy-XaGjSicX.js";
3
- export { e as voltroPasswordStrategy };
1
+ import { sessionSecretsOf as e } from "./session.js";
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
+ //#region 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
+ if (f) return f;
10
+ let r = n.sessionRegistry;
11
+ if (r === void 0) return null;
12
+ if (typeof r != "function") return t(r, p);
13
+ if (e === void 0) return null;
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
+ };
19
+ return {
20
+ id: "voltro-password",
21
+ resolve: (t) => {
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
+ if (!f) return {
26
+ kind: "failed",
27
+ reason: "voltro session cookie failed verification"
28
+ };
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 ? {
32
+ kind: "failed",
33
+ reason: "voltro session cookie carries no session id"
34
+ } : {
35
+ kind: "matched",
36
+ subject: p
37
+ };
38
+ let g = h(t.store);
39
+ return g ? p.type === "user" ? i.runPromise(g.isLiveFor(m, p.id)).then((e) => e ? {
40
+ kind: "matched",
41
+ subject: p
42
+ } : {
43
+ kind: "failed",
44
+ reason: "voltro session revoked"
45
+ }) : {
46
+ kind: "failed",
47
+ reason: "registered session requires a user subject"
48
+ } : n.requireSessionId === !0 ? {
49
+ kind: "failed",
50
+ reason: "session registry not available yet"
51
+ } : {
52
+ kind: "matched",
53
+ subject: p
54
+ };
55
+ }
56
+ };
57
+ }, d = (e, t) => {
58
+ if (e.type === "anonymous") return e;
59
+ let n = e.metadata ?? {};
60
+ return {
61
+ ...e,
62
+ metadata: {
63
+ ...n,
64
+ provider: t
65
+ }
66
+ };
67
+ };
68
+ //#endregion
69
+ export { u as voltroPasswordStrategy };
@@ -0,0 +1,11 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/subjectGuard.ts
3
+ var t = { ok: !0 }, n = (n, r) => e.gen(function* () {
4
+ for (let e of n) {
5
+ let t = yield* e(r);
6
+ if (!t.ok) return t;
7
+ }
8
+ return t;
9
+ });
10
+ //#endregion
11
+ export { n as runSubjectGuards };
package/dist/totp.js ADDED
@@ -0,0 +1,55 @@
1
+ import { createHmac as e, randomBytes as t, timingSafeEqual as n } from "node:crypto";
2
+ import { Data as r, Effect as i } from "effect";
3
+ //#region src/totp.ts
4
+ var a = 30, o = 6, s = 20, c = "sha1", l = 1, u = class extends r.TaggedError("TotpVerifyError") {}, d = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", f = (e) => {
5
+ let t = 0, n = 0, r = "";
6
+ for (let i of e) for (n = n << 8 | i, t += 8; t >= 5;) r += d[n >>> t - 5 & 31], t -= 5;
7
+ return t > 0 && (r += d[n << 5 - t & 31]), r;
8
+ }, p = (e) => {
9
+ let t = e.toUpperCase().replace(/[\s=]+/g, "");
10
+ if (!/^[A-Z2-7]+$/.test(t)) return null;
11
+ let n = new Uint8Array(Math.floor(t.length * 5 / 8)), r = 0, i = 0, a = 0;
12
+ for (let e of t) {
13
+ let t = d.indexOf(e);
14
+ if (t === -1) return null;
15
+ i = i << 5 | t, r += 5, r >= 8 && (n[a++] = i >>> r - 8 & 255, r -= 8);
16
+ }
17
+ return n.subarray(0, a);
18
+ }, m = (e) => {
19
+ let t = Buffer.alloc(8);
20
+ return t.writeBigUInt64BE(BigInt(e), 0), t;
21
+ }, h = (t, n) => {
22
+ let r = e(c, Buffer.from(t));
23
+ r.update(m(n));
24
+ let i = r.digest(), a = i[i.length - 1] & 15, s = (i[a] & 127) << 24 | (i[a + 1] & 255) << 16 | (i[a + 2] & 255) << 8 | i[a + 3] & 255;
25
+ return String(s % 10 ** o).padStart(o, "0");
26
+ }, g = () => f(t(s)), _ = (e) => `otpauth://totp/${encodeURIComponent(`${e.issuer}:${e.accountName}`)}?${new URLSearchParams({
27
+ secret: e.secret,
28
+ issuer: e.issuer,
29
+ algorithm: "SHA1",
30
+ digits: String(o),
31
+ period: String(a)
32
+ }).toString()}`, v = (e, t = Date.now()) => {
33
+ let n = p(e);
34
+ return n ? h(n, Math.floor(t / 1e3 / a)) : "000000";
35
+ }, y = (e, t, r = Date.now()) => i.gen(function* () {
36
+ let o = p(e);
37
+ if (!o) return yield* i.fail(new u({ reason: "malformedSecret" }));
38
+ let s = t.replace(/\s+/g, "");
39
+ if (!/^[0-9]{6}$/.test(s)) return yield* i.fail(new u({ reason: "invalidCode" }));
40
+ let c = Math.floor(r / 1e3 / a), d = Buffer.from(s);
41
+ for (let e = -1; e <= l; e++) {
42
+ let t = h(o, c + e), r = Buffer.from(t);
43
+ if (r.length === d.length && n(r, d)) return !0;
44
+ }
45
+ return yield* i.fail(new u({ reason: "invalidCode" }));
46
+ }), b = (e = 10) => {
47
+ let n = [];
48
+ for (let r = 0; r < e; r++) {
49
+ let e = f(t(16)).slice(0, 25).match(/.{1,5}/g) ?? [];
50
+ n.push(e.join("-"));
51
+ }
52
+ return n;
53
+ };
54
+ //#endregion
55
+ export { u as TotpVerifyError, b as generateRecoveryCodes, v as generateTotpCode, g as generateTotpSecret, _ as otpauthUrl, y as verifyTotpCode };