@lastshotlabs/bunshot 0.0.25 → 0.0.27
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/adapters/localStorage.js +20 -5
- package/dist/adapters/memoryAuth.d.ts +6 -0
- package/dist/adapters/memoryAuth.js +117 -2
- package/dist/adapters/mongoAuth.js +97 -1
- package/dist/adapters/sqliteAuth.d.ts +23 -0
- package/dist/adapters/sqliteAuth.js +153 -2
- package/dist/app.d.ts +105 -2
- package/dist/app.js +112 -9
- package/dist/index.d.ts +23 -4
- package/dist/index.js +13 -2
- package/dist/lib/HttpError.d.ts +2 -1
- package/dist/lib/HttpError.js +3 -1
- package/dist/lib/appConfig.d.ts +113 -0
- package/dist/lib/appConfig.js +38 -0
- package/dist/lib/auditLog.d.ts +6 -0
- package/dist/lib/auditLog.js +17 -0
- package/dist/lib/authAdapter.d.ts +71 -1
- package/dist/lib/authRateLimit.js +36 -0
- package/dist/lib/breachedPassword.d.ts +13 -0
- package/dist/lib/breachedPassword.js +48 -0
- package/dist/lib/captcha.d.ts +25 -0
- package/dist/lib/captcha.js +37 -0
- package/dist/lib/context.d.ts +5 -0
- package/dist/lib/credentialStuffing.d.ts +31 -0
- package/dist/lib/credentialStuffing.js +77 -0
- package/dist/lib/emailVerification.d.ts +6 -0
- package/dist/lib/emailVerification.js +46 -3
- package/dist/lib/jwks.d.ts +25 -0
- package/dist/lib/jwks.js +51 -0
- package/dist/lib/jwt.d.ts +15 -2
- package/dist/lib/jwt.js +92 -5
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +6 -0
- package/dist/lib/m2m.d.ts +29 -0
- package/dist/lib/m2m.js +48 -0
- package/dist/lib/mfaChallenge.d.ts +14 -1
- package/dist/lib/mfaChallenge.js +111 -6
- package/dist/lib/mongo.js +1 -1
- package/dist/lib/oauthCode.js +23 -18
- package/dist/lib/resetPassword.js +3 -1
- package/dist/lib/saml.d.ts +25 -0
- package/dist/lib/saml.js +64 -0
- package/dist/lib/scim.d.ts +44 -0
- package/dist/lib/scim.js +54 -0
- package/dist/lib/securityEvents.d.ts +28 -0
- package/dist/lib/securityEvents.js +26 -0
- package/dist/lib/session.d.ts +10 -0
- package/dist/lib/session.js +67 -5
- package/dist/lib/signing.js +5 -2
- package/dist/lib/suspension.d.ts +13 -0
- package/dist/lib/suspension.js +23 -0
- package/dist/lib/upload.d.ts +4 -0
- package/dist/lib/upload.js +26 -1
- package/dist/lib/uploadRegistry.d.ts +18 -0
- package/dist/lib/uploadRegistry.js +83 -0
- package/dist/lib/ws.js +7 -0
- package/dist/middleware/bearerAuth.js +1 -1
- package/dist/middleware/captcha.d.ts +10 -0
- package/dist/middleware/captcha.js +36 -0
- package/dist/middleware/csrf.js +8 -4
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/identify.js +40 -13
- package/dist/middleware/requestSigning.js +6 -5
- package/dist/middleware/requireMfaSetup.js +2 -1
- package/dist/middleware/requireScope.d.ts +10 -0
- package/dist/middleware/requireScope.js +25 -0
- package/dist/middleware/requireStepUp.d.ts +18 -0
- package/dist/middleware/requireStepUp.js +29 -0
- package/dist/middleware/scimAuth.d.ts +8 -0
- package/dist/middleware/scimAuth.js +29 -0
- package/dist/middleware/webhookAuth.d.ts +1 -1
- package/dist/middleware/webhookAuth.js +6 -5
- package/dist/models/AuthUser.d.ts +7 -0
- package/dist/models/AuthUser.js +7 -0
- package/dist/models/M2MClient.d.ts +18 -0
- package/dist/models/M2MClient.js +18 -0
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +155 -16
- package/dist/routes/jobs.js +21 -3
- package/dist/routes/m2m.d.ts +2 -0
- package/dist/routes/m2m.js +72 -0
- package/dist/routes/metrics.d.ts +1 -0
- package/dist/routes/metrics.js +3 -0
- package/dist/routes/mfa.js +9 -1
- package/dist/routes/oauth.js +6 -0
- package/dist/routes/oidc.d.ts +2 -0
- package/dist/routes/oidc.js +29 -0
- package/dist/routes/passkey.d.ts +1 -0
- package/dist/routes/passkey.js +157 -0
- package/dist/routes/saml.d.ts +2 -0
- package/dist/routes/saml.js +86 -0
- package/dist/routes/scim.d.ts +2 -0
- package/dist/routes/scim.js +255 -0
- package/dist/routes/uploads.d.ts +13 -1
- package/dist/routes/uploads.js +98 -6
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +101 -22
- package/dist/services/mfa.js +2 -2
- package/dist/ws/index.js +2 -1
- package/docs/sections/auth-flow/full.md +790 -779
- package/docs/sections/auth-security-examples/full.md +23 -0
- package/docs/sections/metrics/full.md +6 -2
- package/docs/sections/passkey-login/full.md +90 -0
- package/docs/sections/passkey-login/overview.md +1 -0
- package/docs/sections/uploads/full.md +11 -2
- package/docs/sections/webhook-auth/full.md +1 -1
- package/docs/sections/websocket/full.md +12 -0
- package/package.json +3 -2
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { unlink } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve, sep, dirname } from "node:path";
|
|
3
|
+
import { HttpError } from "../lib/HttpError";
|
|
4
|
+
function resolveKey(directory, key) {
|
|
5
|
+
if (!key || !key.trim())
|
|
6
|
+
throw new HttpError(400, "Invalid storage key");
|
|
7
|
+
const normalized = key.replace(/\\/g, "/");
|
|
8
|
+
if (normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || normalized.startsWith("//")) {
|
|
9
|
+
throw new HttpError(400, "Invalid storage key");
|
|
10
|
+
}
|
|
11
|
+
const root = resolve(directory);
|
|
12
|
+
const candidate = resolve(root, normalized);
|
|
13
|
+
if (candidate === root || !candidate.startsWith(root + sep)) {
|
|
14
|
+
throw new HttpError(400, "Invalid storage key");
|
|
15
|
+
}
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
3
18
|
export const localStorage = (config) => ({
|
|
4
19
|
async put(key, data, _meta) {
|
|
5
|
-
const filePath =
|
|
20
|
+
const filePath = resolveKey(config.directory, key);
|
|
6
21
|
// Ensure parent directory exists
|
|
7
|
-
const dir =
|
|
22
|
+
const dir = dirname(filePath);
|
|
8
23
|
if (dir) {
|
|
9
24
|
const { mkdir } = await import("node:fs/promises");
|
|
10
25
|
await mkdir(dir, { recursive: true });
|
|
@@ -24,7 +39,7 @@ export const localStorage = (config) => ({
|
|
|
24
39
|
return { ...(url !== undefined ? { url } : {}) };
|
|
25
40
|
},
|
|
26
41
|
async get(key) {
|
|
27
|
-
const filePath =
|
|
42
|
+
const filePath = resolveKey(config.directory, key);
|
|
28
43
|
const file = Bun.file(filePath);
|
|
29
44
|
const exists = await file.exists();
|
|
30
45
|
if (!exists)
|
|
@@ -33,7 +48,7 @@ export const localStorage = (config) => ({
|
|
|
33
48
|
return { stream, size: file.size };
|
|
34
49
|
},
|
|
35
50
|
async delete(key) {
|
|
36
|
-
const filePath =
|
|
51
|
+
const filePath = resolveKey(config.directory, key);
|
|
37
52
|
try {
|
|
38
53
|
await unlink(filePath);
|
|
39
54
|
}
|
|
@@ -12,6 +12,8 @@ export declare const memoryEvictOldestSession: (userId: string) => void;
|
|
|
12
12
|
export declare const memoryUpdateSessionLastActive: (sessionId: string) => void;
|
|
13
13
|
export declare const memoryGetSessionFingerprint: (sessionId: string) => string | null;
|
|
14
14
|
export declare const memorySetSessionFingerprint: (sessionId: string, fingerprint: string) => void;
|
|
15
|
+
export declare const memoryGetMfaVerifiedAt: (sessionId: string) => number | null;
|
|
16
|
+
export declare const memorySetMfaVerifiedAt: (sessionId: string, ts: number) => void;
|
|
15
17
|
export declare const memorySetRefreshToken: (sessionId: string, refreshToken: string) => void;
|
|
16
18
|
import type { RefreshResult } from "../lib/session";
|
|
17
19
|
export declare const memoryGetSessionByRefreshToken: (refreshToken: string) => RefreshResult | null;
|
|
@@ -31,6 +33,10 @@ export declare const memoryGetVerificationToken: (token: string) => {
|
|
|
31
33
|
email: string;
|
|
32
34
|
} | null;
|
|
33
35
|
export declare const memoryDeleteVerificationToken: (token: string) => void;
|
|
36
|
+
export declare const memoryConsumeVerificationToken: (token: string) => {
|
|
37
|
+
userId: string;
|
|
38
|
+
email: string;
|
|
39
|
+
} | null;
|
|
34
40
|
export declare const memoryCreateResetToken: (token: string, userId: string, email: string, ttlSeconds: number) => void;
|
|
35
41
|
export declare const memoryConsumeResetToken: (hash: string) => {
|
|
36
42
|
userId: string;
|
|
@@ -7,6 +7,7 @@ import { clearPresenceStore } from "../lib/wsPresence";
|
|
|
7
7
|
import { clearWsMessageMemoryStore } from "../lib/wsMessages";
|
|
8
8
|
import { clearHeartbeatState } from "../lib/wsHeartbeat";
|
|
9
9
|
import { clearMemoryUploadStore } from "./memoryStorage";
|
|
10
|
+
import { clearUploadRegistry } from "../lib/uploadRegistry";
|
|
10
11
|
const _users = new Map();
|
|
11
12
|
const _byEmail = new Map();
|
|
12
13
|
const _sessions = new Map(); // sessionId → session
|
|
@@ -21,6 +22,7 @@ const _oauthCodes = new Map();
|
|
|
21
22
|
const _tenantRoles = new Map(); // "userId:tenantId" → roles
|
|
22
23
|
const _groups = new Map(); // groupId → GroupRecord
|
|
23
24
|
const _groupMemberships = new Map();
|
|
25
|
+
const _m2mClients = new Map();
|
|
24
26
|
/** Reset all in-memory state. Useful for test isolation. */
|
|
25
27
|
export const clearMemoryStore = () => {
|
|
26
28
|
_users.clear();
|
|
@@ -37,6 +39,7 @@ export const clearMemoryStore = () => {
|
|
|
37
39
|
_verificationTokens.clear();
|
|
38
40
|
_resetTokens.clear();
|
|
39
41
|
_cancelTokens.clear();
|
|
42
|
+
_m2mClients.clear();
|
|
40
43
|
clearMemoryRateLimitStore();
|
|
41
44
|
clearMemoryMfaChallenges();
|
|
42
45
|
clearAuditLogMemoryStore();
|
|
@@ -44,6 +47,7 @@ export const clearMemoryStore = () => {
|
|
|
44
47
|
clearWsMessageMemoryStore();
|
|
45
48
|
clearHeartbeatState();
|
|
46
49
|
clearMemoryUploadStore();
|
|
50
|
+
clearUploadRegistry();
|
|
47
51
|
};
|
|
48
52
|
// ---------------------------------------------------------------------------
|
|
49
53
|
// Auth adapter
|
|
@@ -63,7 +67,7 @@ export const memoryAuthAdapter = {
|
|
|
63
67
|
if (_byEmail.has(normalised))
|
|
64
68
|
throw new HttpError(409, "Email already registered");
|
|
65
69
|
const id = crypto.randomUUID();
|
|
66
|
-
const user = { id, email: normalised, passwordHash, providerIds: [], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [], webauthnCredentials: [] };
|
|
70
|
+
const user = { id, email: normalised, passwordHash, providerIds: [], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [], webauthnCredentials: [], suspended: false };
|
|
67
71
|
_users.set(id, user);
|
|
68
72
|
_byEmail.set(normalised, id);
|
|
69
73
|
return { id };
|
|
@@ -89,7 +93,7 @@ export const memoryAuthAdapter = {
|
|
|
89
93
|
}
|
|
90
94
|
const id = crypto.randomUUID();
|
|
91
95
|
const email = profile.email ? profile.email.toLowerCase() : null;
|
|
92
|
-
const user = { id, email, passwordHash: null, providerIds: [key], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [], webauthnCredentials: [] };
|
|
96
|
+
const user = { id, email, passwordHash: null, providerIds: [key], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [], webauthnCredentials: [], suspended: false };
|
|
93
97
|
_users.set(id, user);
|
|
94
98
|
if (email)
|
|
95
99
|
_byEmail.set(email, id);
|
|
@@ -133,6 +137,12 @@ export const memoryAuthAdapter = {
|
|
|
133
137
|
email: user.email ?? undefined,
|
|
134
138
|
providerIds: [...user.providerIds],
|
|
135
139
|
emailVerified: user.emailVerified,
|
|
140
|
+
displayName: user.displayName,
|
|
141
|
+
firstName: user.firstName,
|
|
142
|
+
lastName: user.lastName,
|
|
143
|
+
externalId: user.externalId,
|
|
144
|
+
suspended: user.suspended,
|
|
145
|
+
suspendedReason: user.suspendedReason,
|
|
136
146
|
};
|
|
137
147
|
},
|
|
138
148
|
async unlinkProvider(userId, provider) {
|
|
@@ -258,6 +268,68 @@ export const memoryAuthAdapter = {
|
|
|
258
268
|
_tenantRoles.set(key, current.filter((r) => r !== role));
|
|
259
269
|
}
|
|
260
270
|
},
|
|
271
|
+
async setSuspended(userId, suspended, reason) {
|
|
272
|
+
const user = _users.get(userId);
|
|
273
|
+
if (!user)
|
|
274
|
+
return;
|
|
275
|
+
user.suspended = suspended;
|
|
276
|
+
if (suspended) {
|
|
277
|
+
user.suspendedAt = new Date();
|
|
278
|
+
user.suspendedReason = reason;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
user.suspendedAt = undefined;
|
|
282
|
+
user.suspendedReason = undefined;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
async getSuspended(userId) {
|
|
286
|
+
const user = _users.get(userId);
|
|
287
|
+
if (!user)
|
|
288
|
+
return null;
|
|
289
|
+
return { suspended: user.suspended, suspendedReason: user.suspendedReason };
|
|
290
|
+
},
|
|
291
|
+
async updateProfile(userId, fields) {
|
|
292
|
+
const user = _users.get(userId);
|
|
293
|
+
if (!user)
|
|
294
|
+
return;
|
|
295
|
+
if ("displayName" in fields)
|
|
296
|
+
user.displayName = fields.displayName;
|
|
297
|
+
if ("firstName" in fields)
|
|
298
|
+
user.firstName = fields.firstName;
|
|
299
|
+
if ("lastName" in fields)
|
|
300
|
+
user.lastName = fields.lastName;
|
|
301
|
+
if ("externalId" in fields)
|
|
302
|
+
user.externalId = fields.externalId;
|
|
303
|
+
},
|
|
304
|
+
async listUsers(query) {
|
|
305
|
+
let users = [..._users.values()];
|
|
306
|
+
if (query.email !== undefined)
|
|
307
|
+
users = users.filter((u) => u.email === query.email);
|
|
308
|
+
if (query.externalId !== undefined)
|
|
309
|
+
users = users.filter((u) => u.externalId === query.externalId);
|
|
310
|
+
if (query.suspended !== undefined)
|
|
311
|
+
users = users.filter((u) => u.suspended === query.suspended);
|
|
312
|
+
const totalResults = users.length;
|
|
313
|
+
const startIndex = query.startIndex ?? 0;
|
|
314
|
+
const count = query.count ?? 100;
|
|
315
|
+
const page = users.slice(startIndex, startIndex + count);
|
|
316
|
+
return {
|
|
317
|
+
users: page.map((u) => ({
|
|
318
|
+
id: u.id,
|
|
319
|
+
email: u.email ?? undefined,
|
|
320
|
+
displayName: u.displayName,
|
|
321
|
+
firstName: u.firstName,
|
|
322
|
+
lastName: u.lastName,
|
|
323
|
+
externalId: u.externalId,
|
|
324
|
+
suspended: u.suspended,
|
|
325
|
+
suspendedAt: u.suspendedAt,
|
|
326
|
+
suspendedReason: u.suspendedReason,
|
|
327
|
+
emailVerified: u.emailVerified,
|
|
328
|
+
providerIds: [...u.providerIds],
|
|
329
|
+
})),
|
|
330
|
+
totalResults,
|
|
331
|
+
};
|
|
332
|
+
},
|
|
261
333
|
// ---------------------------------------------------------------------------
|
|
262
334
|
// Groups
|
|
263
335
|
// ---------------------------------------------------------------------------
|
|
@@ -358,6 +430,32 @@ export const memoryAuthAdapter = {
|
|
|
358
430
|
]);
|
|
359
431
|
return [...new Set([...direct, ...groupRoles])];
|
|
360
432
|
},
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// M2M client credentials
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
async getM2MClient(clientId) {
|
|
437
|
+
for (const c of _m2mClients.values()) {
|
|
438
|
+
if (c.clientId === clientId && c.active)
|
|
439
|
+
return { ...c };
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
},
|
|
443
|
+
async createM2MClient(data) {
|
|
444
|
+
const id = crypto.randomUUID();
|
|
445
|
+
_m2mClients.set(id, { id, ...data, active: true });
|
|
446
|
+
return { id };
|
|
447
|
+
},
|
|
448
|
+
async deleteM2MClient(clientId) {
|
|
449
|
+
for (const [key, c] of _m2mClients.entries()) {
|
|
450
|
+
if (c.clientId === clientId) {
|
|
451
|
+
_m2mClients.delete(key);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
async listM2MClients() {
|
|
457
|
+
return Array.from(_m2mClients.values()).map(({ clientSecretHash: _, ...rest }) => rest);
|
|
458
|
+
},
|
|
361
459
|
};
|
|
362
460
|
// ---------------------------------------------------------------------------
|
|
363
461
|
// Session helpers (used by src/lib/session.ts)
|
|
@@ -473,6 +571,14 @@ export const memorySetSessionFingerprint = (sessionId, fingerprint) => {
|
|
|
473
571
|
if (entry)
|
|
474
572
|
entry.fingerprint = fingerprint;
|
|
475
573
|
};
|
|
574
|
+
export const memoryGetMfaVerifiedAt = (sessionId) => {
|
|
575
|
+
return _sessions.get(sessionId)?.mfaVerifiedAt ?? null;
|
|
576
|
+
};
|
|
577
|
+
export const memorySetMfaVerifiedAt = (sessionId, ts) => {
|
|
578
|
+
const entry = _sessions.get(sessionId);
|
|
579
|
+
if (entry)
|
|
580
|
+
entry.mfaVerifiedAt = ts;
|
|
581
|
+
};
|
|
476
582
|
export const memorySetRefreshToken = (sessionId, refreshToken) => {
|
|
477
583
|
const entry = _sessions.get(sessionId);
|
|
478
584
|
if (!entry)
|
|
@@ -579,6 +685,15 @@ export const memoryGetVerificationToken = (token) => {
|
|
|
579
685
|
export const memoryDeleteVerificationToken = (token) => {
|
|
580
686
|
_verificationTokens.delete(token);
|
|
581
687
|
};
|
|
688
|
+
export const memoryConsumeVerificationToken = (token) => {
|
|
689
|
+
const entry = _verificationTokens.get(token);
|
|
690
|
+
if (!entry || entry.expiresAt <= Date.now()) {
|
|
691
|
+
_verificationTokens.delete(token);
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
_verificationTokens.delete(token);
|
|
695
|
+
return { userId: entry.userId, email: entry.email };
|
|
696
|
+
};
|
|
582
697
|
// ---------------------------------------------------------------------------
|
|
583
698
|
// Password reset token helpers (used by src/lib/resetPassword.ts)
|
|
584
699
|
// ---------------------------------------------------------------------------
|
|
@@ -2,6 +2,7 @@ import { AuthUser } from "../models/AuthUser";
|
|
|
2
2
|
import { TenantRole } from "../models/TenantRole";
|
|
3
3
|
import { Group } from "../models/Group";
|
|
4
4
|
import { GroupMembership } from "../models/GroupMembership";
|
|
5
|
+
import { M2MClient } from "../models/M2MClient";
|
|
5
6
|
import { HttpError } from "../lib/HttpError";
|
|
6
7
|
export const mongoAuthAdapter = {
|
|
7
8
|
async findByEmail(email) {
|
|
@@ -64,13 +65,19 @@ export const mongoAuthAdapter = {
|
|
|
64
65
|
await AuthUser.findByIdAndUpdate(userId, { $pull: { roles: role } });
|
|
65
66
|
},
|
|
66
67
|
async getUser(userId) {
|
|
67
|
-
const user = await AuthUser.findById(userId, "email providerIds emailVerified").lean();
|
|
68
|
+
const user = await AuthUser.findById(userId, "email providerIds emailVerified displayName firstName lastName externalId suspended suspendedReason").lean();
|
|
68
69
|
if (!user)
|
|
69
70
|
return null;
|
|
70
71
|
return {
|
|
71
72
|
email: user.email,
|
|
72
73
|
providerIds: user.providerIds,
|
|
73
74
|
emailVerified: user.emailVerified ?? false,
|
|
75
|
+
displayName: user.displayName ?? undefined,
|
|
76
|
+
firstName: user.firstName ?? undefined,
|
|
77
|
+
lastName: user.lastName ?? undefined,
|
|
78
|
+
externalId: user.externalId ?? undefined,
|
|
79
|
+
suspended: user.suspended ?? false,
|
|
80
|
+
suspendedReason: user.suspendedReason ?? undefined,
|
|
74
81
|
};
|
|
75
82
|
},
|
|
76
83
|
async unlinkProvider(userId, provider) {
|
|
@@ -186,6 +193,62 @@ export const mongoAuthAdapter = {
|
|
|
186
193
|
async removeTenantRole(userId, tenantId, role) {
|
|
187
194
|
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $pull: { roles: role } });
|
|
188
195
|
},
|
|
196
|
+
async setSuspended(userId, suspended, reason) {
|
|
197
|
+
const update = { suspended };
|
|
198
|
+
if (suspended) {
|
|
199
|
+
update.suspendedAt = new Date();
|
|
200
|
+
update.suspendedReason = reason ?? null;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
update.suspendedAt = null;
|
|
204
|
+
update.suspendedReason = null;
|
|
205
|
+
}
|
|
206
|
+
await AuthUser.updateOne({ _id: userId }, { $set: update });
|
|
207
|
+
},
|
|
208
|
+
async getSuspended(userId) {
|
|
209
|
+
const user = await AuthUser.findById(userId, { suspended: 1, suspendedReason: 1 }).lean();
|
|
210
|
+
if (!user)
|
|
211
|
+
return null;
|
|
212
|
+
return { suspended: user.suspended ?? false, suspendedReason: user.suspendedReason ?? undefined };
|
|
213
|
+
},
|
|
214
|
+
async updateProfile(userId, fields) {
|
|
215
|
+
await AuthUser.updateOne({ _id: userId }, { $set: fields });
|
|
216
|
+
},
|
|
217
|
+
async listUsers(query) {
|
|
218
|
+
const filter = {};
|
|
219
|
+
if (query.email !== undefined)
|
|
220
|
+
filter.email = query.email;
|
|
221
|
+
if (query.externalId !== undefined)
|
|
222
|
+
filter.externalId = query.externalId;
|
|
223
|
+
if (query.suspended !== undefined)
|
|
224
|
+
filter.suspended = query.suspended;
|
|
225
|
+
const startIndex = query.startIndex ?? 0;
|
|
226
|
+
const count = query.count ?? 100;
|
|
227
|
+
const [users, totalResults] = await Promise.all([
|
|
228
|
+
AuthUser.find(filter, {
|
|
229
|
+
_id: 1, email: 1, displayName: 1, firstName: 1, lastName: 1,
|
|
230
|
+
externalId: 1, suspended: 1, suspendedAt: 1, suspendedReason: 1,
|
|
231
|
+
emailVerified: 1, providerIds: 1,
|
|
232
|
+
}).skip(startIndex).limit(count).lean(),
|
|
233
|
+
AuthUser.countDocuments(filter),
|
|
234
|
+
]);
|
|
235
|
+
return {
|
|
236
|
+
users: users.map((u) => ({
|
|
237
|
+
id: String(u._id),
|
|
238
|
+
email: u.email ?? undefined,
|
|
239
|
+
displayName: u.displayName ?? undefined,
|
|
240
|
+
firstName: u.firstName ?? undefined,
|
|
241
|
+
lastName: u.lastName ?? undefined,
|
|
242
|
+
externalId: u.externalId ?? undefined,
|
|
243
|
+
suspended: u.suspended ?? false,
|
|
244
|
+
suspendedAt: u.suspendedAt ?? undefined,
|
|
245
|
+
suspendedReason: u.suspendedReason ?? undefined,
|
|
246
|
+
emailVerified: u.emailVerified ?? undefined,
|
|
247
|
+
providerIds: u.providerIds ?? undefined,
|
|
248
|
+
})),
|
|
249
|
+
totalResults,
|
|
250
|
+
};
|
|
251
|
+
},
|
|
189
252
|
// ---------------------------------------------------------------------------
|
|
190
253
|
// Groups
|
|
191
254
|
// ---------------------------------------------------------------------------
|
|
@@ -292,6 +355,39 @@ export const mongoAuthAdapter = {
|
|
|
292
355
|
]);
|
|
293
356
|
return [...new Set([...direct, ...groupRoles])];
|
|
294
357
|
},
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// M2M client credentials
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
async getM2MClient(clientId) {
|
|
362
|
+
const client = await M2MClient.findOne({ clientId, active: true }).lean();
|
|
363
|
+
if (!client)
|
|
364
|
+
return null;
|
|
365
|
+
return {
|
|
366
|
+
id: String(client._id),
|
|
367
|
+
clientId: client.clientId,
|
|
368
|
+
name: client.name,
|
|
369
|
+
scopes: client.scopes,
|
|
370
|
+
active: client.active,
|
|
371
|
+
clientSecretHash: client.clientSecretHash,
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
async createM2MClient(data) {
|
|
375
|
+
const client = await M2MClient.create(data);
|
|
376
|
+
return { id: String(client._id) };
|
|
377
|
+
},
|
|
378
|
+
async deleteM2MClient(clientId) {
|
|
379
|
+
await M2MClient.deleteOne({ clientId });
|
|
380
|
+
},
|
|
381
|
+
async listM2MClients() {
|
|
382
|
+
const clients = await M2MClient.find({}).lean();
|
|
383
|
+
return clients.map((c) => ({
|
|
384
|
+
id: String(c._id),
|
|
385
|
+
clientId: c.clientId,
|
|
386
|
+
name: c.name,
|
|
387
|
+
scopes: c.scopes,
|
|
388
|
+
active: c.active,
|
|
389
|
+
}));
|
|
390
|
+
},
|
|
295
391
|
};
|
|
296
392
|
function mongoGroupToRecord(doc) {
|
|
297
393
|
return {
|
|
@@ -17,6 +17,8 @@ export declare const sqliteGetSessionByRefreshToken: (refreshToken: string) => R
|
|
|
17
17
|
export declare const sqliteRotateRefreshToken: (sessionId: string, newRefreshToken: string, newAccessToken: string) => void;
|
|
18
18
|
export declare const sqliteGetSessionFingerprint: (sessionId: string) => string | null;
|
|
19
19
|
export declare const sqliteSetSessionFingerprint: (sessionId: string, fingerprint: string) => void;
|
|
20
|
+
export declare const sqliteGetMfaVerifiedAt: (sessionId: string) => number | null;
|
|
21
|
+
export declare const sqliteSetMfaVerifiedAt: (sessionId: string, ts: number) => void;
|
|
20
22
|
export declare const sqliteStoreOAuthState: (state: string, codeVerifier?: string, linkUserId?: string) => void;
|
|
21
23
|
export declare const sqliteConsumeOAuthState: (state: string) => {
|
|
22
24
|
codeVerifier?: string;
|
|
@@ -33,6 +35,10 @@ export declare const sqliteGetVerificationToken: (token: string) => {
|
|
|
33
35
|
email: string;
|
|
34
36
|
} | null;
|
|
35
37
|
export declare const sqliteDeleteVerificationToken: (token: string) => void;
|
|
38
|
+
export declare const sqliteConsumeVerificationToken: (token: string) => {
|
|
39
|
+
userId: string;
|
|
40
|
+
email: string;
|
|
41
|
+
} | null;
|
|
36
42
|
export declare const sqliteCreateResetToken: (token: string, userId: string, email: string, ttlSeconds: number) => void;
|
|
37
43
|
export declare const sqliteConsumeResetToken: (hash: string) => {
|
|
38
44
|
userId: string;
|
|
@@ -47,3 +53,20 @@ import type { OAuthCodePayload } from "../lib/oauthCode";
|
|
|
47
53
|
export declare const sqliteStoreOAuthCode: (hash: string, payload: OAuthCodePayload, ttlSeconds: number) => void;
|
|
48
54
|
export declare const sqliteConsumeOAuthCode: (hash: string) => OAuthCodePayload | null;
|
|
49
55
|
export declare const startSqliteCleanup: (intervalMs?: number) => ReturnType<typeof setInterval>;
|
|
56
|
+
export declare const sqliteRegisterUpload: (record: {
|
|
57
|
+
key: string;
|
|
58
|
+
ownerUserId?: string;
|
|
59
|
+
tenantId?: string;
|
|
60
|
+
mimeType?: string;
|
|
61
|
+
bucket?: string;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
}) => void;
|
|
64
|
+
export declare const sqliteGetUploadRecord: (key: string) => {
|
|
65
|
+
key: string;
|
|
66
|
+
ownerUserId?: string;
|
|
67
|
+
tenantId?: string;
|
|
68
|
+
mimeType?: string;
|
|
69
|
+
bucket?: string;
|
|
70
|
+
createdAt: number;
|
|
71
|
+
} | null;
|
|
72
|
+
export declare const sqliteDeleteUploadRecord: (key: string) => boolean;
|
|
@@ -25,13 +25,35 @@ function initSchema(db) {
|
|
|
25
25
|
passwordHash TEXT,
|
|
26
26
|
providerIds TEXT NOT NULL DEFAULT '[]',
|
|
27
27
|
roles TEXT NOT NULL DEFAULT '[]',
|
|
28
|
-
emailVerified INTEGER NOT NULL DEFAULT 0
|
|
28
|
+
emailVerified INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
displayName TEXT,
|
|
30
|
+
firstName TEXT,
|
|
31
|
+
lastName TEXT,
|
|
32
|
+
externalId TEXT,
|
|
33
|
+
suspended INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
suspendedAt TEXT,
|
|
35
|
+
suspendedReason TEXT
|
|
29
36
|
)`);
|
|
30
37
|
// Add emailVerified to pre-existing databases that lack the column
|
|
31
38
|
try {
|
|
32
39
|
db.run("ALTER TABLE users ADD COLUMN emailVerified INTEGER NOT NULL DEFAULT 0");
|
|
33
40
|
}
|
|
34
41
|
catch { /* already exists */ }
|
|
42
|
+
// Add profile and suspension columns to pre-existing databases
|
|
43
|
+
for (const col of [
|
|
44
|
+
"ALTER TABLE users ADD COLUMN displayName TEXT",
|
|
45
|
+
"ALTER TABLE users ADD COLUMN firstName TEXT",
|
|
46
|
+
"ALTER TABLE users ADD COLUMN lastName TEXT",
|
|
47
|
+
"ALTER TABLE users ADD COLUMN externalId TEXT",
|
|
48
|
+
"ALTER TABLE users ADD COLUMN suspended INTEGER NOT NULL DEFAULT 0",
|
|
49
|
+
"ALTER TABLE users ADD COLUMN suspendedAt TEXT",
|
|
50
|
+
"ALTER TABLE users ADD COLUMN suspendedReason TEXT",
|
|
51
|
+
]) {
|
|
52
|
+
try {
|
|
53
|
+
db.run(col);
|
|
54
|
+
}
|
|
55
|
+
catch { /* column already exists */ }
|
|
56
|
+
}
|
|
35
57
|
// Add MFA columns to pre-existing databases
|
|
36
58
|
try {
|
|
37
59
|
db.run("ALTER TABLE users ADD COLUMN mfaSecret TEXT");
|
|
@@ -82,6 +104,10 @@ function initSchema(db) {
|
|
|
82
104
|
db.run("ALTER TABLE sessions ADD COLUMN fingerprint TEXT");
|
|
83
105
|
}
|
|
84
106
|
catch { /* already exists */ }
|
|
107
|
+
try {
|
|
108
|
+
db.run("ALTER TABLE sessions ADD COLUMN mfaVerifiedAt INTEGER");
|
|
109
|
+
}
|
|
110
|
+
catch { /* already exists */ }
|
|
85
111
|
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_refreshToken ON sessions(refreshToken) WHERE refreshToken IS NOT NULL");
|
|
86
112
|
db.run(`CREATE TABLE IF NOT EXISTS oauth_states (
|
|
87
113
|
state TEXT PRIMARY KEY,
|
|
@@ -166,6 +192,14 @@ function initSchema(db) {
|
|
|
166
192
|
jobId TEXT NOT NULL,
|
|
167
193
|
expiresAt INTEGER NOT NULL
|
|
168
194
|
)`);
|
|
195
|
+
db.run(`CREATE TABLE IF NOT EXISTS upload_registry (
|
|
196
|
+
key TEXT PRIMARY KEY,
|
|
197
|
+
ownerUserId TEXT,
|
|
198
|
+
tenantId TEXT,
|
|
199
|
+
mimeType TEXT,
|
|
200
|
+
bucket TEXT,
|
|
201
|
+
createdAt INTEGER NOT NULL
|
|
202
|
+
)`);
|
|
169
203
|
}
|
|
170
204
|
// ---------------------------------------------------------------------------
|
|
171
205
|
// Auth adapter
|
|
@@ -244,13 +278,19 @@ export const sqliteAuthAdapter = {
|
|
|
244
278
|
db.run("UPDATE users SET roles = ? WHERE id = ?", [JSON.stringify(roles.filter((r) => r !== role)), userId]);
|
|
245
279
|
},
|
|
246
280
|
async getUser(userId) {
|
|
247
|
-
const row = getDb().query("SELECT email, providerIds, emailVerified FROM users WHERE id = ?").get(userId);
|
|
281
|
+
const row = getDb().query("SELECT email, providerIds, emailVerified, displayName, firstName, lastName, externalId, suspended, suspendedReason FROM users WHERE id = ?").get(userId);
|
|
248
282
|
if (!row)
|
|
249
283
|
return null;
|
|
250
284
|
return {
|
|
251
285
|
email: row.email ?? undefined,
|
|
252
286
|
providerIds: JSON.parse(row.providerIds),
|
|
253
287
|
emailVerified: row.emailVerified === 1,
|
|
288
|
+
displayName: row.displayName ?? undefined,
|
|
289
|
+
firstName: row.firstName ?? undefined,
|
|
290
|
+
lastName: row.lastName ?? undefined,
|
|
291
|
+
externalId: row.externalId ?? undefined,
|
|
292
|
+
suspended: row.suspended === 1,
|
|
293
|
+
suspendedReason: row.suspendedReason ?? undefined,
|
|
254
294
|
};
|
|
255
295
|
},
|
|
256
296
|
async unlinkProvider(userId, provider) {
|
|
@@ -364,6 +404,85 @@ export const sqliteAuthAdapter = {
|
|
|
364
404
|
async removeTenantRole(userId, tenantId, role) {
|
|
365
405
|
getDb().run("DELETE FROM tenant_roles WHERE userId = ? AND tenantId = ? AND role = ?", [userId, tenantId, role]);
|
|
366
406
|
},
|
|
407
|
+
async setSuspended(userId, suspended, reason) {
|
|
408
|
+
if (suspended) {
|
|
409
|
+
getDb().run("UPDATE users SET suspended = 1, suspendedAt = ?, suspendedReason = ? WHERE id = ?", [new Date().toISOString(), reason ?? null, userId]);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
getDb().run("UPDATE users SET suspended = 0, suspendedAt = NULL, suspendedReason = NULL WHERE id = ?", [userId]);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
async getSuspended(userId) {
|
|
416
|
+
const row = getDb().query("SELECT suspended, suspendedReason FROM users WHERE id = ?").get(userId);
|
|
417
|
+
if (!row)
|
|
418
|
+
return null;
|
|
419
|
+
return { suspended: row.suspended === 1, suspendedReason: row.suspendedReason ?? undefined };
|
|
420
|
+
},
|
|
421
|
+
async updateProfile(userId, fields) {
|
|
422
|
+
const sets = [];
|
|
423
|
+
const params = [];
|
|
424
|
+
if ("displayName" in fields) {
|
|
425
|
+
sets.push("displayName = ?");
|
|
426
|
+
params.push(fields.displayName ?? null);
|
|
427
|
+
}
|
|
428
|
+
if ("firstName" in fields) {
|
|
429
|
+
sets.push("firstName = ?");
|
|
430
|
+
params.push(fields.firstName ?? null);
|
|
431
|
+
}
|
|
432
|
+
if ("lastName" in fields) {
|
|
433
|
+
sets.push("lastName = ?");
|
|
434
|
+
params.push(fields.lastName ?? null);
|
|
435
|
+
}
|
|
436
|
+
if ("externalId" in fields) {
|
|
437
|
+
sets.push("externalId = ?");
|
|
438
|
+
params.push(fields.externalId ?? null);
|
|
439
|
+
}
|
|
440
|
+
if (sets.length === 0)
|
|
441
|
+
return;
|
|
442
|
+
params.push(userId);
|
|
443
|
+
getDb().run(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
444
|
+
},
|
|
445
|
+
async listUsers(query) {
|
|
446
|
+
const db = getDb();
|
|
447
|
+
const conditions = [];
|
|
448
|
+
const params = [];
|
|
449
|
+
if (query.email !== undefined) {
|
|
450
|
+
conditions.push("email = ?");
|
|
451
|
+
params.push(query.email);
|
|
452
|
+
}
|
|
453
|
+
if (query.externalId !== undefined) {
|
|
454
|
+
conditions.push("externalId = ?");
|
|
455
|
+
params.push(query.externalId);
|
|
456
|
+
}
|
|
457
|
+
if (query.suspended !== undefined) {
|
|
458
|
+
conditions.push("suspended = ?");
|
|
459
|
+
params.push(query.suspended ? 1 : 0);
|
|
460
|
+
}
|
|
461
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
462
|
+
const startIndex = query.startIndex ?? 0;
|
|
463
|
+
const count = query.count ?? 100;
|
|
464
|
+
const queryParams = [...params, count, startIndex];
|
|
465
|
+
const countParams = params;
|
|
466
|
+
const rows = db.prepare(`SELECT id, email, displayName, firstName, lastName, externalId, suspended, suspendedAt, suspendedReason, emailVerified, providerIds FROM users ${where} LIMIT ? OFFSET ?`).all(...queryParams);
|
|
467
|
+
const totalRow = db.prepare(`SELECT COUNT(*) as c FROM users ${where}`).get(...countParams);
|
|
468
|
+
const totalResults = totalRow?.c ?? 0;
|
|
469
|
+
return {
|
|
470
|
+
users: rows.map((r) => ({
|
|
471
|
+
id: r.id,
|
|
472
|
+
email: r.email ?? undefined,
|
|
473
|
+
displayName: r.displayName ?? undefined,
|
|
474
|
+
firstName: r.firstName ?? undefined,
|
|
475
|
+
lastName: r.lastName ?? undefined,
|
|
476
|
+
externalId: r.externalId ?? undefined,
|
|
477
|
+
suspended: r.suspended === 1,
|
|
478
|
+
suspendedAt: r.suspendedAt ? new Date(r.suspendedAt) : undefined,
|
|
479
|
+
suspendedReason: r.suspendedReason ?? undefined,
|
|
480
|
+
emailVerified: r.emailVerified === 1,
|
|
481
|
+
providerIds: JSON.parse(r.providerIds),
|
|
482
|
+
})),
|
|
483
|
+
totalResults,
|
|
484
|
+
};
|
|
485
|
+
},
|
|
367
486
|
// ---------------------------------------------------------------------------
|
|
368
487
|
// Groups
|
|
369
488
|
// ---------------------------------------------------------------------------
|
|
@@ -601,6 +720,13 @@ export const sqliteGetSessionFingerprint = (sessionId) => {
|
|
|
601
720
|
export const sqliteSetSessionFingerprint = (sessionId, fingerprint) => {
|
|
602
721
|
getDb().run("UPDATE sessions SET fingerprint = ? WHERE sessionId = ?", [fingerprint, sessionId]);
|
|
603
722
|
};
|
|
723
|
+
export const sqliteGetMfaVerifiedAt = (sessionId) => {
|
|
724
|
+
const row = getDb().query("SELECT mfaVerifiedAt FROM sessions WHERE sessionId = ?").get(sessionId);
|
|
725
|
+
return row?.mfaVerifiedAt ?? null;
|
|
726
|
+
};
|
|
727
|
+
export const sqliteSetMfaVerifiedAt = (sessionId, ts) => {
|
|
728
|
+
getDb().run("UPDATE sessions SET mfaVerifiedAt = ? WHERE sessionId = ?", [ts, sessionId]);
|
|
729
|
+
};
|
|
604
730
|
// ---------------------------------------------------------------------------
|
|
605
731
|
// OAuth state helpers (used by src/lib/oauth.ts)
|
|
606
732
|
// ---------------------------------------------------------------------------
|
|
@@ -652,6 +778,10 @@ export const sqliteGetVerificationToken = (token) => {
|
|
|
652
778
|
export const sqliteDeleteVerificationToken = (token) => {
|
|
653
779
|
getDb().run("DELETE FROM email_verifications WHERE token = ?", [token]);
|
|
654
780
|
};
|
|
781
|
+
export const sqliteConsumeVerificationToken = (token) => {
|
|
782
|
+
const row = getDb().query("DELETE FROM email_verifications WHERE token = ? AND expiresAt > ? RETURNING userId, email").get(token, Date.now());
|
|
783
|
+
return row ?? null;
|
|
784
|
+
};
|
|
655
785
|
// ---------------------------------------------------------------------------
|
|
656
786
|
// Password reset token helpers (used by src/lib/resetPassword.ts)
|
|
657
787
|
// ---------------------------------------------------------------------------
|
|
@@ -705,3 +835,24 @@ export const startSqliteCleanup = (intervalMs = 3_600_000) => {
|
|
|
705
835
|
db.run("DELETE FROM oauth_codes WHERE expiresAt <= ?", [now]);
|
|
706
836
|
}, intervalMs);
|
|
707
837
|
};
|
|
838
|
+
export const sqliteRegisterUpload = (record) => {
|
|
839
|
+
getDb().run(`INSERT OR REPLACE INTO upload_registry (key, ownerUserId, tenantId, mimeType, bucket, createdAt)
|
|
840
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [record.key, record.ownerUserId ?? null, record.tenantId ?? null, record.mimeType ?? null, record.bucket ?? null, record.createdAt]);
|
|
841
|
+
};
|
|
842
|
+
export const sqliteGetUploadRecord = (key) => {
|
|
843
|
+
const row = getDb().query("SELECT key, ownerUserId, tenantId, mimeType, bucket, createdAt FROM upload_registry WHERE key = ?").get(key);
|
|
844
|
+
if (!row)
|
|
845
|
+
return null;
|
|
846
|
+
return {
|
|
847
|
+
key: row.key,
|
|
848
|
+
...(row.ownerUserId !== null ? { ownerUserId: row.ownerUserId } : {}),
|
|
849
|
+
...(row.tenantId !== null ? { tenantId: row.tenantId } : {}),
|
|
850
|
+
...(row.mimeType !== null ? { mimeType: row.mimeType } : {}),
|
|
851
|
+
...(row.bucket !== null ? { bucket: row.bucket } : {}),
|
|
852
|
+
createdAt: row.createdAt,
|
|
853
|
+
};
|
|
854
|
+
};
|
|
855
|
+
export const sqliteDeleteUploadRecord = (key) => {
|
|
856
|
+
const result = getDb().run("DELETE FROM upload_registry WHERE key = ?", [key]);
|
|
857
|
+
return result.changes > 0;
|
|
858
|
+
};
|