@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.
Files changed (108) hide show
  1. package/dist/adapters/localStorage.js +20 -5
  2. package/dist/adapters/memoryAuth.d.ts +6 -0
  3. package/dist/adapters/memoryAuth.js +117 -2
  4. package/dist/adapters/mongoAuth.js +97 -1
  5. package/dist/adapters/sqliteAuth.d.ts +23 -0
  6. package/dist/adapters/sqliteAuth.js +153 -2
  7. package/dist/app.d.ts +105 -2
  8. package/dist/app.js +112 -9
  9. package/dist/index.d.ts +23 -4
  10. package/dist/index.js +13 -2
  11. package/dist/lib/HttpError.d.ts +2 -1
  12. package/dist/lib/HttpError.js +3 -1
  13. package/dist/lib/appConfig.d.ts +113 -0
  14. package/dist/lib/appConfig.js +38 -0
  15. package/dist/lib/auditLog.d.ts +6 -0
  16. package/dist/lib/auditLog.js +17 -0
  17. package/dist/lib/authAdapter.d.ts +71 -1
  18. package/dist/lib/authRateLimit.js +36 -0
  19. package/dist/lib/breachedPassword.d.ts +13 -0
  20. package/dist/lib/breachedPassword.js +48 -0
  21. package/dist/lib/captcha.d.ts +25 -0
  22. package/dist/lib/captcha.js +37 -0
  23. package/dist/lib/context.d.ts +5 -0
  24. package/dist/lib/credentialStuffing.d.ts +31 -0
  25. package/dist/lib/credentialStuffing.js +77 -0
  26. package/dist/lib/emailVerification.d.ts +6 -0
  27. package/dist/lib/emailVerification.js +46 -3
  28. package/dist/lib/jwks.d.ts +25 -0
  29. package/dist/lib/jwks.js +51 -0
  30. package/dist/lib/jwt.d.ts +15 -2
  31. package/dist/lib/jwt.js +92 -5
  32. package/dist/lib/logger.d.ts +2 -0
  33. package/dist/lib/logger.js +6 -0
  34. package/dist/lib/m2m.d.ts +29 -0
  35. package/dist/lib/m2m.js +48 -0
  36. package/dist/lib/mfaChallenge.d.ts +14 -1
  37. package/dist/lib/mfaChallenge.js +111 -6
  38. package/dist/lib/mongo.js +1 -1
  39. package/dist/lib/oauthCode.js +23 -18
  40. package/dist/lib/resetPassword.js +3 -1
  41. package/dist/lib/saml.d.ts +25 -0
  42. package/dist/lib/saml.js +64 -0
  43. package/dist/lib/scim.d.ts +44 -0
  44. package/dist/lib/scim.js +54 -0
  45. package/dist/lib/securityEvents.d.ts +28 -0
  46. package/dist/lib/securityEvents.js +26 -0
  47. package/dist/lib/session.d.ts +10 -0
  48. package/dist/lib/session.js +67 -5
  49. package/dist/lib/signing.js +5 -2
  50. package/dist/lib/suspension.d.ts +13 -0
  51. package/dist/lib/suspension.js +23 -0
  52. package/dist/lib/upload.d.ts +4 -0
  53. package/dist/lib/upload.js +26 -1
  54. package/dist/lib/uploadRegistry.d.ts +18 -0
  55. package/dist/lib/uploadRegistry.js +83 -0
  56. package/dist/lib/ws.js +7 -0
  57. package/dist/middleware/bearerAuth.js +1 -1
  58. package/dist/middleware/captcha.d.ts +10 -0
  59. package/dist/middleware/captcha.js +36 -0
  60. package/dist/middleware/csrf.js +8 -4
  61. package/dist/middleware/errorHandler.js +4 -1
  62. package/dist/middleware/identify.js +40 -13
  63. package/dist/middleware/requestSigning.js +6 -5
  64. package/dist/middleware/requireMfaSetup.js +2 -1
  65. package/dist/middleware/requireScope.d.ts +10 -0
  66. package/dist/middleware/requireScope.js +25 -0
  67. package/dist/middleware/requireStepUp.d.ts +18 -0
  68. package/dist/middleware/requireStepUp.js +29 -0
  69. package/dist/middleware/scimAuth.d.ts +8 -0
  70. package/dist/middleware/scimAuth.js +29 -0
  71. package/dist/middleware/webhookAuth.d.ts +1 -1
  72. package/dist/middleware/webhookAuth.js +6 -5
  73. package/dist/models/AuthUser.d.ts +7 -0
  74. package/dist/models/AuthUser.js +7 -0
  75. package/dist/models/M2MClient.d.ts +18 -0
  76. package/dist/models/M2MClient.js +18 -0
  77. package/dist/routes/auth.d.ts +3 -2
  78. package/dist/routes/auth.js +155 -16
  79. package/dist/routes/jobs.js +21 -3
  80. package/dist/routes/m2m.d.ts +2 -0
  81. package/dist/routes/m2m.js +72 -0
  82. package/dist/routes/metrics.d.ts +1 -0
  83. package/dist/routes/metrics.js +3 -0
  84. package/dist/routes/mfa.js +9 -1
  85. package/dist/routes/oauth.js +6 -0
  86. package/dist/routes/oidc.d.ts +2 -0
  87. package/dist/routes/oidc.js +29 -0
  88. package/dist/routes/passkey.d.ts +1 -0
  89. package/dist/routes/passkey.js +157 -0
  90. package/dist/routes/saml.d.ts +2 -0
  91. package/dist/routes/saml.js +86 -0
  92. package/dist/routes/scim.d.ts +2 -0
  93. package/dist/routes/scim.js +255 -0
  94. package/dist/routes/uploads.d.ts +13 -1
  95. package/dist/routes/uploads.js +98 -6
  96. package/dist/services/auth.d.ts +2 -0
  97. package/dist/services/auth.js +101 -22
  98. package/dist/services/mfa.js +2 -2
  99. package/dist/ws/index.js +2 -1
  100. package/docs/sections/auth-flow/full.md +790 -779
  101. package/docs/sections/auth-security-examples/full.md +23 -0
  102. package/docs/sections/metrics/full.md +6 -2
  103. package/docs/sections/passkey-login/full.md +90 -0
  104. package/docs/sections/passkey-login/overview.md +1 -0
  105. package/docs/sections/uploads/full.md +11 -2
  106. package/docs/sections/webhook-auth/full.md +1 -1
  107. package/docs/sections/websocket/full.md +12 -0
  108. package/package.json +3 -2
@@ -1,10 +1,25 @@
1
1
  import { unlink } from "node:fs/promises";
2
- import { join } from "node:path";
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 = join(config.directory, key);
20
+ const filePath = resolveKey(config.directory, key);
6
21
  // Ensure parent directory exists
7
- const dir = filePath.substring(0, filePath.lastIndexOf("/"));
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 = join(config.directory, key);
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 = join(config.directory, key);
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
+ };