@lastshotlabs/bunshot 0.0.13 → 0.0.18
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 +2816 -1747
- package/dist/adapters/memoryAuth.d.ts +7 -0
- package/dist/adapters/memoryAuth.js +177 -2
- package/dist/adapters/mongoAuth.js +94 -0
- package/dist/adapters/sqliteAuth.d.ts +9 -0
- package/dist/adapters/sqliteAuth.js +190 -2
- package/dist/app.d.ts +120 -2
- package/dist/app.js +104 -4
- package/dist/entrypoints/queue.d.ts +2 -2
- package/dist/entrypoints/queue.js +1 -1
- package/dist/index.d.ts +24 -8
- package/dist/index.js +15 -5
- package/dist/lib/appConfig.d.ts +81 -0
- package/dist/lib/appConfig.js +30 -0
- package/dist/lib/authAdapter.d.ts +54 -0
- package/dist/lib/authRateLimit.d.ts +2 -0
- package/dist/lib/authRateLimit.js +4 -0
- package/dist/lib/clientIp.d.ts +14 -0
- package/dist/lib/clientIp.js +52 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +2 -0
- package/dist/lib/createDtoMapper.d.ts +33 -0
- package/dist/lib/createDtoMapper.js +69 -0
- package/dist/lib/crypto.d.ts +11 -0
- package/dist/lib/crypto.js +22 -0
- package/dist/lib/emailVerification.d.ts +4 -0
- package/dist/lib/emailVerification.js +20 -12
- package/dist/lib/jwt.d.ts +1 -1
- package/dist/lib/jwt.js +19 -6
- package/dist/lib/mfaChallenge.d.ts +42 -0
- package/dist/lib/mfaChallenge.js +293 -0
- package/dist/lib/oauth.d.ts +14 -1
- package/dist/lib/oauth.js +19 -1
- package/dist/lib/oauthCode.d.ts +15 -0
- package/dist/lib/oauthCode.js +90 -0
- package/dist/lib/queue.d.ts +33 -0
- package/dist/lib/queue.js +98 -0
- package/dist/lib/resetPassword.js +12 -16
- package/dist/lib/roles.d.ts +4 -0
- package/dist/lib/roles.js +27 -0
- package/dist/lib/session.d.ts +12 -0
- package/dist/lib/session.js +165 -5
- package/dist/lib/tenant.d.ts +15 -0
- package/dist/lib/tenant.js +65 -0
- package/dist/lib/ws.js +5 -1
- package/dist/lib/zodToMongoose.d.ts +38 -0
- package/dist/lib/zodToMongoose.js +84 -0
- package/dist/middleware/bearerAuth.js +4 -3
- package/dist/middleware/botProtection.js +2 -2
- package/dist/middleware/cacheResponse.d.ts +1 -0
- package/dist/middleware/cacheResponse.js +18 -3
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.js +22 -8
- package/dist/middleware/csrf.d.ts +18 -0
- package/dist/middleware/csrf.js +115 -0
- package/dist/middleware/rateLimit.d.ts +2 -1
- package/dist/middleware/rateLimit.js +7 -5
- package/dist/middleware/requireRole.d.ts +14 -3
- package/dist/middleware/requireRole.js +46 -6
- package/dist/middleware/tenant.d.ts +5 -0
- package/dist/middleware/tenant.js +116 -0
- package/dist/models/AuthUser.d.ts +17 -0
- package/dist/models/AuthUser.js +17 -0
- package/dist/models/TenantRole.d.ts +15 -0
- package/dist/models/TenantRole.js +23 -0
- package/dist/routes/auth.d.ts +5 -3
- package/dist/routes/auth.js +173 -30
- package/dist/routes/jobs.d.ts +2 -0
- package/dist/routes/jobs.js +270 -0
- package/dist/routes/mfa.d.ts +5 -0
- package/dist/routes/mfa.js +616 -0
- package/dist/routes/oauth.js +378 -23
- package/dist/schemas/auth.d.ts +2 -0
- package/dist/schemas/auth.js +22 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +19 -3
- package/dist/services/auth.d.ts +18 -5
- package/dist/services/auth.js +112 -18
- package/dist/services/mfa.d.ts +84 -0
- package/dist/services/mfa.js +543 -0
- package/dist/ws/index.js +3 -2
- package/docs/sections/adding-middleware/full.md +35 -0
- package/docs/sections/adding-models/full.md +125 -0
- package/docs/sections/adding-models/overview.md +13 -0
- package/docs/sections/adding-routes/full.md +182 -0
- package/docs/sections/adding-routes/overview.md +23 -0
- package/docs/sections/auth-flow/full.md +634 -0
- package/docs/sections/auth-flow/overview.md +10 -0
- package/docs/sections/cli/full.md +30 -0
- package/docs/sections/configuration/full.md +155 -0
- package/docs/sections/configuration/overview.md +17 -0
- package/docs/sections/configuration-example/full.md +117 -0
- package/docs/sections/configuration-example/overview.md +30 -0
- package/docs/sections/documentation/full.md +171 -0
- package/docs/sections/environment-variables/full.md +55 -0
- package/docs/sections/exports/full.md +92 -0
- package/docs/sections/extending-context/full.md +59 -0
- package/docs/sections/header.md +3 -0
- package/docs/sections/installation/full.md +6 -0
- package/docs/sections/jobs/full.md +140 -0
- package/docs/sections/jobs/overview.md +15 -0
- package/docs/sections/mongodb-connections/full.md +45 -0
- package/docs/sections/mongodb-connections/overview.md +7 -0
- package/docs/sections/multi-tenancy/full.md +66 -0
- package/docs/sections/multi-tenancy/overview.md +15 -0
- package/docs/sections/oauth/full.md +189 -0
- package/docs/sections/oauth/overview.md +16 -0
- package/docs/sections/package-development/full.md +7 -0
- package/docs/sections/peer-dependencies/full.md +47 -0
- package/docs/sections/quick-start/full.md +43 -0
- package/docs/sections/response-caching/full.md +117 -0
- package/docs/sections/response-caching/overview.md +13 -0
- package/docs/sections/roles/full.md +136 -0
- package/docs/sections/roles/overview.md +12 -0
- package/docs/sections/running-without-redis/full.md +16 -0
- package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
- package/docs/sections/stack/full.md +10 -0
- package/docs/sections/websocket/full.md +101 -0
- package/docs/sections/websocket/overview.md +5 -0
- package/docs/sections/websocket-rooms/full.md +97 -0
- package/docs/sections/websocket-rooms/overview.md +5 -0
- package/package.json +30 -9
|
@@ -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 memorySetRefreshToken: (sessionId: string, refreshToken: string) => void;
|
|
14
|
+
import type { RefreshResult } from "../lib/session";
|
|
15
|
+
export declare const memoryGetSessionByRefreshToken: (refreshToken: string) => RefreshResult | null;
|
|
16
|
+
export declare const memoryRotateRefreshToken: (sessionId: string, newRefreshToken: string, newAccessToken: string) => void;
|
|
13
17
|
export declare const memoryStoreOAuthState: (state: string, codeVerifier?: string, linkUserId?: string) => void;
|
|
14
18
|
export declare const memoryConsumeOAuthState: (state: string) => {
|
|
15
19
|
codeVerifier?: string;
|
|
@@ -30,3 +34,6 @@ export declare const memoryConsumeResetToken: (hash: string) => {
|
|
|
30
34
|
userId: string;
|
|
31
35
|
email: string;
|
|
32
36
|
} | null;
|
|
37
|
+
import type { OAuthCodePayload } from "../lib/oauthCode";
|
|
38
|
+
export declare const memoryStoreOAuthCode: (hash: string, payload: OAuthCodePayload, ttlSeconds: number) => void;
|
|
39
|
+
export declare const memoryConsumeOAuthCode: (hash: string) => OAuthCodePayload | null;
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { HttpError } from "../lib/HttpError";
|
|
2
2
|
import { getPersistSessionMetadata, getIncludeInactiveSessions } from "../lib/appConfig";
|
|
3
|
+
import { clearMemoryRateLimitStore } from "../lib/authRateLimit";
|
|
4
|
+
import { clearMemoryMfaChallenges } from "../lib/mfaChallenge";
|
|
3
5
|
const _users = new Map();
|
|
4
6
|
const _byEmail = new Map();
|
|
5
7
|
const _sessions = new Map(); // sessionId → session
|
|
6
8
|
const _userSessionIds = new Map(); // userId → Set<sessionId>
|
|
9
|
+
const _refreshTokenIndex = new Map(); // refreshToken → sessionId
|
|
7
10
|
const _oauthStates = new Map();
|
|
8
11
|
const _cache = new Map();
|
|
9
12
|
const _verificationTokens = new Map();
|
|
10
13
|
const _resetTokens = new Map();
|
|
14
|
+
const _oauthCodes = new Map();
|
|
15
|
+
const _tenantRoles = new Map(); // "userId:tenantId" → roles
|
|
11
16
|
/** Reset all in-memory state. Useful for test isolation. */
|
|
12
17
|
export const clearMemoryStore = () => {
|
|
13
18
|
_users.clear();
|
|
14
19
|
_byEmail.clear();
|
|
15
20
|
_sessions.clear();
|
|
16
21
|
_userSessionIds.clear();
|
|
22
|
+
_refreshTokenIndex.clear();
|
|
23
|
+
_tenantRoles.clear();
|
|
17
24
|
_oauthStates.clear();
|
|
25
|
+
_oauthCodes.clear();
|
|
18
26
|
_cache.clear();
|
|
19
27
|
_verificationTokens.clear();
|
|
20
28
|
_resetTokens.clear();
|
|
29
|
+
clearMemoryRateLimitStore();
|
|
30
|
+
clearMemoryMfaChallenges();
|
|
21
31
|
};
|
|
22
32
|
// ---------------------------------------------------------------------------
|
|
23
33
|
// Auth adapter
|
|
@@ -37,7 +47,7 @@ export const memoryAuthAdapter = {
|
|
|
37
47
|
if (_byEmail.has(normalised))
|
|
38
48
|
throw new HttpError(409, "Email already registered");
|
|
39
49
|
const id = crypto.randomUUID();
|
|
40
|
-
const user = { id, email: normalised, passwordHash, providerIds: [], roles: [], emailVerified: false };
|
|
50
|
+
const user = { id, email: normalised, passwordHash, providerIds: [], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [], webauthnCredentials: [] };
|
|
41
51
|
_users.set(id, user);
|
|
42
52
|
_byEmail.set(normalised, id);
|
|
43
53
|
return { id };
|
|
@@ -63,7 +73,7 @@ export const memoryAuthAdapter = {
|
|
|
63
73
|
}
|
|
64
74
|
const id = crypto.randomUUID();
|
|
65
75
|
const email = profile.email ? profile.email.toLowerCase() : null;
|
|
66
|
-
const user = { id, email, passwordHash: null, providerIds: [key], roles: [], emailVerified: false };
|
|
76
|
+
const user = { id, email, passwordHash: null, providerIds: [key], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [], webauthnCredentials: [] };
|
|
67
77
|
_users.set(id, user);
|
|
68
78
|
if (email)
|
|
69
79
|
_byEmail.set(email, id);
|
|
@@ -132,6 +142,106 @@ export const memoryAuthAdapter = {
|
|
|
132
142
|
async getEmailVerified(userId) {
|
|
133
143
|
return _users.get(userId)?.emailVerified ?? false;
|
|
134
144
|
},
|
|
145
|
+
async deleteUser(userId) {
|
|
146
|
+
const user = _users.get(userId);
|
|
147
|
+
if (user?.email)
|
|
148
|
+
_byEmail.delete(user.email);
|
|
149
|
+
_users.delete(userId);
|
|
150
|
+
},
|
|
151
|
+
async hasPassword(userId) {
|
|
152
|
+
return !!_users.get(userId)?.passwordHash;
|
|
153
|
+
},
|
|
154
|
+
async setMfaSecret(userId, secret) {
|
|
155
|
+
const user = _users.get(userId);
|
|
156
|
+
if (user)
|
|
157
|
+
user.mfaSecret = secret;
|
|
158
|
+
},
|
|
159
|
+
async getMfaSecret(userId) {
|
|
160
|
+
return _users.get(userId)?.mfaSecret ?? null;
|
|
161
|
+
},
|
|
162
|
+
async isMfaEnabled(userId) {
|
|
163
|
+
return _users.get(userId)?.mfaEnabled ?? false;
|
|
164
|
+
},
|
|
165
|
+
async setMfaEnabled(userId, enabled) {
|
|
166
|
+
const user = _users.get(userId);
|
|
167
|
+
if (user)
|
|
168
|
+
user.mfaEnabled = enabled;
|
|
169
|
+
},
|
|
170
|
+
async setRecoveryCodes(userId, codes) {
|
|
171
|
+
const user = _users.get(userId);
|
|
172
|
+
if (user)
|
|
173
|
+
user.recoveryCodes = [...codes];
|
|
174
|
+
},
|
|
175
|
+
async getRecoveryCodes(userId) {
|
|
176
|
+
return _users.get(userId)?.recoveryCodes ?? [];
|
|
177
|
+
},
|
|
178
|
+
async removeRecoveryCode(userId, code) {
|
|
179
|
+
const user = _users.get(userId);
|
|
180
|
+
if (user)
|
|
181
|
+
user.recoveryCodes = user.recoveryCodes.filter((c) => c !== code);
|
|
182
|
+
},
|
|
183
|
+
async getMfaMethods(userId) {
|
|
184
|
+
const user = _users.get(userId);
|
|
185
|
+
if (!user)
|
|
186
|
+
return [];
|
|
187
|
+
// Backward compat: if mfaEnabled but no methods recorded, assume TOTP
|
|
188
|
+
if (user.mfaMethods.length === 0 && user.mfaEnabled)
|
|
189
|
+
return ["totp"];
|
|
190
|
+
return [...user.mfaMethods];
|
|
191
|
+
},
|
|
192
|
+
async setMfaMethods(userId, methods) {
|
|
193
|
+
const user = _users.get(userId);
|
|
194
|
+
if (user)
|
|
195
|
+
user.mfaMethods = [...methods];
|
|
196
|
+
},
|
|
197
|
+
async getWebAuthnCredentials(userId) {
|
|
198
|
+
return [...(_users.get(userId)?.webauthnCredentials ?? [])];
|
|
199
|
+
},
|
|
200
|
+
async addWebAuthnCredential(userId, credential) {
|
|
201
|
+
const user = _users.get(userId);
|
|
202
|
+
if (user)
|
|
203
|
+
user.webauthnCredentials.push({ ...credential });
|
|
204
|
+
},
|
|
205
|
+
async removeWebAuthnCredential(userId, credentialId) {
|
|
206
|
+
const user = _users.get(userId);
|
|
207
|
+
if (user)
|
|
208
|
+
user.webauthnCredentials = user.webauthnCredentials.filter((c) => c.credentialId !== credentialId);
|
|
209
|
+
},
|
|
210
|
+
async updateWebAuthnCredentialSignCount(userId, credentialId, signCount) {
|
|
211
|
+
const user = _users.get(userId);
|
|
212
|
+
if (!user)
|
|
213
|
+
return;
|
|
214
|
+
const cred = user.webauthnCredentials.find((c) => c.credentialId === credentialId);
|
|
215
|
+
if (cred)
|
|
216
|
+
cred.signCount = signCount;
|
|
217
|
+
},
|
|
218
|
+
async findUserByWebAuthnCredentialId(credentialId) {
|
|
219
|
+
for (const user of _users.values()) {
|
|
220
|
+
if (user.webauthnCredentials.some((c) => c.credentialId === credentialId))
|
|
221
|
+
return user.id;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
},
|
|
225
|
+
async getTenantRoles(userId, tenantId) {
|
|
226
|
+
return _tenantRoles.get(`${userId}:${tenantId}`) ?? [];
|
|
227
|
+
},
|
|
228
|
+
async setTenantRoles(userId, tenantId, roles) {
|
|
229
|
+
_tenantRoles.set(`${userId}:${tenantId}`, [...roles]);
|
|
230
|
+
},
|
|
231
|
+
async addTenantRole(userId, tenantId, role) {
|
|
232
|
+
const key = `${userId}:${tenantId}`;
|
|
233
|
+
const current = _tenantRoles.get(key) ?? [];
|
|
234
|
+
if (!current.includes(role)) {
|
|
235
|
+
_tenantRoles.set(key, [...current, role]);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
async removeTenantRole(userId, tenantId, role) {
|
|
239
|
+
const key = `${userId}:${tenantId}`;
|
|
240
|
+
const current = _tenantRoles.get(key);
|
|
241
|
+
if (current) {
|
|
242
|
+
_tenantRoles.set(key, current.filter((r) => r !== role));
|
|
243
|
+
}
|
|
244
|
+
},
|
|
135
245
|
};
|
|
136
246
|
// ---------------------------------------------------------------------------
|
|
137
247
|
// Session helpers (used by src/lib/session.ts)
|
|
@@ -160,8 +270,16 @@ export const memoryDeleteSession = (sessionId) => {
|
|
|
160
270
|
const entry = _sessions.get(sessionId);
|
|
161
271
|
if (!entry)
|
|
162
272
|
return;
|
|
273
|
+
// Clean up refresh token reverse-lookup keys
|
|
274
|
+
if (entry.refreshToken)
|
|
275
|
+
_refreshTokenIndex.delete(entry.refreshToken);
|
|
276
|
+
if (entry.prevRefreshToken)
|
|
277
|
+
_refreshTokenIndex.delete(entry.prevRefreshToken);
|
|
163
278
|
if (getPersistSessionMetadata()) {
|
|
164
279
|
entry.token = null;
|
|
280
|
+
entry.refreshToken = null;
|
|
281
|
+
entry.prevRefreshToken = null;
|
|
282
|
+
entry.prevTokenExpiresAt = null;
|
|
165
283
|
}
|
|
166
284
|
else {
|
|
167
285
|
_sessions.delete(sessionId);
|
|
@@ -231,6 +349,51 @@ export const memoryUpdateSessionLastActive = (sessionId) => {
|
|
|
231
349
|
if (entry)
|
|
232
350
|
entry.lastActiveAt = Date.now();
|
|
233
351
|
};
|
|
352
|
+
export const memorySetRefreshToken = (sessionId, refreshToken) => {
|
|
353
|
+
const entry = _sessions.get(sessionId);
|
|
354
|
+
if (!entry)
|
|
355
|
+
return;
|
|
356
|
+
entry.refreshToken = refreshToken;
|
|
357
|
+
_refreshTokenIndex.set(refreshToken, sessionId);
|
|
358
|
+
};
|
|
359
|
+
import { getRotationGraceSeconds } from "../lib/appConfig";
|
|
360
|
+
export const memoryGetSessionByRefreshToken = (refreshToken) => {
|
|
361
|
+
const sessionId = _refreshTokenIndex.get(refreshToken);
|
|
362
|
+
if (!sessionId)
|
|
363
|
+
return null;
|
|
364
|
+
const entry = _sessions.get(sessionId);
|
|
365
|
+
if (!entry)
|
|
366
|
+
return null;
|
|
367
|
+
// Current refresh token matches
|
|
368
|
+
if (entry.refreshToken === refreshToken) {
|
|
369
|
+
return { sessionId: entry.sessionId, userId: entry.userId, newRefreshToken: refreshToken };
|
|
370
|
+
}
|
|
371
|
+
// Check grace window
|
|
372
|
+
if (entry.prevRefreshToken === refreshToken && entry.prevTokenExpiresAt && entry.prevTokenExpiresAt > Date.now()) {
|
|
373
|
+
return { sessionId: entry.sessionId, userId: entry.userId, newRefreshToken: entry.refreshToken };
|
|
374
|
+
}
|
|
375
|
+
// Grace window expired — theft detected, invalidate session
|
|
376
|
+
if (entry.prevRefreshToken === refreshToken) {
|
|
377
|
+
memoryDeleteSession(sessionId);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
};
|
|
382
|
+
export const memoryRotateRefreshToken = (sessionId, newRefreshToken, newAccessToken) => {
|
|
383
|
+
const entry = _sessions.get(sessionId);
|
|
384
|
+
if (!entry)
|
|
385
|
+
return;
|
|
386
|
+
const graceSeconds = getRotationGraceSeconds();
|
|
387
|
+
// Move current to prev
|
|
388
|
+
const oldRefreshToken = entry.refreshToken;
|
|
389
|
+
entry.prevRefreshToken = oldRefreshToken;
|
|
390
|
+
entry.prevTokenExpiresAt = Date.now() + graceSeconds * 1000;
|
|
391
|
+
entry.refreshToken = newRefreshToken;
|
|
392
|
+
entry.token = newAccessToken;
|
|
393
|
+
// Update reverse-lookup index
|
|
394
|
+
_refreshTokenIndex.set(newRefreshToken, sessionId);
|
|
395
|
+
// Old token stays in index during grace window — cleaned up on next lookup or session delete
|
|
396
|
+
};
|
|
234
397
|
// ---------------------------------------------------------------------------
|
|
235
398
|
// OAuth state helpers (used by src/lib/oauth.ts)
|
|
236
399
|
// ---------------------------------------------------------------------------
|
|
@@ -313,3 +476,15 @@ export const memoryConsumeResetToken = (hash) => {
|
|
|
313
476
|
_resetTokens.delete(hash);
|
|
314
477
|
return { userId: entry.userId, email: entry.email };
|
|
315
478
|
};
|
|
479
|
+
export const memoryStoreOAuthCode = (hash, payload, ttlSeconds) => {
|
|
480
|
+
_oauthCodes.set(hash, { ...payload, expiresAt: Date.now() + ttlSeconds * 1000 });
|
|
481
|
+
};
|
|
482
|
+
export const memoryConsumeOAuthCode = (hash) => {
|
|
483
|
+
const entry = _oauthCodes.get(hash);
|
|
484
|
+
if (!entry || entry.expiresAt <= Date.now()) {
|
|
485
|
+
_oauthCodes.delete(hash);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
_oauthCodes.delete(hash);
|
|
489
|
+
return { token: entry.token, userId: entry.userId, email: entry.email, refreshToken: entry.refreshToken };
|
|
490
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthUser } from "../models/AuthUser";
|
|
2
|
+
import { TenantRole } from "../models/TenantRole";
|
|
2
3
|
import { HttpError } from "../lib/HttpError";
|
|
3
4
|
export const mongoAuthAdapter = {
|
|
4
5
|
async findByEmail(email) {
|
|
@@ -90,4 +91,97 @@ export const mongoAuthAdapter = {
|
|
|
90
91
|
const user = await AuthUser.findById(userId, "emailVerified").lean();
|
|
91
92
|
return user?.emailVerified ?? false;
|
|
92
93
|
},
|
|
94
|
+
async deleteUser(userId) {
|
|
95
|
+
await AuthUser.findByIdAndDelete(userId);
|
|
96
|
+
},
|
|
97
|
+
async hasPassword(userId) {
|
|
98
|
+
const user = await AuthUser.findById(userId, "password").lean();
|
|
99
|
+
return !!user?.password;
|
|
100
|
+
},
|
|
101
|
+
async setMfaSecret(userId, secret) {
|
|
102
|
+
await AuthUser.findByIdAndUpdate(userId, { mfaSecret: secret });
|
|
103
|
+
},
|
|
104
|
+
async getMfaSecret(userId) {
|
|
105
|
+
const user = await AuthUser.findById(userId, "mfaSecret").lean();
|
|
106
|
+
return user?.mfaSecret ?? null;
|
|
107
|
+
},
|
|
108
|
+
async isMfaEnabled(userId) {
|
|
109
|
+
const user = await AuthUser.findById(userId, "mfaEnabled").lean();
|
|
110
|
+
return user?.mfaEnabled ?? false;
|
|
111
|
+
},
|
|
112
|
+
async setMfaEnabled(userId, enabled) {
|
|
113
|
+
await AuthUser.findByIdAndUpdate(userId, { mfaEnabled: enabled });
|
|
114
|
+
},
|
|
115
|
+
async setRecoveryCodes(userId, codes) {
|
|
116
|
+
await AuthUser.findByIdAndUpdate(userId, { recoveryCodes: codes });
|
|
117
|
+
},
|
|
118
|
+
async getRecoveryCodes(userId) {
|
|
119
|
+
const user = await AuthUser.findById(userId, "recoveryCodes").lean();
|
|
120
|
+
return user?.recoveryCodes ?? [];
|
|
121
|
+
},
|
|
122
|
+
async removeRecoveryCode(userId, code) {
|
|
123
|
+
await AuthUser.findByIdAndUpdate(userId, { $pull: { recoveryCodes: code } });
|
|
124
|
+
},
|
|
125
|
+
async getMfaMethods(userId) {
|
|
126
|
+
const user = await AuthUser.findById(userId, "mfaMethods mfaEnabled").lean();
|
|
127
|
+
const methods = user?.mfaMethods ?? [];
|
|
128
|
+
// Backward compat: if mfaEnabled but no methods recorded, assume TOTP
|
|
129
|
+
if (methods.length === 0 && user?.mfaEnabled)
|
|
130
|
+
return ["totp"];
|
|
131
|
+
return methods;
|
|
132
|
+
},
|
|
133
|
+
async setMfaMethods(userId, methods) {
|
|
134
|
+
await AuthUser.findByIdAndUpdate(userId, { mfaMethods: methods });
|
|
135
|
+
},
|
|
136
|
+
async getWebAuthnCredentials(userId) {
|
|
137
|
+
const user = await AuthUser.findById(userId, "webauthnCredentials").lean();
|
|
138
|
+
const creds = user?.webauthnCredentials ?? [];
|
|
139
|
+
return creds.map((c) => ({
|
|
140
|
+
credentialId: c.credentialId,
|
|
141
|
+
publicKey: c.publicKey,
|
|
142
|
+
signCount: c.signCount,
|
|
143
|
+
transports: c.transports,
|
|
144
|
+
name: c.name,
|
|
145
|
+
createdAt: c.createdAt instanceof Date ? c.createdAt.getTime() : c.createdAt,
|
|
146
|
+
}));
|
|
147
|
+
},
|
|
148
|
+
async addWebAuthnCredential(userId, credential) {
|
|
149
|
+
await AuthUser.findByIdAndUpdate(userId, {
|
|
150
|
+
$push: {
|
|
151
|
+
webauthnCredentials: {
|
|
152
|
+
credentialId: credential.credentialId,
|
|
153
|
+
publicKey: credential.publicKey,
|
|
154
|
+
signCount: credential.signCount,
|
|
155
|
+
transports: credential.transports,
|
|
156
|
+
name: credential.name,
|
|
157
|
+
createdAt: new Date(credential.createdAt),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
async removeWebAuthnCredential(userId, credentialId) {
|
|
163
|
+
await AuthUser.findByIdAndUpdate(userId, {
|
|
164
|
+
$pull: { webauthnCredentials: { credentialId } },
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
async updateWebAuthnCredentialSignCount(userId, credentialId, signCount) {
|
|
168
|
+
await AuthUser.findOneAndUpdate({ _id: userId, "webauthnCredentials.credentialId": credentialId }, { $set: { "webauthnCredentials.$.signCount": signCount } });
|
|
169
|
+
},
|
|
170
|
+
async findUserByWebAuthnCredentialId(credentialId) {
|
|
171
|
+
const user = await AuthUser.findOne({ "webauthnCredentials.credentialId": credentialId }, "_id").lean();
|
|
172
|
+
return user ? String(user._id) : null;
|
|
173
|
+
},
|
|
174
|
+
async getTenantRoles(userId, tenantId) {
|
|
175
|
+
const doc = await TenantRole.findOne({ userId, tenantId }, "roles").lean();
|
|
176
|
+
return doc?.roles ?? [];
|
|
177
|
+
},
|
|
178
|
+
async setTenantRoles(userId, tenantId, roles) {
|
|
179
|
+
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $set: { roles } }, { upsert: true });
|
|
180
|
+
},
|
|
181
|
+
async addTenantRole(userId, tenantId, role) {
|
|
182
|
+
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $addToSet: { roles: role } }, { upsert: true });
|
|
183
|
+
},
|
|
184
|
+
async removeTenantRole(userId, tenantId, role) {
|
|
185
|
+
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $pull: { roles: role } });
|
|
186
|
+
},
|
|
93
187
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import type { AuthAdapter } from "../lib/authAdapter";
|
|
2
3
|
export declare const setSqliteDb: (path: string) => void;
|
|
4
|
+
export declare function getDb(): Database;
|
|
3
5
|
export declare const sqliteAuthAdapter: AuthAdapter;
|
|
4
6
|
import type { SessionMetadata, SessionInfo } from "../lib/session";
|
|
5
7
|
export declare const sqliteCreateSession: (userId: string, token: string, sessionId: string, metadata?: SessionMetadata) => void;
|
|
@@ -9,6 +11,10 @@ export declare const sqliteGetUserSessions: (userId: string) => SessionInfo[];
|
|
|
9
11
|
export declare const sqliteGetActiveSessionCount: (userId: string) => number;
|
|
10
12
|
export declare const sqliteEvictOldestSession: (userId: string) => void;
|
|
11
13
|
export declare const sqliteUpdateSessionLastActive: (sessionId: string) => void;
|
|
14
|
+
import type { RefreshResult } from "../lib/session";
|
|
15
|
+
export declare const sqliteSetRefreshToken: (sessionId: string, refreshToken: string) => void;
|
|
16
|
+
export declare const sqliteGetSessionByRefreshToken: (refreshToken: string) => RefreshResult | null;
|
|
17
|
+
export declare const sqliteRotateRefreshToken: (sessionId: string, newRefreshToken: string, newAccessToken: string) => void;
|
|
12
18
|
export declare const sqliteStoreOAuthState: (state: string, codeVerifier?: string, linkUserId?: string) => void;
|
|
13
19
|
export declare const sqliteConsumeOAuthState: (state: string) => {
|
|
14
20
|
codeVerifier?: string;
|
|
@@ -30,4 +36,7 @@ export declare const sqliteConsumeResetToken: (hash: string) => {
|
|
|
30
36
|
userId: string;
|
|
31
37
|
email: string;
|
|
32
38
|
} | null;
|
|
39
|
+
import type { OAuthCodePayload } from "../lib/oauthCode";
|
|
40
|
+
export declare const sqliteStoreOAuthCode: (hash: string, payload: OAuthCodePayload, ttlSeconds: number) => void;
|
|
41
|
+
export declare const sqliteConsumeOAuthCode: (hash: string) => OAuthCodePayload | null;
|
|
33
42
|
export declare const startSqliteCleanup: (intervalMs?: number) => ReturnType<typeof setInterval>;
|
|
@@ -10,7 +10,7 @@ export const setSqliteDb = (path) => {
|
|
|
10
10
|
_db.run("PRAGMA foreign_keys = ON");
|
|
11
11
|
initSchema(_db);
|
|
12
12
|
};
|
|
13
|
-
function getDb() {
|
|
13
|
+
export function getDb() {
|
|
14
14
|
if (!_db)
|
|
15
15
|
throw new Error("SQLite not initialized — call setSqliteDb(path) before using sqliteAuthAdapter or sessionStore: 'sqlite'");
|
|
16
16
|
return _db;
|
|
@@ -32,6 +32,23 @@ function initSchema(db) {
|
|
|
32
32
|
db.run("ALTER TABLE users ADD COLUMN emailVerified INTEGER NOT NULL DEFAULT 0");
|
|
33
33
|
}
|
|
34
34
|
catch { /* already exists */ }
|
|
35
|
+
// Add MFA columns to pre-existing databases
|
|
36
|
+
try {
|
|
37
|
+
db.run("ALTER TABLE users ADD COLUMN mfaSecret TEXT");
|
|
38
|
+
}
|
|
39
|
+
catch { /* already exists */ }
|
|
40
|
+
try {
|
|
41
|
+
db.run("ALTER TABLE users ADD COLUMN mfaEnabled INTEGER NOT NULL DEFAULT 0");
|
|
42
|
+
}
|
|
43
|
+
catch { /* already exists */ }
|
|
44
|
+
try {
|
|
45
|
+
db.run("ALTER TABLE users ADD COLUMN recoveryCodes TEXT NOT NULL DEFAULT '[]'");
|
|
46
|
+
}
|
|
47
|
+
catch { /* already exists */ }
|
|
48
|
+
try {
|
|
49
|
+
db.run("ALTER TABLE users ADD COLUMN mfaMethods TEXT NOT NULL DEFAULT '[]'");
|
|
50
|
+
}
|
|
51
|
+
catch { /* already exists */ }
|
|
35
52
|
// Migrate legacy sessions table (userId PK) to new multi-session schema (sessionId PK)
|
|
36
53
|
try {
|
|
37
54
|
db.run("ALTER TABLE sessions RENAME TO sessions_legacy");
|
|
@@ -48,6 +65,20 @@ function initSchema(db) {
|
|
|
48
65
|
userAgent TEXT
|
|
49
66
|
)`);
|
|
50
67
|
db.run("CREATE INDEX IF NOT EXISTS idx_sessions_userId ON sessions(userId)");
|
|
68
|
+
// Add refresh token columns to pre-existing databases
|
|
69
|
+
try {
|
|
70
|
+
db.run("ALTER TABLE sessions ADD COLUMN refreshToken TEXT");
|
|
71
|
+
}
|
|
72
|
+
catch { /* already exists */ }
|
|
73
|
+
try {
|
|
74
|
+
db.run("ALTER TABLE sessions ADD COLUMN prevRefreshToken TEXT");
|
|
75
|
+
}
|
|
76
|
+
catch { /* already exists */ }
|
|
77
|
+
try {
|
|
78
|
+
db.run("ALTER TABLE sessions ADD COLUMN prevTokenExpiresAt INTEGER");
|
|
79
|
+
}
|
|
80
|
+
catch { /* already exists */ }
|
|
81
|
+
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_refreshToken ON sessions(refreshToken) WHERE refreshToken IS NOT NULL");
|
|
51
82
|
db.run(`CREATE TABLE IF NOT EXISTS oauth_states (
|
|
52
83
|
state TEXT PRIMARY KEY,
|
|
53
84
|
codeVerifier TEXT,
|
|
@@ -71,6 +102,31 @@ function initSchema(db) {
|
|
|
71
102
|
email TEXT NOT NULL,
|
|
72
103
|
expiresAt INTEGER NOT NULL
|
|
73
104
|
)`);
|
|
105
|
+
db.run(`CREATE TABLE IF NOT EXISTS tenant_roles (
|
|
106
|
+
userId TEXT NOT NULL,
|
|
107
|
+
tenantId TEXT NOT NULL,
|
|
108
|
+
role TEXT NOT NULL,
|
|
109
|
+
PRIMARY KEY (userId, tenantId, role)
|
|
110
|
+
)`);
|
|
111
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_tenant_roles_tenant ON tenant_roles(tenantId)");
|
|
112
|
+
db.run(`CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
|
113
|
+
credentialId TEXT PRIMARY KEY,
|
|
114
|
+
userId TEXT NOT NULL,
|
|
115
|
+
publicKey TEXT NOT NULL,
|
|
116
|
+
signCount INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
transports TEXT NOT NULL DEFAULT '[]',
|
|
118
|
+
name TEXT,
|
|
119
|
+
createdAt INTEGER NOT NULL
|
|
120
|
+
)`);
|
|
121
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_webauthn_userId ON webauthn_credentials(userId)");
|
|
122
|
+
db.run(`CREATE TABLE IF NOT EXISTS oauth_codes (
|
|
123
|
+
codeHash TEXT PRIMARY KEY,
|
|
124
|
+
token TEXT NOT NULL,
|
|
125
|
+
userId TEXT NOT NULL,
|
|
126
|
+
email TEXT,
|
|
127
|
+
refreshToken TEXT,
|
|
128
|
+
expiresAt INTEGER NOT NULL
|
|
129
|
+
)`);
|
|
74
130
|
}
|
|
75
131
|
// ---------------------------------------------------------------------------
|
|
76
132
|
// Auth adapter
|
|
@@ -177,6 +233,98 @@ export const sqliteAuthAdapter = {
|
|
|
177
233
|
const row = getDb().query("SELECT emailVerified FROM users WHERE id = ?").get(userId);
|
|
178
234
|
return row?.emailVerified === 1;
|
|
179
235
|
},
|
|
236
|
+
async deleteUser(userId) {
|
|
237
|
+
getDb().run("DELETE FROM users WHERE id = ?", [userId]);
|
|
238
|
+
},
|
|
239
|
+
async hasPassword(userId) {
|
|
240
|
+
const row = getDb().query("SELECT passwordHash FROM users WHERE id = ?").get(userId);
|
|
241
|
+
return !!row?.passwordHash;
|
|
242
|
+
},
|
|
243
|
+
async setMfaSecret(userId, secret) {
|
|
244
|
+
getDb().run("UPDATE users SET mfaSecret = ? WHERE id = ?", [secret, userId]);
|
|
245
|
+
},
|
|
246
|
+
async getMfaSecret(userId) {
|
|
247
|
+
const row = getDb().query("SELECT mfaSecret FROM users WHERE id = ?").get(userId);
|
|
248
|
+
return row?.mfaSecret ?? null;
|
|
249
|
+
},
|
|
250
|
+
async isMfaEnabled(userId) {
|
|
251
|
+
const row = getDb().query("SELECT mfaEnabled FROM users WHERE id = ?").get(userId);
|
|
252
|
+
return row?.mfaEnabled === 1;
|
|
253
|
+
},
|
|
254
|
+
async setMfaEnabled(userId, enabled) {
|
|
255
|
+
getDb().run("UPDATE users SET mfaEnabled = ? WHERE id = ?", [enabled ? 1 : 0, userId]);
|
|
256
|
+
},
|
|
257
|
+
async setRecoveryCodes(userId, codes) {
|
|
258
|
+
getDb().run("UPDATE users SET recoveryCodes = ? WHERE id = ?", [JSON.stringify(codes), userId]);
|
|
259
|
+
},
|
|
260
|
+
async getRecoveryCodes(userId) {
|
|
261
|
+
const row = getDb().query("SELECT recoveryCodes FROM users WHERE id = ?").get(userId);
|
|
262
|
+
return row?.recoveryCodes ? JSON.parse(row.recoveryCodes) : [];
|
|
263
|
+
},
|
|
264
|
+
async removeRecoveryCode(userId, code) {
|
|
265
|
+
const current = await sqliteAuthAdapter.getRecoveryCodes(userId);
|
|
266
|
+
const idx = current.indexOf(code);
|
|
267
|
+
if (idx !== -1) {
|
|
268
|
+
current.splice(idx, 1);
|
|
269
|
+
await sqliteAuthAdapter.setRecoveryCodes(userId, current);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
async getMfaMethods(userId) {
|
|
273
|
+
const row = getDb().query("SELECT mfaMethods, mfaEnabled FROM users WHERE id = ?").get(userId);
|
|
274
|
+
const methods = row?.mfaMethods ? JSON.parse(row.mfaMethods) : [];
|
|
275
|
+
// Backward compat: if mfaEnabled but no methods recorded, assume TOTP
|
|
276
|
+
if (methods.length === 0 && row?.mfaEnabled === 1)
|
|
277
|
+
return ["totp"];
|
|
278
|
+
return methods;
|
|
279
|
+
},
|
|
280
|
+
async setMfaMethods(userId, methods) {
|
|
281
|
+
getDb().run("UPDATE users SET mfaMethods = ? WHERE id = ?", [JSON.stringify(methods), userId]);
|
|
282
|
+
},
|
|
283
|
+
async getWebAuthnCredentials(userId) {
|
|
284
|
+
const rows = getDb().query("SELECT credentialId, publicKey, signCount, transports, name, createdAt FROM webauthn_credentials WHERE userId = ?").all(userId);
|
|
285
|
+
return rows.map((r) => ({
|
|
286
|
+
credentialId: r.credentialId,
|
|
287
|
+
publicKey: r.publicKey,
|
|
288
|
+
signCount: r.signCount,
|
|
289
|
+
transports: JSON.parse(r.transports),
|
|
290
|
+
name: r.name ?? undefined,
|
|
291
|
+
createdAt: r.createdAt,
|
|
292
|
+
}));
|
|
293
|
+
},
|
|
294
|
+
async addWebAuthnCredential(userId, credential) {
|
|
295
|
+
getDb().run("INSERT INTO webauthn_credentials (credentialId, userId, publicKey, signCount, transports, name, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?)", [credential.credentialId, userId, credential.publicKey, credential.signCount, JSON.stringify(credential.transports ?? []), credential.name ?? null, credential.createdAt]);
|
|
296
|
+
},
|
|
297
|
+
async removeWebAuthnCredential(userId, credentialId) {
|
|
298
|
+
getDb().run("DELETE FROM webauthn_credentials WHERE credentialId = ? AND userId = ?", [credentialId, userId]);
|
|
299
|
+
},
|
|
300
|
+
async updateWebAuthnCredentialSignCount(userId, credentialId, signCount) {
|
|
301
|
+
getDb().run("UPDATE webauthn_credentials SET signCount = ? WHERE credentialId = ? AND userId = ?", [signCount, credentialId, userId]);
|
|
302
|
+
},
|
|
303
|
+
async findUserByWebAuthnCredentialId(credentialId) {
|
|
304
|
+
const row = getDb().query("SELECT userId FROM webauthn_credentials WHERE credentialId = ?").get(credentialId);
|
|
305
|
+
return row?.userId ?? null;
|
|
306
|
+
},
|
|
307
|
+
async getTenantRoles(userId, tenantId) {
|
|
308
|
+
const rows = getDb().query("SELECT role FROM tenant_roles WHERE userId = ? AND tenantId = ?").all(userId, tenantId);
|
|
309
|
+
return rows.map((r) => r.role);
|
|
310
|
+
},
|
|
311
|
+
async setTenantRoles(userId, tenantId, roles) {
|
|
312
|
+
const db = getDb();
|
|
313
|
+
db.run("DELETE FROM tenant_roles WHERE userId = ? AND tenantId = ?", [userId, tenantId]);
|
|
314
|
+
const stmt = db.prepare("INSERT INTO tenant_roles (userId, tenantId, role) VALUES (?, ?, ?)");
|
|
315
|
+
for (const role of roles) {
|
|
316
|
+
stmt.run(userId, tenantId, role);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
async addTenantRole(userId, tenantId, role) {
|
|
320
|
+
try {
|
|
321
|
+
getDb().run("INSERT INTO tenant_roles (userId, tenantId, role) VALUES (?, ?, ?)", [userId, tenantId, role]);
|
|
322
|
+
}
|
|
323
|
+
catch { /* already exists */ }
|
|
324
|
+
},
|
|
325
|
+
async removeTenantRole(userId, tenantId, role) {
|
|
326
|
+
getDb().run("DELETE FROM tenant_roles WHERE userId = ? AND tenantId = ? AND role = ?", [userId, tenantId, role]);
|
|
327
|
+
},
|
|
180
328
|
};
|
|
181
329
|
import { getPersistSessionMetadata, getIncludeInactiveSessions } from "../lib/appConfig";
|
|
182
330
|
const SESSION_TTL_MS = 60 * 60 * 24 * 7 * 1000; // 7 days
|
|
@@ -193,7 +341,7 @@ export const sqliteGetSession = (sessionId) => {
|
|
|
193
341
|
};
|
|
194
342
|
export const sqliteDeleteSession = (sessionId) => {
|
|
195
343
|
if (getPersistSessionMetadata()) {
|
|
196
|
-
getDb().run("UPDATE sessions SET token = NULL WHERE sessionId = ?", [sessionId]);
|
|
344
|
+
getDb().run("UPDATE sessions SET token = NULL, refreshToken = NULL, prevRefreshToken = NULL, prevTokenExpiresAt = NULL WHERE sessionId = ?", [sessionId]);
|
|
197
345
|
}
|
|
198
346
|
else {
|
|
199
347
|
getDb().run("DELETE FROM sessions WHERE sessionId = ?", [sessionId]);
|
|
@@ -236,6 +384,35 @@ export const sqliteEvictOldestSession = (userId) => {
|
|
|
236
384
|
export const sqliteUpdateSessionLastActive = (sessionId) => {
|
|
237
385
|
getDb().run("UPDATE sessions SET lastActiveAt = ? WHERE sessionId = ?", [Date.now(), sessionId]);
|
|
238
386
|
};
|
|
387
|
+
import { getRotationGraceSeconds } from "../lib/appConfig";
|
|
388
|
+
export const sqliteSetRefreshToken = (sessionId, refreshToken) => {
|
|
389
|
+
getDb().run("UPDATE sessions SET refreshToken = ? WHERE sessionId = ?", [refreshToken, sessionId]);
|
|
390
|
+
};
|
|
391
|
+
export const sqliteGetSessionByRefreshToken = (refreshToken) => {
|
|
392
|
+
const db = getDb();
|
|
393
|
+
// Check current refresh token
|
|
394
|
+
let row = db.query("SELECT sessionId, userId, refreshToken FROM sessions WHERE refreshToken = ?").get(refreshToken);
|
|
395
|
+
if (row) {
|
|
396
|
+
return { sessionId: row.sessionId, userId: row.userId, newRefreshToken: refreshToken };
|
|
397
|
+
}
|
|
398
|
+
// Check previous refresh token (grace window)
|
|
399
|
+
row = db.query("SELECT sessionId, userId, refreshToken, prevTokenExpiresAt FROM sessions WHERE prevRefreshToken = ?").get(refreshToken);
|
|
400
|
+
if (!row)
|
|
401
|
+
return null;
|
|
402
|
+
const prevExpiry = row.prevTokenExpiresAt;
|
|
403
|
+
if (prevExpiry && prevExpiry > Date.now()) {
|
|
404
|
+
// Within grace window — return current refresh token
|
|
405
|
+
return { sessionId: row.sessionId, userId: row.userId, newRefreshToken: row.refreshToken };
|
|
406
|
+
}
|
|
407
|
+
// Grace window expired — theft detected, invalidate session
|
|
408
|
+
sqliteDeleteSession(row.sessionId);
|
|
409
|
+
return null;
|
|
410
|
+
};
|
|
411
|
+
export const sqliteRotateRefreshToken = (sessionId, newRefreshToken, newAccessToken) => {
|
|
412
|
+
const graceSeconds = getRotationGraceSeconds();
|
|
413
|
+
const prevTokenExpiresAt = Date.now() + graceSeconds * 1000;
|
|
414
|
+
getDb().run("UPDATE sessions SET prevRefreshToken = refreshToken, prevTokenExpiresAt = ?, refreshToken = ?, token = ? WHERE sessionId = ?", [prevTokenExpiresAt, newRefreshToken, newAccessToken, sessionId]);
|
|
415
|
+
};
|
|
239
416
|
// ---------------------------------------------------------------------------
|
|
240
417
|
// OAuth state helpers (used by src/lib/oauth.ts)
|
|
241
418
|
// ---------------------------------------------------------------------------
|
|
@@ -298,6 +475,16 @@ export const sqliteConsumeResetToken = (hash) => {
|
|
|
298
475
|
const row = getDb().query("DELETE FROM password_resets WHERE token = ? AND expiresAt > ? RETURNING userId, email").get(hash, Date.now());
|
|
299
476
|
return row ?? null;
|
|
300
477
|
};
|
|
478
|
+
export const sqliteStoreOAuthCode = (hash, payload, ttlSeconds) => {
|
|
479
|
+
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
480
|
+
getDb().run("INSERT INTO oauth_codes (codeHash, token, userId, email, refreshToken, expiresAt) VALUES (?, ?, ?, ?, ?, ?)", [hash, payload.token, payload.userId, payload.email ?? null, payload.refreshToken ?? null, expiresAt]);
|
|
481
|
+
};
|
|
482
|
+
export const sqliteConsumeOAuthCode = (hash) => {
|
|
483
|
+
const row = getDb().query("DELETE FROM oauth_codes WHERE codeHash = ? AND expiresAt > ? RETURNING token, userId, email, refreshToken").get(hash, Date.now());
|
|
484
|
+
if (!row)
|
|
485
|
+
return null;
|
|
486
|
+
return { token: row.token, userId: row.userId, email: row.email ?? undefined, refreshToken: row.refreshToken ?? undefined };
|
|
487
|
+
};
|
|
301
488
|
// ---------------------------------------------------------------------------
|
|
302
489
|
// Optional periodic cleanup of expired rows
|
|
303
490
|
// ---------------------------------------------------------------------------
|
|
@@ -316,5 +503,6 @@ export const startSqliteCleanup = (intervalMs = 3_600_000) => {
|
|
|
316
503
|
db.run("DELETE FROM cache_entries WHERE expiresAt IS NOT NULL AND expiresAt <= ?", [now]);
|
|
317
504
|
db.run("DELETE FROM email_verifications WHERE expiresAt <= ?", [now]);
|
|
318
505
|
db.run("DELETE FROM password_resets WHERE expiresAt <= ?", [now]);
|
|
506
|
+
db.run("DELETE FROM oauth_codes WHERE expiresAt <= ?", [now]);
|
|
319
507
|
}, intervalMs);
|
|
320
508
|
};
|