@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.
Files changed (185) hide show
  1. package/README.md +3035 -1249
  2. package/dist/adapters/localStorage.d.ts +6 -0
  3. package/dist/adapters/localStorage.js +59 -0
  4. package/dist/adapters/memoryAuth.d.ts +13 -0
  5. package/dist/adapters/memoryAuth.js +261 -2
  6. package/dist/adapters/memoryStorage.d.ts +3 -0
  7. package/dist/adapters/memoryStorage.js +44 -0
  8. package/dist/adapters/mongoAuth.js +217 -1
  9. package/dist/adapters/s3Storage.d.ts +14 -0
  10. package/dist/adapters/s3Storage.js +126 -0
  11. package/dist/adapters/sqliteAuth.d.ts +30 -0
  12. package/dist/adapters/sqliteAuth.js +352 -2
  13. package/dist/app.d.ts +203 -3
  14. package/dist/app.js +352 -48
  15. package/dist/cli.js +118 -38
  16. package/dist/index.d.ts +69 -8
  17. package/dist/index.js +46 -5
  18. package/dist/lib/HttpError.d.ts +7 -1
  19. package/dist/lib/HttpError.js +10 -1
  20. package/dist/lib/appConfig.d.ts +157 -0
  21. package/dist/lib/appConfig.js +54 -0
  22. package/dist/lib/auditLog.d.ts +58 -0
  23. package/dist/lib/auditLog.js +218 -0
  24. package/dist/lib/authAdapter.d.ts +140 -1
  25. package/dist/lib/authRateLimit.js +36 -0
  26. package/dist/lib/breachedPassword.d.ts +13 -0
  27. package/dist/lib/breachedPassword.js +48 -0
  28. package/dist/lib/captcha.d.ts +25 -0
  29. package/dist/lib/captcha.js +37 -0
  30. package/dist/lib/constants.d.ts +4 -0
  31. package/dist/lib/constants.js +4 -0
  32. package/dist/lib/context.d.ts +24 -1
  33. package/dist/lib/context.js +17 -3
  34. package/dist/lib/createRoute.d.ts +28 -2
  35. package/dist/lib/createRoute.js +54 -3
  36. package/dist/lib/credentialStuffing.d.ts +31 -0
  37. package/dist/lib/credentialStuffing.js +77 -0
  38. package/dist/lib/deletionCancelToken.d.ts +12 -0
  39. package/dist/lib/deletionCancelToken.js +88 -0
  40. package/dist/lib/emailVerification.d.ts +6 -0
  41. package/dist/lib/emailVerification.js +46 -3
  42. package/dist/lib/groups.d.ts +113 -0
  43. package/dist/lib/groups.js +133 -0
  44. package/dist/lib/idempotency.d.ts +22 -0
  45. package/dist/lib/idempotency.js +182 -0
  46. package/dist/lib/jwks.d.ts +25 -0
  47. package/dist/lib/jwks.js +51 -0
  48. package/dist/lib/jwt.d.ts +15 -2
  49. package/dist/lib/jwt.js +92 -5
  50. package/dist/lib/logger.d.ts +2 -0
  51. package/dist/lib/logger.js +6 -0
  52. package/dist/lib/m2m.d.ts +29 -0
  53. package/dist/lib/m2m.js +48 -0
  54. package/dist/lib/metrics.d.ts +14 -0
  55. package/dist/lib/metrics.js +158 -0
  56. package/dist/lib/mfaChallenge.d.ts +14 -1
  57. package/dist/lib/mfaChallenge.js +111 -6
  58. package/dist/lib/mongo.js +1 -1
  59. package/dist/lib/oauthCode.js +23 -18
  60. package/dist/lib/pagination.d.ts +119 -0
  61. package/dist/lib/pagination.js +166 -0
  62. package/dist/lib/resetPassword.js +3 -1
  63. package/dist/lib/saml.d.ts +25 -0
  64. package/dist/lib/saml.js +64 -0
  65. package/dist/lib/scim.d.ts +44 -0
  66. package/dist/lib/scim.js +54 -0
  67. package/dist/lib/securityEvents.d.ts +28 -0
  68. package/dist/lib/securityEvents.js +26 -0
  69. package/dist/lib/session.d.ts +14 -0
  70. package/dist/lib/session.js +121 -5
  71. package/dist/lib/signing.d.ts +52 -0
  72. package/dist/lib/signing.js +183 -0
  73. package/dist/lib/storageAdapter.d.ts +30 -0
  74. package/dist/lib/storageAdapter.js +1 -0
  75. package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
  76. package/dist/lib/stripUnreferencedSchemas.js +79 -0
  77. package/dist/lib/suspension.d.ts +13 -0
  78. package/dist/lib/suspension.js +23 -0
  79. package/dist/lib/tenant.js +2 -2
  80. package/dist/lib/upload.d.ts +39 -0
  81. package/dist/lib/upload.js +112 -0
  82. package/dist/lib/uploadRegistry.d.ts +18 -0
  83. package/dist/lib/uploadRegistry.js +83 -0
  84. package/dist/lib/validate.js +2 -2
  85. package/dist/lib/ws.d.ts +1 -0
  86. package/dist/lib/ws.js +28 -0
  87. package/dist/lib/wsHeartbeat.d.ts +12 -0
  88. package/dist/lib/wsHeartbeat.js +57 -0
  89. package/dist/lib/wsMessages.d.ts +40 -0
  90. package/dist/lib/wsMessages.js +330 -0
  91. package/dist/lib/wsPresence.d.ts +25 -0
  92. package/dist/lib/wsPresence.js +99 -0
  93. package/dist/middleware/auditLog.d.ts +22 -0
  94. package/dist/middleware/auditLog.js +39 -0
  95. package/dist/middleware/bearerAuth.js +1 -1
  96. package/dist/middleware/cacheResponse.js +5 -1
  97. package/dist/middleware/captcha.d.ts +10 -0
  98. package/dist/middleware/captcha.js +36 -0
  99. package/dist/middleware/csrf.js +18 -4
  100. package/dist/middleware/errorHandler.js +4 -1
  101. package/dist/middleware/identify.js +89 -14
  102. package/dist/middleware/metrics.d.ts +9 -0
  103. package/dist/middleware/metrics.js +26 -0
  104. package/dist/middleware/requestId.d.ts +3 -0
  105. package/dist/middleware/requestId.js +7 -0
  106. package/dist/middleware/requestLogger.d.ts +38 -0
  107. package/dist/middleware/requestLogger.js +68 -0
  108. package/dist/middleware/requestSigning.d.ts +20 -0
  109. package/dist/middleware/requestSigning.js +100 -0
  110. package/dist/middleware/requireMfaSetup.d.ts +16 -0
  111. package/dist/middleware/requireMfaSetup.js +37 -0
  112. package/dist/middleware/requireRole.d.ts +9 -3
  113. package/dist/middleware/requireRole.js +23 -36
  114. package/dist/middleware/requireScope.d.ts +10 -0
  115. package/dist/middleware/requireScope.js +25 -0
  116. package/dist/middleware/requireStepUp.d.ts +18 -0
  117. package/dist/middleware/requireStepUp.js +29 -0
  118. package/dist/middleware/scimAuth.d.ts +8 -0
  119. package/dist/middleware/scimAuth.js +29 -0
  120. package/dist/middleware/upload.d.ts +5 -0
  121. package/dist/middleware/upload.js +27 -0
  122. package/dist/middleware/webhookAuth.d.ts +30 -0
  123. package/dist/middleware/webhookAuth.js +58 -0
  124. package/dist/models/AuditLog.d.ts +30 -0
  125. package/dist/models/AuditLog.js +39 -0
  126. package/dist/models/AuthUser.d.ts +7 -0
  127. package/dist/models/AuthUser.js +7 -0
  128. package/dist/models/Group.d.ts +21 -0
  129. package/dist/models/Group.js +28 -0
  130. package/dist/models/GroupMembership.d.ts +21 -0
  131. package/dist/models/GroupMembership.js +25 -0
  132. package/dist/models/M2MClient.d.ts +18 -0
  133. package/dist/models/M2MClient.js +18 -0
  134. package/dist/routes/auth.d.ts +3 -2
  135. package/dist/routes/auth.js +238 -21
  136. package/dist/routes/groups.d.ts +21 -0
  137. package/dist/routes/groups.js +346 -0
  138. package/dist/routes/jobs.js +66 -46
  139. package/dist/routes/m2m.d.ts +2 -0
  140. package/dist/routes/m2m.js +72 -0
  141. package/dist/routes/metrics.d.ts +8 -0
  142. package/dist/routes/metrics.js +55 -0
  143. package/dist/routes/mfa.js +13 -1
  144. package/dist/routes/oauth.js +6 -0
  145. package/dist/routes/oidc.d.ts +2 -0
  146. package/dist/routes/oidc.js +29 -0
  147. package/dist/routes/passkey.d.ts +1 -0
  148. package/dist/routes/passkey.js +157 -0
  149. package/dist/routes/saml.d.ts +2 -0
  150. package/dist/routes/saml.js +86 -0
  151. package/dist/routes/scim.d.ts +2 -0
  152. package/dist/routes/scim.js +255 -0
  153. package/dist/routes/uploads.d.ts +14 -0
  154. package/dist/routes/uploads.js +227 -0
  155. package/dist/server.d.ts +26 -0
  156. package/dist/server.js +46 -3
  157. package/dist/services/auth.d.ts +2 -0
  158. package/dist/services/auth.js +101 -22
  159. package/dist/services/mfa.js +2 -2
  160. package/dist/ws/index.js +5 -1
  161. package/docs/sections/auth-flow/full.md +203 -47
  162. package/docs/sections/auth-flow/overview.md +2 -2
  163. package/docs/sections/auth-security-examples/full.md +388 -0
  164. package/docs/sections/authentication/full.md +130 -0
  165. package/docs/sections/authentication/overview.md +5 -0
  166. package/docs/sections/cli/full.md +13 -1
  167. package/docs/sections/configuration/full.md +17 -0
  168. package/docs/sections/configuration/overview.md +1 -0
  169. package/docs/sections/exports/full.md +34 -3
  170. package/docs/sections/logging/full.md +83 -0
  171. package/docs/sections/metrics/full.md +131 -0
  172. package/docs/sections/oauth/full.md +189 -189
  173. package/docs/sections/oauth/overview.md +1 -1
  174. package/docs/sections/pagination/full.md +93 -0
  175. package/docs/sections/passkey-login/full.md +90 -0
  176. package/docs/sections/passkey-login/overview.md +1 -0
  177. package/docs/sections/roles/full.md +224 -135
  178. package/docs/sections/roles/overview.md +3 -1
  179. package/docs/sections/signing/full.md +203 -0
  180. package/docs/sections/uploads/full.md +208 -0
  181. package/docs/sections/versioning/full.md +85 -0
  182. package/docs/sections/webhook-auth/full.md +100 -0
  183. package/docs/sections/websocket/full.md +95 -0
  184. package/docs/sections/websocket-rooms/full.md +6 -1
  185. package/package.json +18 -5
@@ -0,0 +1,6 @@
1
+ import type { StorageAdapter } from "../lib/storageAdapter";
2
+ export interface LocalStorageConfig {
3
+ directory: string;
4
+ baseUrl?: string;
5
+ }
6
+ export declare const localStorage: (config: LocalStorageConfig) => StorageAdapter;
@@ -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,3 @@
1
+ import type { StorageAdapter } from "../lib/storageAdapter";
2
+ export declare const clearMemoryUploadStore: () => void;
3
+ export declare const memoryStorage: () => StorageAdapter;
@@ -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
+ });