@mountsqli/auth 0.1.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/index.d.ts +550 -0
- package/dist/index.js +1247 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/core/auth.ts
|
|
9
|
+
import { eq as eq3 } from "@mountsqli/core";
|
|
10
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes as randomBytes3, createHmac as createHmac2 } from "crypto";
|
|
11
|
+
|
|
12
|
+
// src/core/password.ts
|
|
13
|
+
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
|
14
|
+
import { promisify } from "util";
|
|
15
|
+
var scryptAsync = promisify(scrypt);
|
|
16
|
+
var SALT_LENGTH = 32;
|
|
17
|
+
var HASH_LENGTH = 64;
|
|
18
|
+
async function hashPassword(password) {
|
|
19
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
20
|
+
const hash = await scryptAsync(password, salt, HASH_LENGTH);
|
|
21
|
+
return `$scrypt$${salt.toString("hex")}$${hash.toString("hex")}`;
|
|
22
|
+
}
|
|
23
|
+
async function verifyPassword(password, hash) {
|
|
24
|
+
if (!hash.startsWith("$scrypt$")) return false;
|
|
25
|
+
const [, , saltHex, storedHashHex] = hash.split("$");
|
|
26
|
+
if (!saltHex || !storedHashHex) return false;
|
|
27
|
+
const salt = Buffer.from(saltHex, "hex");
|
|
28
|
+
const storedHash = Buffer.from(storedHashHex, "hex");
|
|
29
|
+
const computedHash = await scryptAsync(password, salt, HASH_LENGTH);
|
|
30
|
+
if (computedHash.length !== storedHash.length) return false;
|
|
31
|
+
return timingSafeEqual(computedHash, storedHash);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/core/session-manager.ts
|
|
35
|
+
import { eq, and, gt } from "@mountsqli/core";
|
|
36
|
+
|
|
37
|
+
// src/core/jwt.ts
|
|
38
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
39
|
+
var encoder = new TextEncoder();
|
|
40
|
+
async function createToken(payload, secret, options) {
|
|
41
|
+
const secretKey = encoder.encode(secret);
|
|
42
|
+
const maxAge = options?.maxAge ?? 30 * 24 * 60 * 60;
|
|
43
|
+
return new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${maxAge}s`).sign(secretKey);
|
|
44
|
+
}
|
|
45
|
+
async function verifyToken(token, secret) {
|
|
46
|
+
try {
|
|
47
|
+
const secretKey = encoder.encode(secret);
|
|
48
|
+
const { payload } = await jwtVerify(token, secretKey);
|
|
49
|
+
return payload;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function decodeToken(token) {
|
|
55
|
+
try {
|
|
56
|
+
const [, payloadB64] = token.split(".");
|
|
57
|
+
if (!payloadB64) return null;
|
|
58
|
+
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
|
|
59
|
+
return payload;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/core/session-manager.ts
|
|
66
|
+
async function createSession(config, user, options) {
|
|
67
|
+
const expiresAt = new Date(Date.now() + config.maxAge * 1e3);
|
|
68
|
+
if (config.strategy === "jwt") {
|
|
69
|
+
const token2 = await createToken(
|
|
70
|
+
{
|
|
71
|
+
sub: user.id,
|
|
72
|
+
email: user.email,
|
|
73
|
+
name: user.name ?? void 0,
|
|
74
|
+
image: user.image ?? void 0
|
|
75
|
+
},
|
|
76
|
+
config.secret,
|
|
77
|
+
{ maxAge: config.maxAge }
|
|
78
|
+
);
|
|
79
|
+
return {
|
|
80
|
+
user,
|
|
81
|
+
session: { user, expiresAt },
|
|
82
|
+
token: token2
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const token = await createToken(
|
|
86
|
+
{
|
|
87
|
+
sub: user.id,
|
|
88
|
+
email: user.email,
|
|
89
|
+
name: user.name ?? void 0,
|
|
90
|
+
image: user.image ?? void 0
|
|
91
|
+
},
|
|
92
|
+
config.secret,
|
|
93
|
+
{ maxAge: config.maxAge }
|
|
94
|
+
);
|
|
95
|
+
const [session] = await config.db.insert(config.sessionsTable).values({
|
|
96
|
+
userId: user.id,
|
|
97
|
+
token,
|
|
98
|
+
expiresAt,
|
|
99
|
+
ipAddress: options?.ipAddress,
|
|
100
|
+
userAgent: options?.userAgent
|
|
101
|
+
}).returning();
|
|
102
|
+
return {
|
|
103
|
+
user,
|
|
104
|
+
session: { user, expiresAt },
|
|
105
|
+
token
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function validateSession(config, token) {
|
|
109
|
+
const payload = await verifyToken(token, config.secret);
|
|
110
|
+
if (!payload) return null;
|
|
111
|
+
if (config.strategy === "jwt") {
|
|
112
|
+
const [user2] = await config.db.select().from(config.usersTable).where(eq(config.usersTable.id, payload.sub));
|
|
113
|
+
if (!user2) return null;
|
|
114
|
+
return {
|
|
115
|
+
user: sanitizeUser(user2),
|
|
116
|
+
expiresAt: new Date(payload.exp * 1e3)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const [session] = await config.db.select().from(config.sessionsTable).where(
|
|
120
|
+
and(
|
|
121
|
+
eq(config.sessionsTable.token, token),
|
|
122
|
+
gt(config.sessionsTable.expiresAt, /* @__PURE__ */ new Date())
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
if (!session) return null;
|
|
126
|
+
const [user] = await config.db.select().from(config.usersTable).where(eq(config.usersTable.id, session.userId));
|
|
127
|
+
if (!user) return null;
|
|
128
|
+
return {
|
|
129
|
+
user: sanitizeUser(user),
|
|
130
|
+
expiresAt: session.expiresAt
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function revokeSession(config, token) {
|
|
134
|
+
if (config.strategy === "jwt") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await config.db.delete(config.sessionsTable).where(eq(config.sessionsTable.token, token));
|
|
138
|
+
}
|
|
139
|
+
async function revokeAllSessions(config, userId) {
|
|
140
|
+
if (config.strategy === "jwt") {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await config.db.delete(config.sessionsTable).where(eq(config.sessionsTable.userId, userId));
|
|
144
|
+
}
|
|
145
|
+
function sanitizeUser(user) {
|
|
146
|
+
return {
|
|
147
|
+
id: user.id,
|
|
148
|
+
email: user.email,
|
|
149
|
+
emailVerified: user.emailVerified,
|
|
150
|
+
name: user.name,
|
|
151
|
+
image: user.image,
|
|
152
|
+
twoFactorEnabled: user.twoFactorEnabled,
|
|
153
|
+
createdAt: user.createdAt,
|
|
154
|
+
updatedAt: user.updatedAt
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/core/totp.ts
|
|
159
|
+
import { createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
160
|
+
function generateSecret(length = 20) {
|
|
161
|
+
return randomBytes2(length).toString("base64url");
|
|
162
|
+
}
|
|
163
|
+
function generateCode(secret, timestamp6, config) {
|
|
164
|
+
const digits = config?.digits ?? 6;
|
|
165
|
+
const period = config?.period ?? 30;
|
|
166
|
+
const algorithm = config?.algorithm ?? "sha1";
|
|
167
|
+
const time = Math.floor((timestamp6 ?? Date.now()) / 1e3 / period);
|
|
168
|
+
const timeBuffer = Buffer.alloc(8);
|
|
169
|
+
timeBuffer.writeUInt32BE(0, 0);
|
|
170
|
+
timeBuffer.writeUInt32BE(time, 4);
|
|
171
|
+
const secretBuffer = Buffer.from(secret, "base64url");
|
|
172
|
+
const hmac = createHmac(algorithm, secretBuffer).update(timeBuffer).digest();
|
|
173
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
174
|
+
const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
175
|
+
return String(code % 10 ** digits).padStart(digits, "0");
|
|
176
|
+
}
|
|
177
|
+
function verifyCode(secret, code, config) {
|
|
178
|
+
const period = config?.period ?? 30;
|
|
179
|
+
const digits = config?.digits ?? 6;
|
|
180
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
181
|
+
for (const drift of [-1, 0, 1]) {
|
|
182
|
+
const timestamp6 = (now + drift * period) * 1e3;
|
|
183
|
+
const expectedCode = generateCode(secret, timestamp6, config);
|
|
184
|
+
const expectedBuf = Buffer.from(expectedCode.padEnd(digits, "0"));
|
|
185
|
+
const inputBuf = Buffer.from(code.padEnd(digits, "0"));
|
|
186
|
+
if (expectedBuf.length === inputBuf.length) {
|
|
187
|
+
const { timingSafeEqual: timingSafeEqual2 } = __require("crypto");
|
|
188
|
+
if (timingSafeEqual2(expectedBuf, inputBuf)) return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function generateURI(secret, email, issuer = "MountSQLi", config) {
|
|
194
|
+
const digits = config?.digits ?? 6;
|
|
195
|
+
const period = config?.period ?? 30;
|
|
196
|
+
const algorithm = config?.algorithm ?? "sha1";
|
|
197
|
+
const encodedIssuer = encodeURIComponent(issuer);
|
|
198
|
+
const encodedEmail = encodeURIComponent(email);
|
|
199
|
+
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=${algorithm.toUpperCase()}&digits=${digits}&period=${period}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/core/oauth.ts
|
|
203
|
+
function getAuthorizationUrl(provider, state) {
|
|
204
|
+
const scope = provider.scope?.join(" ") ?? "openid email profile";
|
|
205
|
+
const params = new URLSearchParams({
|
|
206
|
+
client_id: provider.clientId,
|
|
207
|
+
redirect_uri: `${getBaseUrl()}/auth/callback/${provider.id}`,
|
|
208
|
+
response_type: "code",
|
|
209
|
+
scope,
|
|
210
|
+
state
|
|
211
|
+
});
|
|
212
|
+
const authUrl = provider.authorizationUrl ?? getDefaultAuthorizationUrl(provider.id);
|
|
213
|
+
return `${authUrl}?${params.toString()}`;
|
|
214
|
+
}
|
|
215
|
+
async function exchangeCode(provider, code) {
|
|
216
|
+
const tokenUrl = provider.tokenUrl ?? getDefaultTokenUrl(provider.id);
|
|
217
|
+
const response = await fetch(tokenUrl, {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: {
|
|
220
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
221
|
+
Accept: "application/json"
|
|
222
|
+
},
|
|
223
|
+
body: new URLSearchParams({
|
|
224
|
+
client_id: provider.clientId,
|
|
225
|
+
client_secret: provider.clientSecret,
|
|
226
|
+
code,
|
|
227
|
+
grant_type: "authorization_code",
|
|
228
|
+
redirect_uri: `${getBaseUrl()}/auth/callback/${provider.id}`
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`OAuth token exchange failed: ${response.statusText}`);
|
|
233
|
+
}
|
|
234
|
+
return response.json();
|
|
235
|
+
}
|
|
236
|
+
async function getUserInfo(provider, accessToken) {
|
|
237
|
+
const userInfoUrl = provider.userInfoUrl ?? getDefaultUserInfoUrl(provider.id);
|
|
238
|
+
const response = await fetch(userInfoUrl, {
|
|
239
|
+
headers: {
|
|
240
|
+
Authorization: `Bearer ${accessToken}`,
|
|
241
|
+
Accept: "application/json"
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw new Error(`OAuth user info fetch failed: ${response.statusText}`);
|
|
246
|
+
}
|
|
247
|
+
const data = await response.json();
|
|
248
|
+
return normalizeUserInfo(provider.id, data);
|
|
249
|
+
}
|
|
250
|
+
function generateState() {
|
|
251
|
+
const array = new Uint8Array(32);
|
|
252
|
+
crypto.getRandomValues(array);
|
|
253
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
254
|
+
}
|
|
255
|
+
function getDefaultAuthorizationUrl(providerId) {
|
|
256
|
+
const urls = {
|
|
257
|
+
google: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
258
|
+
github: "https://github.com/login/oauth/authorize"
|
|
259
|
+
};
|
|
260
|
+
return urls[providerId] ?? "";
|
|
261
|
+
}
|
|
262
|
+
function getDefaultTokenUrl(providerId) {
|
|
263
|
+
const urls = {
|
|
264
|
+
google: "https://oauth2.googleapis.com/token",
|
|
265
|
+
github: "https://github.com/login/oauth/access_token"
|
|
266
|
+
};
|
|
267
|
+
return urls[providerId] ?? "";
|
|
268
|
+
}
|
|
269
|
+
function getDefaultUserInfoUrl(providerId) {
|
|
270
|
+
const urls = {
|
|
271
|
+
google: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
272
|
+
github: "https://api.github.com/user"
|
|
273
|
+
};
|
|
274
|
+
return urls[providerId] ?? "";
|
|
275
|
+
}
|
|
276
|
+
function normalizeUserInfo(providerId, data) {
|
|
277
|
+
switch (providerId) {
|
|
278
|
+
case "google":
|
|
279
|
+
return {
|
|
280
|
+
id: data.id ?? data.sub,
|
|
281
|
+
email: data.email,
|
|
282
|
+
name: data.name ?? data.given_name,
|
|
283
|
+
image: data.picture,
|
|
284
|
+
emailVerified: data.verified_email ?? false
|
|
285
|
+
};
|
|
286
|
+
case "github":
|
|
287
|
+
return {
|
|
288
|
+
id: String(data.id),
|
|
289
|
+
email: data.email,
|
|
290
|
+
name: data.name ?? data.login,
|
|
291
|
+
image: data.avatar_url,
|
|
292
|
+
emailVerified: true
|
|
293
|
+
};
|
|
294
|
+
default:
|
|
295
|
+
return {
|
|
296
|
+
id: String(data.id ?? data.sub),
|
|
297
|
+
email: data.email,
|
|
298
|
+
name: data.name,
|
|
299
|
+
image: data.image ?? data.picture ?? data.avatar_url,
|
|
300
|
+
emailVerified: data.email_verified ?? false
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function getBaseUrl() {
|
|
305
|
+
return process.env.AUTH_BASE_URL ?? process.env.NEXTAUTH_URL ?? "http://localhost:3000";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/core/rbac.ts
|
|
309
|
+
import { eq as eq2 } from "@mountsqli/core";
|
|
310
|
+
async function createRole(config, data) {
|
|
311
|
+
const [role] = await config.db.insert(config.rolesTable).values(data).returning();
|
|
312
|
+
return {
|
|
313
|
+
id: role.id,
|
|
314
|
+
name: role.name,
|
|
315
|
+
permissions: role.permissions,
|
|
316
|
+
createdAt: role.createdAt
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function deleteRole(config, roleId) {
|
|
320
|
+
await config.db.delete(config.rolesTable).where(eq2(config.rolesTable.id, roleId));
|
|
321
|
+
}
|
|
322
|
+
async function getRoleByName(config, name) {
|
|
323
|
+
const [role] = await config.db.select().from(config.rolesTable).where(eq2(config.rolesTable.name, name));
|
|
324
|
+
return role ?? null;
|
|
325
|
+
}
|
|
326
|
+
async function assignRole(config, userId, roleName) {
|
|
327
|
+
const role = await getRoleByName(config, roleName);
|
|
328
|
+
if (!role) throw new Error(`Role "${roleName}" not found`);
|
|
329
|
+
await config.db.insert(config.userRolesTable).values({ userId, roleId: role.id }).onConflictDoNothing();
|
|
330
|
+
}
|
|
331
|
+
async function removeRole(config, userId, roleName) {
|
|
332
|
+
const role = await getRoleByName(config, roleName);
|
|
333
|
+
if (!role) return;
|
|
334
|
+
await config.db.delete(config.userRolesTable).where(
|
|
335
|
+
eq2(config.userRolesTable.userId, userId) && eq2(config.userRolesTable.roleId, role.id)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
async function getUserRoles(config, userId) {
|
|
339
|
+
const results = await config.db.select({
|
|
340
|
+
id: config.rolesTable.id,
|
|
341
|
+
name: config.rolesTable.name,
|
|
342
|
+
permissions: config.rolesTable.permissions,
|
|
343
|
+
createdAt: config.rolesTable.createdAt
|
|
344
|
+
}).from(config.userRolesTable).innerJoin(
|
|
345
|
+
config.rolesTable,
|
|
346
|
+
eq2(config.userRolesTable.roleId, config.rolesTable.id)
|
|
347
|
+
).where(eq2(config.userRolesTable.userId, userId));
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
async function hasRole(config, userId, roleName) {
|
|
351
|
+
const roles = await getUserRoles(config, userId);
|
|
352
|
+
return roles.some((r) => r.name === roleName);
|
|
353
|
+
}
|
|
354
|
+
async function hasPermission(config, userId, permission) {
|
|
355
|
+
const roles = await getUserRoles(config, userId);
|
|
356
|
+
if (roles.some((r) => r.permissions.includes("*"))) return true;
|
|
357
|
+
return roles.some((r) => r.permissions.includes(permission));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/core/email.ts
|
|
361
|
+
var ConsoleEmailAdapter = class {
|
|
362
|
+
async send(params) {
|
|
363
|
+
console.log(`[Auth Email] To: ${params.to}`);
|
|
364
|
+
console.log(`[Auth Email] Subject: ${params.subject}`);
|
|
365
|
+
console.log(`[Auth Email] Body: ${params.text ?? params.html ?? ""}`);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
async function sendVerificationEmail(adapter, email, token, baseUrl = "http://localhost:3000") {
|
|
369
|
+
const verifyUrl = `${baseUrl}/auth/verify?token=${token}`;
|
|
370
|
+
await adapter.send({
|
|
371
|
+
to: email,
|
|
372
|
+
subject: "Verify your email address",
|
|
373
|
+
text: `Click the link to verify your email: ${verifyUrl}`,
|
|
374
|
+
html: `
|
|
375
|
+
<h1>Verify your email</h1>
|
|
376
|
+
<p>Click the link below to verify your email address:</p>
|
|
377
|
+
<a href="${verifyUrl}">${verifyUrl}</a>
|
|
378
|
+
<p>This link will expire in 24 hours.</p>
|
|
379
|
+
`
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function sendPasswordResetEmail(adapter, email, token, baseUrl = "http://localhost:3000") {
|
|
383
|
+
const resetUrl = `${baseUrl}/auth/reset-password?token=${token}`;
|
|
384
|
+
await adapter.send({
|
|
385
|
+
to: email,
|
|
386
|
+
subject: "Reset your password",
|
|
387
|
+
text: `Click the link to reset your password: ${resetUrl}`,
|
|
388
|
+
html: `
|
|
389
|
+
<h1>Reset your password</h1>
|
|
390
|
+
<p>Click the link below to reset your password:</p>
|
|
391
|
+
<a href="${resetUrl}">${resetUrl}</a>
|
|
392
|
+
<p>This link will expire in 1 hour.</p>
|
|
393
|
+
<p>If you didn't request this, ignore this email.</p>
|
|
394
|
+
`
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/core/rate-limiter.ts
|
|
399
|
+
var InMemoryRateLimiter = class {
|
|
400
|
+
store = /* @__PURE__ */ new Map();
|
|
401
|
+
maxRequests;
|
|
402
|
+
windowMs;
|
|
403
|
+
cleanupInterval;
|
|
404
|
+
constructor(config) {
|
|
405
|
+
this.maxRequests = config?.maxRequests ?? 5;
|
|
406
|
+
this.windowMs = config?.windowMs ?? 15 * 60 * 1e3;
|
|
407
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1e3);
|
|
408
|
+
}
|
|
409
|
+
async check(key) {
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
const entry = this.store.get(key);
|
|
412
|
+
if (!entry || now > entry.resetAt) {
|
|
413
|
+
const resetAt = now + this.windowMs;
|
|
414
|
+
this.store.set(key, { count: 1, resetAt });
|
|
415
|
+
return {
|
|
416
|
+
allowed: true,
|
|
417
|
+
remaining: this.maxRequests - 1,
|
|
418
|
+
resetAt: new Date(resetAt)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
entry.count++;
|
|
422
|
+
return {
|
|
423
|
+
allowed: entry.count <= this.maxRequests,
|
|
424
|
+
remaining: Math.max(0, this.maxRequests - entry.count),
|
|
425
|
+
resetAt: new Date(entry.resetAt)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
cleanup() {
|
|
429
|
+
const now = Date.now();
|
|
430
|
+
for (const [key, entry] of this.store.entries()) {
|
|
431
|
+
if (now > entry.resetAt) {
|
|
432
|
+
this.store.delete(key);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
destroy() {
|
|
437
|
+
clearInterval(this.cleanupInterval);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/errors.ts
|
|
442
|
+
var AuthError = class extends Error {
|
|
443
|
+
constructor(message) {
|
|
444
|
+
super(message);
|
|
445
|
+
this.name = "AuthError";
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var UnauthorizedError = class extends AuthError {
|
|
449
|
+
constructor(message = "Unauthorized") {
|
|
450
|
+
super(message);
|
|
451
|
+
this.name = "UnauthorizedError";
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
var ForbiddenError = class extends AuthError {
|
|
455
|
+
constructor(message = "Forbidden") {
|
|
456
|
+
super(message);
|
|
457
|
+
this.name = "ForbiddenError";
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
var InvalidCredentialsError = class extends AuthError {
|
|
461
|
+
constructor(message = "Invalid email or password") {
|
|
462
|
+
super(message);
|
|
463
|
+
this.name = "InvalidCredentialsError";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
var UserNotFoundError = class extends AuthError {
|
|
467
|
+
constructor(message = "User not found") {
|
|
468
|
+
super(message);
|
|
469
|
+
this.name = "UserNotFoundError";
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
var EmailAlreadyExistsError = class extends AuthError {
|
|
473
|
+
constructor(message = "Email already registered") {
|
|
474
|
+
super(message);
|
|
475
|
+
this.name = "EmailAlreadyExistsError";
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var InvalidTokenError = class extends AuthError {
|
|
479
|
+
constructor(message = "Invalid or expired token") {
|
|
480
|
+
super(message);
|
|
481
|
+
this.name = "InvalidTokenError";
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var TwoFactorRequiredError = class extends AuthError {
|
|
485
|
+
constructor(message = "Two-factor authentication required") {
|
|
486
|
+
super(message);
|
|
487
|
+
this.name = "TwoFactorRequiredError";
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
var TwoFactorInvalidError = class extends AuthError {
|
|
491
|
+
constructor(message = "Invalid two-factor code") {
|
|
492
|
+
super(message);
|
|
493
|
+
this.name = "TwoFactorInvalidError";
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var SessionExpiredError = class extends AuthError {
|
|
497
|
+
constructor(message = "Session expired") {
|
|
498
|
+
super(message);
|
|
499
|
+
this.name = "SessionExpiredError";
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
var ProviderError = class extends AuthError {
|
|
503
|
+
constructor(message = "Authentication provider error") {
|
|
504
|
+
super(message);
|
|
505
|
+
this.name = "ProviderError";
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/core/auth.ts
|
|
510
|
+
async function encryptToken(token, secret) {
|
|
511
|
+
const key = createHash("sha256").update(secret).digest();
|
|
512
|
+
const iv = randomBytes3(16);
|
|
513
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
514
|
+
const encrypted = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
|
|
515
|
+
const tag = cipher.getAuthTag();
|
|
516
|
+
return `${iv.toString("hex")}:${tag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
517
|
+
}
|
|
518
|
+
var Auth = class {
|
|
519
|
+
config;
|
|
520
|
+
db;
|
|
521
|
+
secret;
|
|
522
|
+
sessionConfig;
|
|
523
|
+
emailAdapter;
|
|
524
|
+
rateLimiter;
|
|
525
|
+
// Schema tables (with fallback defaults)
|
|
526
|
+
usersTable;
|
|
527
|
+
sessionsTable;
|
|
528
|
+
accountsTable;
|
|
529
|
+
verificationTable;
|
|
530
|
+
rolesTable;
|
|
531
|
+
userRolesTable;
|
|
532
|
+
constructor(config) {
|
|
533
|
+
this.config = config;
|
|
534
|
+
this.db = config.db;
|
|
535
|
+
this.secret = config.secret;
|
|
536
|
+
this.sessionConfig = {
|
|
537
|
+
strategy: config.session?.strategy ?? "jwt",
|
|
538
|
+
maxAge: config.session?.maxAge ?? 30 * 24 * 60 * 60
|
|
539
|
+
};
|
|
540
|
+
this.rateLimiter = config.rateLimiter ?? new InMemoryRateLimiter();
|
|
541
|
+
this.usersTable = config.tables?.users;
|
|
542
|
+
this.sessionsTable = config.tables?.sessions;
|
|
543
|
+
this.accountsTable = config.tables?.accounts;
|
|
544
|
+
this.verificationTable = config.tables?.verification;
|
|
545
|
+
this.rolesTable = config.tables?.roles;
|
|
546
|
+
this.userRolesTable = config.tables?.userRoles;
|
|
547
|
+
this.emailAdapter = config.callbacks?.onRegister ? new ConsoleEmailAdapter() : new ConsoleEmailAdapter();
|
|
548
|
+
}
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// Registration
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
async register(data) {
|
|
553
|
+
const rateLimitResult = await this.rateLimiter.check(`register:${data.email}`);
|
|
554
|
+
if (!rateLimitResult.allowed) {
|
|
555
|
+
throw new UnauthorizedError("Too many registration attempts. Please try again later.");
|
|
556
|
+
}
|
|
557
|
+
if (!this.isValidEmail(data.email)) {
|
|
558
|
+
throw new InvalidCredentialsError("Invalid email format");
|
|
559
|
+
}
|
|
560
|
+
const passwordValidation = this.isValidPassword(data.password);
|
|
561
|
+
if (!passwordValidation.valid) {
|
|
562
|
+
throw new InvalidCredentialsError(passwordValidation.errors.join(". "));
|
|
563
|
+
}
|
|
564
|
+
const [existing] = await this.db.select().from(this.usersTable).where(eq3(this.usersTable.email, data.email));
|
|
565
|
+
if (existing) {
|
|
566
|
+
throw new EmailAlreadyExistsError();
|
|
567
|
+
}
|
|
568
|
+
const hashedPassword = await hashPassword(data.password);
|
|
569
|
+
const [user] = await this.db.insert(this.usersTable).values({
|
|
570
|
+
email: data.email,
|
|
571
|
+
password: hashedPassword,
|
|
572
|
+
name: data.name
|
|
573
|
+
}).returning();
|
|
574
|
+
const sanitizedUser = this.sanitizeUser(user);
|
|
575
|
+
const result = await createSession(
|
|
576
|
+
{
|
|
577
|
+
db: this.db,
|
|
578
|
+
secret: this.secret,
|
|
579
|
+
strategy: this.sessionConfig.strategy,
|
|
580
|
+
maxAge: this.sessionConfig.maxAge,
|
|
581
|
+
sessionsTable: this.sessionsTable,
|
|
582
|
+
usersTable: this.usersTable
|
|
583
|
+
},
|
|
584
|
+
sanitizedUser
|
|
585
|
+
);
|
|
586
|
+
await this.config.callbacks?.onRegister?.(sanitizedUser);
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// Login
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
async login(providerId, credentials2) {
|
|
593
|
+
const provider = this.config.providers.find((p) => p.id === providerId);
|
|
594
|
+
if (!provider) {
|
|
595
|
+
throw new ProviderError(`Provider "${providerId}" not found`);
|
|
596
|
+
}
|
|
597
|
+
if (provider.type === "credentials") {
|
|
598
|
+
return this.loginWithCredentials(provider, credentials2);
|
|
599
|
+
}
|
|
600
|
+
throw new ProviderError(`Provider "${providerId}" is not a credentials provider`);
|
|
601
|
+
}
|
|
602
|
+
async loginWithCredentials(provider, credentials2) {
|
|
603
|
+
const { email, password } = credentials2;
|
|
604
|
+
if (!email || !password) {
|
|
605
|
+
throw new InvalidCredentialsError();
|
|
606
|
+
}
|
|
607
|
+
const rateLimitResult = await this.rateLimiter.check(`login:${email}`);
|
|
608
|
+
if (!rateLimitResult.allowed) {
|
|
609
|
+
throw new UnauthorizedError("Too many login attempts. Please try again later.");
|
|
610
|
+
}
|
|
611
|
+
const [user] = await this.db.select().from(this.usersTable).where(eq3(this.usersTable.email, email));
|
|
612
|
+
if (!user || !user.password) {
|
|
613
|
+
throw new InvalidCredentialsError();
|
|
614
|
+
}
|
|
615
|
+
const valid = await verifyPassword(password, user.password);
|
|
616
|
+
if (!valid) {
|
|
617
|
+
throw new InvalidCredentialsError();
|
|
618
|
+
}
|
|
619
|
+
if (user.twoFactorEnabled) {
|
|
620
|
+
const code = credentials2.totp;
|
|
621
|
+
if (!code) {
|
|
622
|
+
throw new TwoFactorRequiredError();
|
|
623
|
+
}
|
|
624
|
+
if (!user.twoFactorSecret || !verifyCode(user.twoFactorSecret, code)) {
|
|
625
|
+
throw new TwoFactorInvalidError();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const sanitizedUser = this.sanitizeUser(user);
|
|
629
|
+
const result = await createSession(
|
|
630
|
+
{
|
|
631
|
+
db: this.db,
|
|
632
|
+
secret: this.secret,
|
|
633
|
+
strategy: this.sessionConfig.strategy,
|
|
634
|
+
maxAge: this.sessionConfig.maxAge,
|
|
635
|
+
sessionsTable: this.sessionsTable,
|
|
636
|
+
usersTable: this.usersTable
|
|
637
|
+
},
|
|
638
|
+
sanitizedUser
|
|
639
|
+
);
|
|
640
|
+
await this.config.callbacks?.onLogin?.(sanitizedUser);
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
// OAuth
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
async signIn(providerId) {
|
|
647
|
+
const provider = this.config.providers.find((p) => p.id === providerId);
|
|
648
|
+
if (!provider || provider.type !== "oauth") {
|
|
649
|
+
throw new ProviderError(`OAuth provider "${providerId}" not found`);
|
|
650
|
+
}
|
|
651
|
+
const state = generateState();
|
|
652
|
+
await this.db.insert(this.verificationTable).values({
|
|
653
|
+
identifier: `state:${state}`,
|
|
654
|
+
token: state,
|
|
655
|
+
expiresAt: new Date(Date.now() + 10 * 60 * 1e3)
|
|
656
|
+
});
|
|
657
|
+
return getAuthorizationUrl(provider, state);
|
|
658
|
+
}
|
|
659
|
+
async handleCallback(providerId, params) {
|
|
660
|
+
if (params.error) {
|
|
661
|
+
throw new ProviderError(`OAuth error: ${params.error}`);
|
|
662
|
+
}
|
|
663
|
+
if (!params.code) {
|
|
664
|
+
throw new ProviderError("Missing authorization code");
|
|
665
|
+
}
|
|
666
|
+
if (params.state) {
|
|
667
|
+
const [stored] = await this.db.select().from(this.verificationTable).where(eq3(this.verificationTable.identifier, `state:${params.state}`));
|
|
668
|
+
if (!stored || stored.expiresAt < /* @__PURE__ */ new Date()) {
|
|
669
|
+
throw new ProviderError("Invalid or expired OAuth state");
|
|
670
|
+
}
|
|
671
|
+
await this.db.delete(this.verificationTable).where(eq3(this.verificationTable.identifier, `state:${params.state}`));
|
|
672
|
+
}
|
|
673
|
+
const provider = this.config.providers.find((p) => p.id === providerId);
|
|
674
|
+
if (!provider || provider.type !== "oauth") {
|
|
675
|
+
throw new ProviderError(`OAuth provider "${providerId}" not found`);
|
|
676
|
+
}
|
|
677
|
+
const oauthProvider = provider;
|
|
678
|
+
const tokenResponse = await exchangeCode(oauthProvider, params.code);
|
|
679
|
+
const userInfo = await getUserInfo(oauthProvider, tokenResponse.access_token);
|
|
680
|
+
if (!userInfo.emailVerified) {
|
|
681
|
+
throw new ProviderError("Email not verified on OAuth provider");
|
|
682
|
+
}
|
|
683
|
+
let [user] = await this.db.select().from(this.usersTable).where(eq3(this.usersTable.email, userInfo.email));
|
|
684
|
+
if (!user) {
|
|
685
|
+
const [newUser] = await this.db.insert(this.usersTable).values({
|
|
686
|
+
email: userInfo.email,
|
|
687
|
+
name: userInfo.name,
|
|
688
|
+
image: userInfo.image,
|
|
689
|
+
emailVerified: /* @__PURE__ */ new Date()
|
|
690
|
+
}).returning();
|
|
691
|
+
user = newUser;
|
|
692
|
+
const encryptedAccessToken = await encryptToken(tokenResponse.access_token, this.secret);
|
|
693
|
+
const encryptedRefreshToken = tokenResponse.refresh_token ? await encryptToken(tokenResponse.refresh_token, this.secret) : null;
|
|
694
|
+
await this.db.insert(this.accountsTable).values({
|
|
695
|
+
userId: user.id,
|
|
696
|
+
provider: providerId,
|
|
697
|
+
providerAccountId: userInfo.id,
|
|
698
|
+
accessToken: encryptedAccessToken,
|
|
699
|
+
refreshToken: encryptedRefreshToken
|
|
700
|
+
});
|
|
701
|
+
await this.config.callbacks?.onRegister?.(this.sanitizeUser(user));
|
|
702
|
+
} else {
|
|
703
|
+
const [existingAccount] = await this.db.select().from(this.accountsTable).where(
|
|
704
|
+
eq3(this.accountsTable.userId, user.id) && eq3(this.accountsTable.provider, providerId)
|
|
705
|
+
);
|
|
706
|
+
if (existingAccount) {
|
|
707
|
+
const encryptedAccessToken = await encryptToken(tokenResponse.access_token, this.secret);
|
|
708
|
+
const encryptedRefreshToken = tokenResponse.refresh_token ? await encryptToken(tokenResponse.refresh_token, this.secret) : null;
|
|
709
|
+
await this.db.update(this.accountsTable).set({
|
|
710
|
+
accessToken: encryptedAccessToken,
|
|
711
|
+
refreshToken: encryptedRefreshToken
|
|
712
|
+
}).where(eq3(this.accountsTable.id, existingAccount.id));
|
|
713
|
+
} else {
|
|
714
|
+
const encryptedAccessToken = await encryptToken(tokenResponse.access_token, this.secret);
|
|
715
|
+
const encryptedRefreshToken = tokenResponse.refresh_token ? await encryptToken(tokenResponse.refresh_token, this.secret) : null;
|
|
716
|
+
await this.db.insert(this.accountsTable).values({
|
|
717
|
+
userId: user.id,
|
|
718
|
+
provider: providerId,
|
|
719
|
+
providerAccountId: userInfo.id,
|
|
720
|
+
accessToken: encryptedAccessToken,
|
|
721
|
+
refreshToken: encryptedRefreshToken
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const sanitizedUser = this.sanitizeUser(user);
|
|
726
|
+
const result = await createSession(
|
|
727
|
+
{
|
|
728
|
+
db: this.db,
|
|
729
|
+
secret: this.secret,
|
|
730
|
+
strategy: this.sessionConfig.strategy,
|
|
731
|
+
maxAge: this.sessionConfig.maxAge,
|
|
732
|
+
sessionsTable: this.sessionsTable,
|
|
733
|
+
usersTable: this.usersTable
|
|
734
|
+
},
|
|
735
|
+
sanitizedUser
|
|
736
|
+
);
|
|
737
|
+
await this.config.callbacks?.onLogin?.(sanitizedUser);
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
// Session
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
async getSession(request) {
|
|
744
|
+
const token = this.extractToken(request);
|
|
745
|
+
if (!token) return null;
|
|
746
|
+
const session = await validateSession(
|
|
747
|
+
{
|
|
748
|
+
db: this.db,
|
|
749
|
+
secret: this.secret,
|
|
750
|
+
strategy: this.sessionConfig.strategy,
|
|
751
|
+
maxAge: this.sessionConfig.maxAge,
|
|
752
|
+
sessionsTable: this.sessionsTable,
|
|
753
|
+
usersTable: this.usersTable
|
|
754
|
+
},
|
|
755
|
+
token
|
|
756
|
+
);
|
|
757
|
+
if (!session) return null;
|
|
758
|
+
const enriched = await this.config.callbacks?.onSession?.(session);
|
|
759
|
+
return enriched ?? session;
|
|
760
|
+
}
|
|
761
|
+
async logout(token) {
|
|
762
|
+
const session = await validateSession(
|
|
763
|
+
{
|
|
764
|
+
db: this.db,
|
|
765
|
+
secret: this.secret,
|
|
766
|
+
strategy: this.sessionConfig.strategy,
|
|
767
|
+
maxAge: this.sessionConfig.maxAge,
|
|
768
|
+
sessionsTable: this.sessionsTable,
|
|
769
|
+
usersTable: this.usersTable
|
|
770
|
+
},
|
|
771
|
+
token
|
|
772
|
+
);
|
|
773
|
+
if (session) {
|
|
774
|
+
await this.config.callbacks?.onLogout?.(session);
|
|
775
|
+
}
|
|
776
|
+
await revokeSession(
|
|
777
|
+
{
|
|
778
|
+
db: this.db,
|
|
779
|
+
secret: this.secret,
|
|
780
|
+
strategy: this.sessionConfig.strategy,
|
|
781
|
+
maxAge: this.sessionConfig.maxAge,
|
|
782
|
+
sessionsTable: this.sessionsTable,
|
|
783
|
+
usersTable: this.usersTable
|
|
784
|
+
},
|
|
785
|
+
token
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
// 2FA
|
|
790
|
+
// ---------------------------------------------------------------------------
|
|
791
|
+
twoFactor = {
|
|
792
|
+
generate: async (userId) => {
|
|
793
|
+
const secret = generateSecret();
|
|
794
|
+
const otpauthUrl = generateURI(secret, userId, "MountSQLi");
|
|
795
|
+
await this.db.update(this.usersTable).set({ twoFactorSecret: secret }).where(eq3(this.usersTable.id, userId));
|
|
796
|
+
return { secret, otpauthUrl };
|
|
797
|
+
},
|
|
798
|
+
enable: async (userId, code) => {
|
|
799
|
+
const [user] = await this.db.select().from(this.usersTable).where(eq3(this.usersTable.id, userId));
|
|
800
|
+
if (!user?.twoFactorSecret) {
|
|
801
|
+
throw new UserNotFoundError();
|
|
802
|
+
}
|
|
803
|
+
if (!verifyCode(user.twoFactorSecret, code)) {
|
|
804
|
+
throw new TwoFactorInvalidError();
|
|
805
|
+
}
|
|
806
|
+
await this.db.update(this.usersTable).set({ twoFactorEnabled: true }).where(eq3(this.usersTable.id, userId));
|
|
807
|
+
},
|
|
808
|
+
disable: async (userId) => {
|
|
809
|
+
await this.db.update(this.usersTable).set({ twoFactorEnabled: false, twoFactorSecret: null }).where(eq3(this.usersTable.id, userId));
|
|
810
|
+
},
|
|
811
|
+
verify: (secret, code) => {
|
|
812
|
+
return verifyCode(secret, code);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
// ---------------------------------------------------------------------------
|
|
816
|
+
// Password Reset
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
passwordReset = {
|
|
819
|
+
create: async (email) => {
|
|
820
|
+
const [user] = await this.db.select().from(this.usersTable).where(eq3(this.usersTable.email, email));
|
|
821
|
+
if (!user) {
|
|
822
|
+
return "If an account exists, a reset email has been sent.";
|
|
823
|
+
}
|
|
824
|
+
const token = generateSecret(32);
|
|
825
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1e3);
|
|
826
|
+
await this.db.insert(this.verificationTable).values({
|
|
827
|
+
identifier: email,
|
|
828
|
+
token,
|
|
829
|
+
expiresAt
|
|
830
|
+
});
|
|
831
|
+
await sendPasswordResetEmail(this.emailAdapter, email, token);
|
|
832
|
+
return "If an account exists, a reset email has been sent.";
|
|
833
|
+
},
|
|
834
|
+
verify: async (token, newPassword) => {
|
|
835
|
+
const [verification] = await this.db.select().from(this.verificationTable).where(eq3(this.verificationTable.token, token));
|
|
836
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) {
|
|
837
|
+
throw new UnauthorizedError("Invalid or expired reset token");
|
|
838
|
+
}
|
|
839
|
+
const hashedPassword = await hashPassword(newPassword);
|
|
840
|
+
await this.db.update(this.usersTable).set({ password: hashedPassword, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(this.usersTable.email, verification.identifier));
|
|
841
|
+
await this.db.delete(this.verificationTable).where(eq3(this.verificationTable.token, token));
|
|
842
|
+
await revokeAllSessions(
|
|
843
|
+
{
|
|
844
|
+
db: this.db,
|
|
845
|
+
secret: this.secret,
|
|
846
|
+
strategy: this.sessionConfig.strategy,
|
|
847
|
+
maxAge: this.sessionConfig.maxAge,
|
|
848
|
+
sessionsTable: this.sessionsTable,
|
|
849
|
+
usersTable: this.usersTable
|
|
850
|
+
},
|
|
851
|
+
(await this.db.select().from(this.usersTable).where(eq3(this.usersTable.email, verification.identifier)))[0]?.id
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
// Email Verification
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
emailVerification = {
|
|
859
|
+
create: async (userId) => {
|
|
860
|
+
const [user] = await this.db.select().from(this.usersTable).where(eq3(this.usersTable.id, userId));
|
|
861
|
+
if (!user) throw new UserNotFoundError();
|
|
862
|
+
const token = generateSecret(32);
|
|
863
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1e3);
|
|
864
|
+
await this.db.insert(this.verificationTable).values({
|
|
865
|
+
identifier: user.email,
|
|
866
|
+
token,
|
|
867
|
+
expiresAt
|
|
868
|
+
});
|
|
869
|
+
await sendVerificationEmail(this.emailAdapter, user.email, token);
|
|
870
|
+
return token;
|
|
871
|
+
},
|
|
872
|
+
verify: async (token) => {
|
|
873
|
+
const [verification] = await this.db.select().from(this.verificationTable).where(eq3(this.verificationTable.token, token));
|
|
874
|
+
if (!verification || verification.expiresAt < /* @__PURE__ */ new Date()) {
|
|
875
|
+
throw new UnauthorizedError("Invalid or expired verification token");
|
|
876
|
+
}
|
|
877
|
+
await this.db.update(this.usersTable).set({ emailVerified: /* @__PURE__ */ new Date() }).where(eq3(this.usersTable.email, verification.identifier));
|
|
878
|
+
await this.db.delete(this.verificationTable).where(eq3(this.verificationTable.token, token));
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
// RBAC
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
rbac = {
|
|
885
|
+
createRole: (data) => createRole({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, data),
|
|
886
|
+
deleteRole: (roleId) => deleteRole({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, roleId),
|
|
887
|
+
assignRole: (userId, roleName) => assignRole({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, userId, roleName),
|
|
888
|
+
removeRole: (userId, roleName) => removeRole({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, userId, roleName),
|
|
889
|
+
getUserRoles: (userId) => getUserRoles({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, userId),
|
|
890
|
+
hasRole: (userId, roleName) => hasRole({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, userId, roleName),
|
|
891
|
+
authorize: (userId, permission) => hasPermission({ db: this.db, rolesTable: this.rolesTable, userRolesTable: this.userRolesTable }, userId, permission)
|
|
892
|
+
};
|
|
893
|
+
// ---------------------------------------------------------------------------
|
|
894
|
+
// Helpers
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
isValidEmail(email) {
|
|
897
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254;
|
|
898
|
+
}
|
|
899
|
+
isValidPassword(password) {
|
|
900
|
+
const errors = [];
|
|
901
|
+
if (password.length < 8) errors.push("Password must be at least 8 characters");
|
|
902
|
+
if (password.length > 128) errors.push("Password must be at most 128 characters");
|
|
903
|
+
if (!/[A-Z]/.test(password)) errors.push("Password must contain an uppercase letter");
|
|
904
|
+
if (!/[a-z]/.test(password)) errors.push("Password must contain a lowercase letter");
|
|
905
|
+
if (!/[0-9]/.test(password)) errors.push("Password must contain a number");
|
|
906
|
+
return { valid: errors.length === 0, errors };
|
|
907
|
+
}
|
|
908
|
+
/** Generate a CSRF token for a session */
|
|
909
|
+
generateCsrfToken(sessionToken) {
|
|
910
|
+
return createHmac2("sha256", this.secret).update(sessionToken).digest("hex");
|
|
911
|
+
}
|
|
912
|
+
/** Validate a CSRF token */
|
|
913
|
+
validateCsrfToken(sessionToken, csrfToken) {
|
|
914
|
+
const expected = this.generateCsrfToken(sessionToken);
|
|
915
|
+
const expectedBuf = Buffer.from(expected);
|
|
916
|
+
const inputBuf = Buffer.from(csrfToken);
|
|
917
|
+
if (expectedBuf.length !== inputBuf.length) return false;
|
|
918
|
+
const { timingSafeEqual: timingSafeEqual2 } = __require("crypto");
|
|
919
|
+
return timingSafeEqual2(expectedBuf, inputBuf);
|
|
920
|
+
}
|
|
921
|
+
extractToken(request) {
|
|
922
|
+
const authHeader = request.headers?.authorization;
|
|
923
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
924
|
+
return authHeader.slice(7);
|
|
925
|
+
}
|
|
926
|
+
const cookieHeader = request.headers?.cookie;
|
|
927
|
+
if (cookieHeader) {
|
|
928
|
+
const match = cookieHeader.match(/auth-token=([^;]+)/);
|
|
929
|
+
if (match) return match[1] ?? null;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
sanitizeUser(user) {
|
|
934
|
+
return {
|
|
935
|
+
id: user.id,
|
|
936
|
+
email: user.email,
|
|
937
|
+
emailVerified: user.emailVerified,
|
|
938
|
+
name: user.name,
|
|
939
|
+
image: user.image,
|
|
940
|
+
twoFactorEnabled: user.twoFactorEnabled,
|
|
941
|
+
createdAt: user.createdAt,
|
|
942
|
+
updatedAt: user.updatedAt
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// src/schema/user.ts
|
|
948
|
+
import { pgTable, uuid, text, boolean, timestamp } from "@mountsqli/pg";
|
|
949
|
+
var authUsers = pgTable("auth_users", {
|
|
950
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
951
|
+
email: text("email").notNull().unique(),
|
|
952
|
+
emailVerified: timestamp("email_verified", { withTimezone: true }),
|
|
953
|
+
password: text("password"),
|
|
954
|
+
// nullable — null for OAuth-only users
|
|
955
|
+
name: text("name"),
|
|
956
|
+
image: text("image"),
|
|
957
|
+
twoFactorEnabled: boolean("two_factor_enabled").default(false).notNull(),
|
|
958
|
+
twoFactorSecret: text("two_factor_secret"),
|
|
959
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
960
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// src/schema/session.ts
|
|
964
|
+
import { pgTable as pgTable2, uuid as uuid2, text as text2, timestamp as timestamp2 } from "@mountsqli/pg";
|
|
965
|
+
var authSessions = pgTable2("auth_sessions", {
|
|
966
|
+
id: uuid2("id").defaultRandom().primaryKey(),
|
|
967
|
+
userId: uuid2("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }),
|
|
968
|
+
token: text2("token").notNull().unique(),
|
|
969
|
+
expiresAt: timestamp2("expires_at", { withTimezone: true }).notNull(),
|
|
970
|
+
ipAddress: text2("ip_address"),
|
|
971
|
+
userAgent: text2("user_agent")
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// src/schema/account.ts
|
|
975
|
+
import { pgTable as pgTable3, uuid as uuid3, text as text3, timestamp as timestamp3 } from "@mountsqli/pg";
|
|
976
|
+
var authAccounts = pgTable3("auth_accounts", {
|
|
977
|
+
id: uuid3("id").defaultRandom().primaryKey(),
|
|
978
|
+
userId: uuid3("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }),
|
|
979
|
+
provider: text3("provider").notNull(),
|
|
980
|
+
providerAccountId: text3("provider_account_id").notNull(),
|
|
981
|
+
accessToken: text3("access_token"),
|
|
982
|
+
refreshToken: text3("refresh_token"),
|
|
983
|
+
expiresAt: timestamp3("expires_at", { withTimezone: true }),
|
|
984
|
+
createdAt: timestamp3("created_at", { withTimezone: true }).defaultNow().notNull()
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// src/schema/verification.ts
|
|
988
|
+
import { pgTable as pgTable4, uuid as uuid4, text as text4, timestamp as timestamp4 } from "@mountsqli/pg";
|
|
989
|
+
var authVerificationTokens = pgTable4("auth_verification_tokens", {
|
|
990
|
+
id: uuid4("id").defaultRandom().primaryKey(),
|
|
991
|
+
identifier: text4("identifier").notNull(),
|
|
992
|
+
// email address
|
|
993
|
+
token: text4("token").notNull().unique(),
|
|
994
|
+
expiresAt: timestamp4("expires_at", { withTimezone: true }).notNull()
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// src/schema/role.ts
|
|
998
|
+
import { pgTable as pgTable5, uuid as uuid5, text as text5, json, timestamp as timestamp5 } from "@mountsqli/pg";
|
|
999
|
+
var authRoles = pgTable5("auth_roles", {
|
|
1000
|
+
id: uuid5("id").defaultRandom().primaryKey(),
|
|
1001
|
+
name: text5("name").notNull().unique(),
|
|
1002
|
+
permissions: json("permissions").$type().default([]).notNull(),
|
|
1003
|
+
createdAt: timestamp5("created_at", { withTimezone: true }).defaultNow().notNull()
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// src/schema/user-role.ts
|
|
1007
|
+
import { pgTable as pgTable6, uuid as uuid6 } from "@mountsqli/pg";
|
|
1008
|
+
var authUserRoles = pgTable6("auth_user_roles", {
|
|
1009
|
+
userId: uuid6("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }),
|
|
1010
|
+
roleId: uuid6("role_id").notNull().references(() => authRoles.id, { onDelete: "cascade" })
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// src/providers/credentials.ts
|
|
1014
|
+
function credentials(config) {
|
|
1015
|
+
return {
|
|
1016
|
+
id: "credentials",
|
|
1017
|
+
name: "Email & Password",
|
|
1018
|
+
type: "credentials",
|
|
1019
|
+
authorize: config?.authorize ?? (async () => null)
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/providers/google.ts
|
|
1024
|
+
function google(config) {
|
|
1025
|
+
return {
|
|
1026
|
+
id: "google",
|
|
1027
|
+
name: "Google",
|
|
1028
|
+
type: "oauth",
|
|
1029
|
+
clientId: config.clientId,
|
|
1030
|
+
clientSecret: config.clientSecret,
|
|
1031
|
+
scope: config.scope ?? ["openid", "email", "profile"],
|
|
1032
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1033
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1034
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo"
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/providers/github.ts
|
|
1039
|
+
function github(config) {
|
|
1040
|
+
return {
|
|
1041
|
+
id: "github",
|
|
1042
|
+
name: "GitHub",
|
|
1043
|
+
type: "oauth",
|
|
1044
|
+
clientId: config.clientId,
|
|
1045
|
+
clientSecret: config.clientSecret,
|
|
1046
|
+
scope: config.scope ?? ["read:user", "user:email"],
|
|
1047
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
1048
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
1049
|
+
userInfoUrl: "https://api.github.com/user"
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/middleware/generic.ts
|
|
1054
|
+
async function resolveAuth(auth, request, options) {
|
|
1055
|
+
const session = await auth.getSession(request);
|
|
1056
|
+
if (options?.required && !session) {
|
|
1057
|
+
throw new UnauthorizedError("Authentication required");
|
|
1058
|
+
}
|
|
1059
|
+
if (options?.requiredRole && session) {
|
|
1060
|
+
const hasRole2 = await auth.rbac.hasRole(session.user.id, options.requiredRole);
|
|
1061
|
+
if (!hasRole2) {
|
|
1062
|
+
throw new ForbiddenError(`Role "${options.requiredRole}" required`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (options?.requiredPermission && session) {
|
|
1066
|
+
const hasPermission2 = await auth.rbac.authorize(session.user.id, options.requiredPermission);
|
|
1067
|
+
if (!hasPermission2) {
|
|
1068
|
+
throw new ForbiddenError(`Permission "${options.requiredPermission}" required`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
user: session?.user ?? null,
|
|
1073
|
+
session
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/middleware/express.ts
|
|
1078
|
+
function expressMiddleware(auth, options) {
|
|
1079
|
+
return async (req, res, next) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const result = await resolveAuth(auth, { headers: req.headers }, options);
|
|
1082
|
+
req.auth = result;
|
|
1083
|
+
next();
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
if (options?.onError) {
|
|
1086
|
+
return options.onError(err, req, res, next);
|
|
1087
|
+
}
|
|
1088
|
+
next(err);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function requireAuth(auth, options) {
|
|
1093
|
+
return async (req, res, next) => {
|
|
1094
|
+
try {
|
|
1095
|
+
const result = await resolveAuth(auth, { headers: req.headers }, {
|
|
1096
|
+
...options,
|
|
1097
|
+
required: true
|
|
1098
|
+
});
|
|
1099
|
+
req.auth = result;
|
|
1100
|
+
next();
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
if (options?.onError) {
|
|
1103
|
+
return options.onError(err, req, res, next);
|
|
1104
|
+
}
|
|
1105
|
+
next(err);
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/middleware/nextjs.ts
|
|
1111
|
+
function nextjsMiddleware(auth, options) {
|
|
1112
|
+
return async (request) => {
|
|
1113
|
+
const result = await resolveAuth(auth, { headers: request.headers });
|
|
1114
|
+
if (options?.authorized) {
|
|
1115
|
+
const isAuthorized = await options.authorized(result.session, request);
|
|
1116
|
+
if (!isAuthorized) {
|
|
1117
|
+
return { redirect: options.pages?.signIn ?? "/auth/signin" };
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (options?.required && !result.session) {
|
|
1121
|
+
return { redirect: options.pages?.signIn ?? "/auth/signin" };
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function nextjsApiAuth(auth) {
|
|
1127
|
+
return async (request) => {
|
|
1128
|
+
return resolveAuth(auth, { headers: Object.fromEntries(request.headers.entries()) });
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/middleware/fastify.ts
|
|
1133
|
+
function fastifyPlugin(auth, options) {
|
|
1134
|
+
return async function authPlugin(fastify) {
|
|
1135
|
+
fastify.decorateRequest("auth", null);
|
|
1136
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
1137
|
+
try {
|
|
1138
|
+
const result = await resolveAuth(auth, { headers: request.headers }, options);
|
|
1139
|
+
request.auth = result;
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
if (options?.onError) {
|
|
1142
|
+
return options.onError(err, request, reply);
|
|
1143
|
+
}
|
|
1144
|
+
throw err;
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function requireAuthFastify(auth, options) {
|
|
1150
|
+
return async function(request, reply) {
|
|
1151
|
+
try {
|
|
1152
|
+
const result = await resolveAuth(auth, { headers: request.headers }, {
|
|
1153
|
+
...options,
|
|
1154
|
+
required: true
|
|
1155
|
+
});
|
|
1156
|
+
request.auth = result;
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
if (options?.onError) {
|
|
1159
|
+
return options.onError(err, request, reply);
|
|
1160
|
+
}
|
|
1161
|
+
throw err;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/middleware/hono.ts
|
|
1167
|
+
function honoMiddleware(auth, options) {
|
|
1168
|
+
return async (c, next) => {
|
|
1169
|
+
try {
|
|
1170
|
+
const result = await resolveAuth(auth, { headers: Object.fromEntries(c.req.raw.headers.entries()) }, options);
|
|
1171
|
+
c.set("auth", result);
|
|
1172
|
+
await next();
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
if (options?.onError) {
|
|
1175
|
+
return options.onError(err, c);
|
|
1176
|
+
}
|
|
1177
|
+
return c.json({ error: err.message }, 401);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function requireAuthHono(auth, options) {
|
|
1182
|
+
return async (c, next) => {
|
|
1183
|
+
try {
|
|
1184
|
+
const result = await resolveAuth(auth, { headers: Object.fromEntries(c.req.raw.headers.entries()) }, {
|
|
1185
|
+
...options,
|
|
1186
|
+
required: true
|
|
1187
|
+
});
|
|
1188
|
+
c.set("auth", result);
|
|
1189
|
+
await next();
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
if (options?.onError) {
|
|
1192
|
+
return options.onError(err, c);
|
|
1193
|
+
}
|
|
1194
|
+
return c.json({ error: err.message }, 401);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/index.ts
|
|
1200
|
+
function createAuth(config) {
|
|
1201
|
+
return new Auth(config);
|
|
1202
|
+
}
|
|
1203
|
+
export {
|
|
1204
|
+
Auth,
|
|
1205
|
+
AuthError,
|
|
1206
|
+
ConsoleEmailAdapter,
|
|
1207
|
+
EmailAlreadyExistsError,
|
|
1208
|
+
ForbiddenError,
|
|
1209
|
+
InvalidCredentialsError,
|
|
1210
|
+
InvalidTokenError,
|
|
1211
|
+
ProviderError,
|
|
1212
|
+
SessionExpiredError,
|
|
1213
|
+
TwoFactorInvalidError,
|
|
1214
|
+
TwoFactorRequiredError,
|
|
1215
|
+
UnauthorizedError,
|
|
1216
|
+
UserNotFoundError,
|
|
1217
|
+
authAccounts,
|
|
1218
|
+
authRoles,
|
|
1219
|
+
authSessions,
|
|
1220
|
+
authUserRoles,
|
|
1221
|
+
authUsers,
|
|
1222
|
+
authVerificationTokens,
|
|
1223
|
+
createAuth,
|
|
1224
|
+
createToken,
|
|
1225
|
+
credentials,
|
|
1226
|
+
decodeToken,
|
|
1227
|
+
expressMiddleware,
|
|
1228
|
+
fastifyPlugin,
|
|
1229
|
+
generateCode,
|
|
1230
|
+
generateSecret,
|
|
1231
|
+
generateURI,
|
|
1232
|
+
github,
|
|
1233
|
+
google,
|
|
1234
|
+
hashPassword,
|
|
1235
|
+
honoMiddleware,
|
|
1236
|
+
nextjsApiAuth,
|
|
1237
|
+
nextjsMiddleware,
|
|
1238
|
+
requireAuth,
|
|
1239
|
+
requireAuthFastify,
|
|
1240
|
+
requireAuthHono,
|
|
1241
|
+
resolveAuth,
|
|
1242
|
+
sendPasswordResetEmail,
|
|
1243
|
+
sendVerificationEmail,
|
|
1244
|
+
verifyCode,
|
|
1245
|
+
verifyPassword,
|
|
1246
|
+
verifyToken
|
|
1247
|
+
};
|