@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.
- package/CHANGELOG.md +5987 -0
- package/README.md +93 -0
- package/THIRD-PARTY-NOTICES.md +1122 -31
- package/dist/admission.js +9 -0
- package/dist/appUserStore.js +19 -0
- package/dist/authExtensions.js +9 -0
- package/dist/emailVerification.js +64 -0
- package/dist/guest.d.ts +99 -0
- package/dist/guest.js +3 -0
- package/dist/guestClient.js +237 -0
- package/dist/guestCredential.js +12 -0
- package/dist/guestPasswordUpgrade.js +85 -0
- package/dist/guestPlugin.js +166 -0
- package/dist/guestSchema.js +20 -0
- package/dist/guestSecrets.js +10 -0
- package/dist/guestService.js +224 -0
- package/dist/handlers.js +337 -0
- package/dist/impersonation.js +176 -0
- package/dist/index.d.ts +282 -20
- package/dist/index.js +30 -88
- package/dist/invitations.js +187 -0
- package/dist/mail.js +10 -0
- package/dist/memberships.js +8 -0
- package/dist/native/guestStorage.js +41 -0
- package/dist/native.d.ts +110 -0
- package/dist/native.js +45 -43
- package/dist/nativeRoutes.js +109 -0
- package/dist/overlayPlugin.js +167 -0
- package/dist/passkeyHandlers.js +162 -0
- package/dist/plugin.d.ts +123 -10
- package/dist/plugin.js +519 -5
- package/dist/postgresUserStore.js +475 -0
- package/dist/revocation.js +50 -0
- package/dist/schema.d.ts +45 -6
- package/dist/schema.js +99 -96
- package/dist/sessionFamily.js +15 -0
- package/dist/sessionRegistry.js +133 -0
- package/dist/signedClaims.js +24 -0
- package/dist/strategy.d.ts +17 -9
- package/dist/strategy.js +69 -3
- package/dist/subjectGuard.js +11 -0
- package/dist/totp.js +55 -0
- package/dist/userStore.js +292 -0
- package/dist/web/guestStorage.js +47 -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.d.ts +94 -0
- package/dist/web.js +6 -90
- package/package.json +8 -3
- package/dist/plugin-CbmcyEJs.js +0 -2018
- package/dist/strategy-XaGjSicX.js +0 -353
package/dist/schema.js
CHANGED
|
@@ -1,105 +1,108 @@
|
|
|
1
|
-
import {
|
|
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
|
|
4
|
-
id:
|
|
5
|
-
email:
|
|
6
|
-
passwordHash:
|
|
7
|
-
tenantId:
|
|
8
|
-
mfaSecret:
|
|
9
|
-
mfaEnrolledAt:
|
|
10
|
-
emailVerifiedAt:
|
|
11
|
-
createdAt:
|
|
12
|
-
updatedAt:
|
|
13
|
-
}).index(["tenantId"]),
|
|
14
|
-
id:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 {
|
|
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 };
|
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;
|
|
@@ -239,10 +243,10 @@ declare interface SessionRecord {
|
|
|
239
243
|
declare type SessionRegistry = SessionStore;
|
|
240
244
|
|
|
241
245
|
declare interface SessionRevocationChecker {
|
|
242
|
-
/** Is this session
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
readonly
|
|
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
|
-
/**
|
|
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
|
|
280
|
-
*
|
|
281
|
-
*
|
|
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
|
|
3
|
-
|
|
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 };
|