@lastshotlabs/bunshot 0.0.21 → 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/README.md +3035 -1249
- package/dist/adapters/localStorage.d.ts +6 -0
- package/dist/adapters/localStorage.js +59 -0
- package/dist/adapters/memoryAuth.d.ts +13 -0
- package/dist/adapters/memoryAuth.js +261 -2
- package/dist/adapters/memoryStorage.d.ts +3 -0
- package/dist/adapters/memoryStorage.js +44 -0
- package/dist/adapters/mongoAuth.js +217 -1
- package/dist/adapters/s3Storage.d.ts +14 -0
- package/dist/adapters/s3Storage.js +126 -0
- package/dist/adapters/sqliteAuth.d.ts +30 -0
- package/dist/adapters/sqliteAuth.js +352 -2
- package/dist/app.d.ts +203 -3
- package/dist/app.js +352 -48
- package/dist/cli.js +118 -38
- package/dist/index.d.ts +69 -8
- package/dist/index.js +46 -5
- package/dist/lib/HttpError.d.ts +7 -1
- package/dist/lib/HttpError.js +10 -1
- package/dist/lib/appConfig.d.ts +157 -0
- package/dist/lib/appConfig.js +54 -0
- package/dist/lib/auditLog.d.ts +58 -0
- package/dist/lib/auditLog.js +218 -0
- package/dist/lib/authAdapter.d.ts +140 -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/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +24 -1
- package/dist/lib/context.js +17 -3
- package/dist/lib/createRoute.d.ts +28 -2
- package/dist/lib/createRoute.js +54 -3
- package/dist/lib/credentialStuffing.d.ts +31 -0
- package/dist/lib/credentialStuffing.js +77 -0
- package/dist/lib/deletionCancelToken.d.ts +12 -0
- package/dist/lib/deletionCancelToken.js +88 -0
- package/dist/lib/emailVerification.d.ts +6 -0
- package/dist/lib/emailVerification.js +46 -3
- package/dist/lib/groups.d.ts +113 -0
- package/dist/lib/groups.js +133 -0
- package/dist/lib/idempotency.d.ts +22 -0
- package/dist/lib/idempotency.js +182 -0
- 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/metrics.d.ts +14 -0
- package/dist/lib/metrics.js +158 -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/pagination.d.ts +119 -0
- package/dist/lib/pagination.js +166 -0
- 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 +14 -0
- package/dist/lib/session.js +121 -5
- package/dist/lib/signing.d.ts +52 -0
- package/dist/lib/signing.js +183 -0
- package/dist/lib/storageAdapter.d.ts +30 -0
- package/dist/lib/storageAdapter.js +1 -0
- package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
- package/dist/lib/stripUnreferencedSchemas.js +79 -0
- package/dist/lib/suspension.d.ts +13 -0
- package/dist/lib/suspension.js +23 -0
- package/dist/lib/tenant.js +2 -2
- package/dist/lib/upload.d.ts +39 -0
- package/dist/lib/upload.js +112 -0
- package/dist/lib/uploadRegistry.d.ts +18 -0
- package/dist/lib/uploadRegistry.js +83 -0
- package/dist/lib/validate.js +2 -2
- package/dist/lib/ws.d.ts +1 -0
- package/dist/lib/ws.js +28 -0
- package/dist/lib/wsHeartbeat.d.ts +12 -0
- package/dist/lib/wsHeartbeat.js +57 -0
- package/dist/lib/wsMessages.d.ts +40 -0
- package/dist/lib/wsMessages.js +330 -0
- package/dist/lib/wsPresence.d.ts +25 -0
- package/dist/lib/wsPresence.js +99 -0
- package/dist/middleware/auditLog.d.ts +22 -0
- package/dist/middleware/auditLog.js +39 -0
- package/dist/middleware/bearerAuth.js +1 -1
- package/dist/middleware/cacheResponse.js +5 -1
- package/dist/middleware/captcha.d.ts +10 -0
- package/dist/middleware/captcha.js +36 -0
- package/dist/middleware/csrf.js +18 -4
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/identify.js +89 -14
- package/dist/middleware/metrics.d.ts +9 -0
- package/dist/middleware/metrics.js +26 -0
- package/dist/middleware/requestId.d.ts +3 -0
- package/dist/middleware/requestId.js +7 -0
- package/dist/middleware/requestLogger.d.ts +38 -0
- package/dist/middleware/requestLogger.js +68 -0
- package/dist/middleware/requestSigning.d.ts +20 -0
- package/dist/middleware/requestSigning.js +100 -0
- package/dist/middleware/requireMfaSetup.d.ts +16 -0
- package/dist/middleware/requireMfaSetup.js +37 -0
- package/dist/middleware/requireRole.d.ts +9 -3
- package/dist/middleware/requireRole.js +23 -36
- 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/upload.d.ts +5 -0
- package/dist/middleware/upload.js +27 -0
- package/dist/middleware/webhookAuth.d.ts +30 -0
- package/dist/middleware/webhookAuth.js +58 -0
- package/dist/models/AuditLog.d.ts +30 -0
- package/dist/models/AuditLog.js +39 -0
- package/dist/models/AuthUser.d.ts +7 -0
- package/dist/models/AuthUser.js +7 -0
- package/dist/models/Group.d.ts +21 -0
- package/dist/models/Group.js +28 -0
- package/dist/models/GroupMembership.d.ts +21 -0
- package/dist/models/GroupMembership.js +25 -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 +238 -21
- package/dist/routes/groups.d.ts +21 -0
- package/dist/routes/groups.js +346 -0
- package/dist/routes/jobs.js +66 -46
- package/dist/routes/m2m.d.ts +2 -0
- package/dist/routes/m2m.js +72 -0
- package/dist/routes/metrics.d.ts +8 -0
- package/dist/routes/metrics.js +55 -0
- package/dist/routes/mfa.js +13 -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 +14 -0
- package/dist/routes/uploads.js +227 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +46 -3
- 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 +5 -1
- package/docs/sections/auth-flow/full.md +203 -47
- package/docs/sections/auth-flow/overview.md +2 -2
- package/docs/sections/auth-security-examples/full.md +388 -0
- package/docs/sections/authentication/full.md +130 -0
- package/docs/sections/authentication/overview.md +5 -0
- package/docs/sections/cli/full.md +13 -1
- package/docs/sections/configuration/full.md +17 -0
- package/docs/sections/configuration/overview.md +1 -0
- package/docs/sections/exports/full.md +34 -3
- package/docs/sections/logging/full.md +83 -0
- package/docs/sections/metrics/full.md +131 -0
- package/docs/sections/oauth/full.md +189 -189
- package/docs/sections/oauth/overview.md +1 -1
- package/docs/sections/pagination/full.md +93 -0
- package/docs/sections/passkey-login/full.md +90 -0
- package/docs/sections/passkey-login/overview.md +1 -0
- package/docs/sections/roles/full.md +224 -135
- package/docs/sections/roles/overview.md +3 -1
- package/docs/sections/signing/full.md +203 -0
- package/docs/sections/uploads/full.md +208 -0
- package/docs/sections/versioning/full.md +85 -0
- package/docs/sections/webhook-auth/full.md +100 -0
- package/docs/sections/websocket/full.md +95 -0
- package/docs/sections/websocket-rooms/full.md +6 -1
- package/package.json +18 -5
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
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
|
+
}
|
|
18
|
+
export const localStorage = (config) => ({
|
|
19
|
+
async put(key, data, _meta) {
|
|
20
|
+
const filePath = resolveKey(config.directory, key);
|
|
21
|
+
// Ensure parent directory exists
|
|
22
|
+
const dir = dirname(filePath);
|
|
23
|
+
if (dir) {
|
|
24
|
+
const { mkdir } = await import("node:fs/promises");
|
|
25
|
+
await mkdir(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
if (data instanceof Blob) {
|
|
28
|
+
await Bun.write(filePath, data);
|
|
29
|
+
}
|
|
30
|
+
else if (data instanceof ReadableStream) {
|
|
31
|
+
const response = new Response(data);
|
|
32
|
+
const blob = await response.blob();
|
|
33
|
+
await Bun.write(filePath, blob);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
await Bun.write(filePath, data);
|
|
37
|
+
}
|
|
38
|
+
const url = config.baseUrl ? `${config.baseUrl.replace(/\/$/, "")}/${key}` : undefined;
|
|
39
|
+
return { ...(url !== undefined ? { url } : {}) };
|
|
40
|
+
},
|
|
41
|
+
async get(key) {
|
|
42
|
+
const filePath = resolveKey(config.directory, key);
|
|
43
|
+
const file = Bun.file(filePath);
|
|
44
|
+
const exists = await file.exists();
|
|
45
|
+
if (!exists)
|
|
46
|
+
return null;
|
|
47
|
+
const stream = file.stream();
|
|
48
|
+
return { stream, size: file.size };
|
|
49
|
+
},
|
|
50
|
+
async delete(key) {
|
|
51
|
+
const filePath = resolveKey(config.directory, key);
|
|
52
|
+
try {
|
|
53
|
+
await unlink(filePath);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// File doesn't exist — ignore
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -10,6 +10,10 @@ export declare const memoryGetUserSessions: (userId: string) => SessionInfo[];
|
|
|
10
10
|
export declare const memoryGetActiveSessionCount: (userId: string) => number;
|
|
11
11
|
export declare const memoryEvictOldestSession: (userId: string) => void;
|
|
12
12
|
export declare const memoryUpdateSessionLastActive: (sessionId: string) => void;
|
|
13
|
+
export declare const memoryGetSessionFingerprint: (sessionId: string) => string | null;
|
|
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;
|
|
13
17
|
export declare const memorySetRefreshToken: (sessionId: string, refreshToken: string) => void;
|
|
14
18
|
import type { RefreshResult } from "../lib/session";
|
|
15
19
|
export declare const memoryGetSessionByRefreshToken: (refreshToken: string) => RefreshResult | null;
|
|
@@ -29,6 +33,10 @@ export declare const memoryGetVerificationToken: (token: string) => {
|
|
|
29
33
|
email: string;
|
|
30
34
|
} | null;
|
|
31
35
|
export declare const memoryDeleteVerificationToken: (token: string) => void;
|
|
36
|
+
export declare const memoryConsumeVerificationToken: (token: string) => {
|
|
37
|
+
userId: string;
|
|
38
|
+
email: string;
|
|
39
|
+
} | null;
|
|
32
40
|
export declare const memoryCreateResetToken: (token: string, userId: string, email: string, ttlSeconds: number) => void;
|
|
33
41
|
export declare const memoryConsumeResetToken: (hash: string) => {
|
|
34
42
|
userId: string;
|
|
@@ -37,3 +45,8 @@ export declare const memoryConsumeResetToken: (hash: string) => {
|
|
|
37
45
|
import type { OAuthCodePayload } from "../lib/oauthCode";
|
|
38
46
|
export declare const memoryStoreOAuthCode: (hash: string, payload: OAuthCodePayload, ttlSeconds: number) => void;
|
|
39
47
|
export declare const memoryConsumeOAuthCode: (hash: string) => OAuthCodePayload | null;
|
|
48
|
+
export declare const memoryCreateDeletionCancelToken: (token: string, userId: string, jobId: string, ttlSeconds: number) => void;
|
|
49
|
+
export declare const memoryConsumeDeletionCancelToken: (hash: string) => {
|
|
50
|
+
userId: string;
|
|
51
|
+
jobId: string;
|
|
52
|
+
} | null;
|
|
@@ -2,6 +2,12 @@ import { HttpError } from "../lib/HttpError";
|
|
|
2
2
|
import { getPersistSessionMetadata, getIncludeInactiveSessions } from "../lib/appConfig";
|
|
3
3
|
import { clearMemoryRateLimitStore } from "../lib/authRateLimit";
|
|
4
4
|
import { clearMemoryMfaChallenges } from "../lib/mfaChallenge";
|
|
5
|
+
import { clearAuditLogMemoryStore } from "../lib/auditLog";
|
|
6
|
+
import { clearPresenceStore } from "../lib/wsPresence";
|
|
7
|
+
import { clearWsMessageMemoryStore } from "../lib/wsMessages";
|
|
8
|
+
import { clearHeartbeatState } from "../lib/wsHeartbeat";
|
|
9
|
+
import { clearMemoryUploadStore } from "./memoryStorage";
|
|
10
|
+
import { clearUploadRegistry } from "../lib/uploadRegistry";
|
|
5
11
|
const _users = new Map();
|
|
6
12
|
const _byEmail = new Map();
|
|
7
13
|
const _sessions = new Map(); // sessionId → session
|
|
@@ -11,8 +17,12 @@ const _oauthStates = new Map();
|
|
|
11
17
|
const _cache = new Map();
|
|
12
18
|
const _verificationTokens = new Map();
|
|
13
19
|
const _resetTokens = new Map();
|
|
20
|
+
const _cancelTokens = new Map();
|
|
14
21
|
const _oauthCodes = new Map();
|
|
15
22
|
const _tenantRoles = new Map(); // "userId:tenantId" → roles
|
|
23
|
+
const _groups = new Map(); // groupId → GroupRecord
|
|
24
|
+
const _groupMemberships = new Map();
|
|
25
|
+
const _m2mClients = new Map();
|
|
16
26
|
/** Reset all in-memory state. Useful for test isolation. */
|
|
17
27
|
export const clearMemoryStore = () => {
|
|
18
28
|
_users.clear();
|
|
@@ -21,13 +31,23 @@ export const clearMemoryStore = () => {
|
|
|
21
31
|
_userSessionIds.clear();
|
|
22
32
|
_refreshTokenIndex.clear();
|
|
23
33
|
_tenantRoles.clear();
|
|
34
|
+
_groups.clear();
|
|
35
|
+
_groupMemberships.clear();
|
|
24
36
|
_oauthStates.clear();
|
|
25
37
|
_oauthCodes.clear();
|
|
26
38
|
_cache.clear();
|
|
27
39
|
_verificationTokens.clear();
|
|
28
40
|
_resetTokens.clear();
|
|
41
|
+
_cancelTokens.clear();
|
|
42
|
+
_m2mClients.clear();
|
|
29
43
|
clearMemoryRateLimitStore();
|
|
30
44
|
clearMemoryMfaChallenges();
|
|
45
|
+
clearAuditLogMemoryStore();
|
|
46
|
+
clearPresenceStore();
|
|
47
|
+
clearWsMessageMemoryStore();
|
|
48
|
+
clearHeartbeatState();
|
|
49
|
+
clearMemoryUploadStore();
|
|
50
|
+
clearUploadRegistry();
|
|
31
51
|
};
|
|
32
52
|
// ---------------------------------------------------------------------------
|
|
33
53
|
// Auth adapter
|
|
@@ -47,7 +67,7 @@ export const memoryAuthAdapter = {
|
|
|
47
67
|
if (_byEmail.has(normalised))
|
|
48
68
|
throw new HttpError(409, "Email already registered");
|
|
49
69
|
const id = crypto.randomUUID();
|
|
50
|
-
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 };
|
|
51
71
|
_users.set(id, user);
|
|
52
72
|
_byEmail.set(normalised, id);
|
|
53
73
|
return { id };
|
|
@@ -73,7 +93,7 @@ export const memoryAuthAdapter = {
|
|
|
73
93
|
}
|
|
74
94
|
const id = crypto.randomUUID();
|
|
75
95
|
const email = profile.email ? profile.email.toLowerCase() : null;
|
|
76
|
-
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 };
|
|
77
97
|
_users.set(id, user);
|
|
78
98
|
if (email)
|
|
79
99
|
_byEmail.set(email, id);
|
|
@@ -117,6 +137,12 @@ export const memoryAuthAdapter = {
|
|
|
117
137
|
email: user.email ?? undefined,
|
|
118
138
|
providerIds: [...user.providerIds],
|
|
119
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,
|
|
120
146
|
};
|
|
121
147
|
},
|
|
122
148
|
async unlinkProvider(userId, provider) {
|
|
@@ -242,6 +268,194 @@ export const memoryAuthAdapter = {
|
|
|
242
268
|
_tenantRoles.set(key, current.filter((r) => r !== role));
|
|
243
269
|
}
|
|
244
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
|
+
},
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Groups
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
async createGroup(group) {
|
|
337
|
+
// Enforce name uniqueness within scope (null = app-wide, string = tenant-scoped)
|
|
338
|
+
for (const g of _groups.values()) {
|
|
339
|
+
if (g.name === group.name && g.tenantId === group.tenantId) {
|
|
340
|
+
throw new HttpError(409, "A group with this name already exists in this scope");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const id = crypto.randomUUID();
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
_groups.set(id, { ...group, id, createdAt: now, updatedAt: now });
|
|
346
|
+
return { id };
|
|
347
|
+
},
|
|
348
|
+
async deleteGroup(groupId) {
|
|
349
|
+
_groups.delete(groupId);
|
|
350
|
+
// Cascade: remove all memberships for this group
|
|
351
|
+
for (const [userId, memberships] of _groupMemberships) {
|
|
352
|
+
const filtered = memberships.filter((m) => m.groupId !== groupId);
|
|
353
|
+
if (filtered.length !== memberships.length) {
|
|
354
|
+
_groupMemberships.set(userId, filtered);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
async getGroup(groupId) {
|
|
359
|
+
return _groups.get(groupId) ?? null;
|
|
360
|
+
},
|
|
361
|
+
async listGroups(tenantId, opts) {
|
|
362
|
+
const limit = Math.min(opts?.limit ?? 50, 200);
|
|
363
|
+
const offset = opts?.offset ?? 0;
|
|
364
|
+
const all = [..._groups.values()].filter((g) => g.tenantId === tenantId);
|
|
365
|
+
return { items: all.slice(offset, offset + limit), total: all.length, limit, offset };
|
|
366
|
+
},
|
|
367
|
+
async updateGroup(groupId, updates) {
|
|
368
|
+
const group = _groups.get(groupId);
|
|
369
|
+
if (!group)
|
|
370
|
+
return;
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
_groups.set(groupId, { ...group, ...updates, id: group.id, tenantId: group.tenantId, createdAt: group.createdAt, updatedAt: now });
|
|
373
|
+
},
|
|
374
|
+
async addGroupMember(groupId, userId, roles = []) {
|
|
375
|
+
const group = _groups.get(groupId);
|
|
376
|
+
if (!group)
|
|
377
|
+
throw new HttpError(404, "Group not found");
|
|
378
|
+
const existing = _groupMemberships.get(userId) ?? [];
|
|
379
|
+
if (existing.some((m) => m.groupId === groupId)) {
|
|
380
|
+
throw new HttpError(409, "User is already a member of this group");
|
|
381
|
+
}
|
|
382
|
+
_groupMemberships.set(userId, [...existing, {
|
|
383
|
+
groupId, roles: [...roles], tenantId: group.tenantId, createdAt: Date.now(),
|
|
384
|
+
}]);
|
|
385
|
+
},
|
|
386
|
+
async updateGroupMembership(groupId, userId, roles) {
|
|
387
|
+
const memberships = _groupMemberships.get(userId);
|
|
388
|
+
if (!memberships)
|
|
389
|
+
return;
|
|
390
|
+
const idx = memberships.findIndex((m) => m.groupId === groupId);
|
|
391
|
+
if (idx === -1)
|
|
392
|
+
return;
|
|
393
|
+
memberships[idx] = { ...memberships[idx], roles: [...roles] };
|
|
394
|
+
},
|
|
395
|
+
async removeGroupMember(groupId, userId) {
|
|
396
|
+
const memberships = _groupMemberships.get(userId);
|
|
397
|
+
if (!memberships)
|
|
398
|
+
return;
|
|
399
|
+
_groupMemberships.set(userId, memberships.filter((m) => m.groupId !== groupId));
|
|
400
|
+
},
|
|
401
|
+
async getGroupMembers(groupId, opts) {
|
|
402
|
+
const limit = Math.min(opts?.limit ?? 50, 200);
|
|
403
|
+
const offset = opts?.offset ?? 0;
|
|
404
|
+
const all = [];
|
|
405
|
+
for (const [userId, memberships] of _groupMemberships) {
|
|
406
|
+
const m = memberships.find((m) => m.groupId === groupId);
|
|
407
|
+
if (m)
|
|
408
|
+
all.push({ userId, roles: [...m.roles] });
|
|
409
|
+
}
|
|
410
|
+
return { items: all.slice(offset, offset + limit), total: all.length, limit, offset };
|
|
411
|
+
},
|
|
412
|
+
async getUserGroups(userId, tenantId) {
|
|
413
|
+
const memberships = (_groupMemberships.get(userId) ?? []).filter((m) => m.tenantId === tenantId);
|
|
414
|
+
const result = [];
|
|
415
|
+
for (const m of memberships) {
|
|
416
|
+
const group = _groups.get(m.groupId);
|
|
417
|
+
if (group)
|
|
418
|
+
result.push({ group: { ...group }, membershipRoles: [...m.roles] });
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
},
|
|
422
|
+
async getEffectiveRoles(userId, tenantId) {
|
|
423
|
+
const direct = tenantId
|
|
424
|
+
? (_tenantRoles.get(`${userId}:${tenantId}`) ?? [])
|
|
425
|
+
: (_users.get(userId)?.roles ?? []);
|
|
426
|
+
const memberships = (_groupMemberships.get(userId) ?? []).filter((m) => m.tenantId === tenantId);
|
|
427
|
+
const groupRoles = memberships.flatMap((m) => [
|
|
428
|
+
...(_groups.get(m.groupId)?.roles ?? []),
|
|
429
|
+
...m.roles,
|
|
430
|
+
]);
|
|
431
|
+
return [...new Set([...direct, ...groupRoles])];
|
|
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
|
+
},
|
|
245
459
|
};
|
|
246
460
|
// ---------------------------------------------------------------------------
|
|
247
461
|
// Session helpers (used by src/lib/session.ts)
|
|
@@ -349,6 +563,22 @@ export const memoryUpdateSessionLastActive = (sessionId) => {
|
|
|
349
563
|
if (entry)
|
|
350
564
|
entry.lastActiveAt = Date.now();
|
|
351
565
|
};
|
|
566
|
+
export const memoryGetSessionFingerprint = (sessionId) => {
|
|
567
|
+
return _sessions.get(sessionId)?.fingerprint ?? null;
|
|
568
|
+
};
|
|
569
|
+
export const memorySetSessionFingerprint = (sessionId, fingerprint) => {
|
|
570
|
+
const entry = _sessions.get(sessionId);
|
|
571
|
+
if (entry)
|
|
572
|
+
entry.fingerprint = fingerprint;
|
|
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
|
+
};
|
|
352
582
|
export const memorySetRefreshToken = (sessionId, refreshToken) => {
|
|
353
583
|
const entry = _sessions.get(sessionId);
|
|
354
584
|
if (!entry)
|
|
@@ -455,6 +685,15 @@ export const memoryGetVerificationToken = (token) => {
|
|
|
455
685
|
export const memoryDeleteVerificationToken = (token) => {
|
|
456
686
|
_verificationTokens.delete(token);
|
|
457
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
|
+
};
|
|
458
697
|
// ---------------------------------------------------------------------------
|
|
459
698
|
// Password reset token helpers (used by src/lib/resetPassword.ts)
|
|
460
699
|
// ---------------------------------------------------------------------------
|
|
@@ -488,3 +727,23 @@ export const memoryConsumeOAuthCode = (hash) => {
|
|
|
488
727
|
_oauthCodes.delete(hash);
|
|
489
728
|
return { token: entry.token, userId: entry.userId, email: entry.email, refreshToken: entry.refreshToken };
|
|
490
729
|
};
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
// Account deletion cancel token helpers (used by src/lib/deletionCancelToken.ts)
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
export const memoryCreateDeletionCancelToken = (token, userId, jobId, ttlSeconds) => {
|
|
734
|
+
const now = Date.now();
|
|
735
|
+
for (const [k, v] of _cancelTokens) {
|
|
736
|
+
if (v.expiresAt <= now)
|
|
737
|
+
_cancelTokens.delete(k);
|
|
738
|
+
}
|
|
739
|
+
_cancelTokens.set(token, { userId, jobId, expiresAt: now + ttlSeconds * 1000 });
|
|
740
|
+
};
|
|
741
|
+
export const memoryConsumeDeletionCancelToken = (hash) => {
|
|
742
|
+
const entry = _cancelTokens.get(hash);
|
|
743
|
+
if (!entry || entry.expiresAt <= Date.now()) {
|
|
744
|
+
_cancelTokens.delete(hash);
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
_cancelTokens.delete(hash);
|
|
748
|
+
return { userId: entry.userId, jobId: entry.jobId };
|
|
749
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const _store = new Map();
|
|
2
|
+
export const clearMemoryUploadStore = () => {
|
|
3
|
+
_store.clear();
|
|
4
|
+
};
|
|
5
|
+
export const memoryStorage = () => ({
|
|
6
|
+
async put(key, data, meta) {
|
|
7
|
+
let buf;
|
|
8
|
+
if (data instanceof Blob) {
|
|
9
|
+
buf = Buffer.from(await data.arrayBuffer());
|
|
10
|
+
}
|
|
11
|
+
else if (data instanceof ReadableStream) {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
const reader = data.getReader();
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done)
|
|
17
|
+
break;
|
|
18
|
+
if (value)
|
|
19
|
+
chunks.push(value);
|
|
20
|
+
}
|
|
21
|
+
buf = Buffer.concat(chunks);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
buf = data;
|
|
25
|
+
}
|
|
26
|
+
_store.set(key, { data: buf, mimeType: meta.mimeType, size: meta.size });
|
|
27
|
+
return {};
|
|
28
|
+
},
|
|
29
|
+
async get(key) {
|
|
30
|
+
const entry = _store.get(key);
|
|
31
|
+
if (!entry)
|
|
32
|
+
return null;
|
|
33
|
+
const stream = new ReadableStream({
|
|
34
|
+
start(controller) {
|
|
35
|
+
controller.enqueue(entry.data);
|
|
36
|
+
controller.close();
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return { stream, mimeType: entry.mimeType, size: entry.size };
|
|
40
|
+
},
|
|
41
|
+
async delete(key) {
|
|
42
|
+
_store.delete(key);
|
|
43
|
+
},
|
|
44
|
+
});
|