@lastshotlabs/bunshot 0.0.13 → 0.0.16
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 +2510 -1747
- package/dist/adapters/memoryAuth.d.ts +4 -0
- package/dist/adapters/memoryAuth.js +131 -2
- package/dist/adapters/mongoAuth.js +56 -0
- package/dist/adapters/sqliteAuth.d.ts +6 -0
- package/dist/adapters/sqliteAuth.js +137 -2
- package/dist/app.d.ts +77 -2
- package/dist/app.js +29 -4
- package/dist/entrypoints/queue.d.ts +2 -2
- package/dist/entrypoints/queue.js +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.js +9 -3
- package/dist/lib/appConfig.d.ts +46 -0
- package/dist/lib/appConfig.js +20 -0
- package/dist/lib/authAdapter.d.ts +30 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -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/jwt.d.ts +1 -1
- package/dist/lib/jwt.js +2 -2
- package/dist/lib/mfaChallenge.d.ts +20 -0
- package/dist/lib/mfaChallenge.js +184 -0
- package/dist/lib/queue.d.ts +33 -0
- package/dist/lib/queue.js +98 -0
- 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 +163 -5
- package/dist/lib/tenant.d.ts +15 -0
- package/dist/lib/tenant.js +65 -0
- package/dist/lib/zodToMongoose.d.ts +38 -0
- package/dist/lib/zodToMongoose.js +84 -0
- package/dist/middleware/cacheResponse.js +4 -1
- package/dist/middleware/rateLimit.d.ts +2 -1
- package/dist/middleware/rateLimit.js +5 -2
- 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 +8 -0
- package/dist/models/AuthUser.js +8 -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 +153 -22
- package/dist/routes/jobs.d.ts +2 -0
- package/dist/routes/jobs.js +270 -0
- package/dist/routes/mfa.d.ts +1 -0
- package/dist/routes/mfa.js +409 -0
- package/dist/routes/oauth.js +107 -16
- package/dist/server.js +9 -0
- package/dist/services/auth.d.ts +17 -5
- package/dist/services/auth.js +95 -17
- package/dist/services/mfa.d.ts +37 -0
- package/dist/services/mfa.js +276 -0
- 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 +456 -0
- package/docs/sections/auth-flow/overview.md +10 -0
- package/docs/sections/cli/full.md +30 -0
- package/docs/sections/configuration/full.md +135 -0
- package/docs/sections/configuration/overview.md +17 -0
- package/docs/sections/configuration-example/full.md +99 -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 +83 -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 +62 -0
- package/docs/sections/multi-tenancy/overview.md +15 -0
- package/docs/sections/oauth/full.md +119 -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 +43 -0
- package/docs/sections/quick-start/full.md +43 -0
- package/docs/sections/response-caching/full.md +115 -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 +100 -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 +19 -10
|
@@ -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;
|
|
@@ -4,16 +4,20 @@ const _users = new Map();
|
|
|
4
4
|
const _byEmail = new Map();
|
|
5
5
|
const _sessions = new Map(); // sessionId → session
|
|
6
6
|
const _userSessionIds = new Map(); // userId → Set<sessionId>
|
|
7
|
+
const _refreshTokenIndex = new Map(); // refreshToken → sessionId
|
|
7
8
|
const _oauthStates = new Map();
|
|
8
9
|
const _cache = new Map();
|
|
9
10
|
const _verificationTokens = new Map();
|
|
10
11
|
const _resetTokens = new Map();
|
|
12
|
+
const _tenantRoles = new Map(); // "userId:tenantId" → roles
|
|
11
13
|
/** Reset all in-memory state. Useful for test isolation. */
|
|
12
14
|
export const clearMemoryStore = () => {
|
|
13
15
|
_users.clear();
|
|
14
16
|
_byEmail.clear();
|
|
15
17
|
_sessions.clear();
|
|
16
18
|
_userSessionIds.clear();
|
|
19
|
+
_refreshTokenIndex.clear();
|
|
20
|
+
_tenantRoles.clear();
|
|
17
21
|
_oauthStates.clear();
|
|
18
22
|
_cache.clear();
|
|
19
23
|
_verificationTokens.clear();
|
|
@@ -37,7 +41,7 @@ export const memoryAuthAdapter = {
|
|
|
37
41
|
if (_byEmail.has(normalised))
|
|
38
42
|
throw new HttpError(409, "Email already registered");
|
|
39
43
|
const id = crypto.randomUUID();
|
|
40
|
-
const user = { id, email: normalised, passwordHash, providerIds: [], roles: [], emailVerified: false };
|
|
44
|
+
const user = { id, email: normalised, passwordHash, providerIds: [], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [] };
|
|
41
45
|
_users.set(id, user);
|
|
42
46
|
_byEmail.set(normalised, id);
|
|
43
47
|
return { id };
|
|
@@ -63,7 +67,7 @@ export const memoryAuthAdapter = {
|
|
|
63
67
|
}
|
|
64
68
|
const id = crypto.randomUUID();
|
|
65
69
|
const email = profile.email ? profile.email.toLowerCase() : null;
|
|
66
|
-
const user = { id, email, passwordHash: null, providerIds: [key], roles: [], emailVerified: false };
|
|
70
|
+
const user = { id, email, passwordHash: null, providerIds: [key], roles: [], emailVerified: false, mfaSecret: null, mfaEnabled: false, recoveryCodes: [], mfaMethods: [] };
|
|
67
71
|
_users.set(id, user);
|
|
68
72
|
if (email)
|
|
69
73
|
_byEmail.set(email, id);
|
|
@@ -132,6 +136,78 @@ export const memoryAuthAdapter = {
|
|
|
132
136
|
async getEmailVerified(userId) {
|
|
133
137
|
return _users.get(userId)?.emailVerified ?? false;
|
|
134
138
|
},
|
|
139
|
+
async deleteUser(userId) {
|
|
140
|
+
const user = _users.get(userId);
|
|
141
|
+
if (user?.email)
|
|
142
|
+
_byEmail.delete(user.email);
|
|
143
|
+
_users.delete(userId);
|
|
144
|
+
},
|
|
145
|
+
async hasPassword(userId) {
|
|
146
|
+
return !!_users.get(userId)?.passwordHash;
|
|
147
|
+
},
|
|
148
|
+
async setMfaSecret(userId, secret) {
|
|
149
|
+
const user = _users.get(userId);
|
|
150
|
+
if (user)
|
|
151
|
+
user.mfaSecret = secret;
|
|
152
|
+
},
|
|
153
|
+
async getMfaSecret(userId) {
|
|
154
|
+
return _users.get(userId)?.mfaSecret ?? null;
|
|
155
|
+
},
|
|
156
|
+
async isMfaEnabled(userId) {
|
|
157
|
+
return _users.get(userId)?.mfaEnabled ?? false;
|
|
158
|
+
},
|
|
159
|
+
async setMfaEnabled(userId, enabled) {
|
|
160
|
+
const user = _users.get(userId);
|
|
161
|
+
if (user)
|
|
162
|
+
user.mfaEnabled = enabled;
|
|
163
|
+
},
|
|
164
|
+
async setRecoveryCodes(userId, codes) {
|
|
165
|
+
const user = _users.get(userId);
|
|
166
|
+
if (user)
|
|
167
|
+
user.recoveryCodes = [...codes];
|
|
168
|
+
},
|
|
169
|
+
async getRecoveryCodes(userId) {
|
|
170
|
+
return _users.get(userId)?.recoveryCodes ?? [];
|
|
171
|
+
},
|
|
172
|
+
async removeRecoveryCode(userId, code) {
|
|
173
|
+
const user = _users.get(userId);
|
|
174
|
+
if (user)
|
|
175
|
+
user.recoveryCodes = user.recoveryCodes.filter((c) => c !== code);
|
|
176
|
+
},
|
|
177
|
+
async getMfaMethods(userId) {
|
|
178
|
+
const user = _users.get(userId);
|
|
179
|
+
if (!user)
|
|
180
|
+
return [];
|
|
181
|
+
// Backward compat: if mfaEnabled but no methods recorded, assume TOTP
|
|
182
|
+
if (user.mfaMethods.length === 0 && user.mfaEnabled)
|
|
183
|
+
return ["totp"];
|
|
184
|
+
return [...user.mfaMethods];
|
|
185
|
+
},
|
|
186
|
+
async setMfaMethods(userId, methods) {
|
|
187
|
+
const user = _users.get(userId);
|
|
188
|
+
if (user)
|
|
189
|
+
user.mfaMethods = [...methods];
|
|
190
|
+
},
|
|
191
|
+
async getTenantRoles(userId, tenantId) {
|
|
192
|
+
return _tenantRoles.get(`${userId}:${tenantId}`) ?? [];
|
|
193
|
+
},
|
|
194
|
+
async setTenantRoles(userId, tenantId, roles) {
|
|
195
|
+
_tenantRoles.set(`${userId}:${tenantId}`, [...roles]);
|
|
196
|
+
},
|
|
197
|
+
async addTenantRole(userId, tenantId, role) {
|
|
198
|
+
const key = `${userId}:${tenantId}`;
|
|
199
|
+
const current = _tenantRoles.get(key) ?? [];
|
|
200
|
+
if (!current.includes(role)) {
|
|
201
|
+
_tenantRoles.set(key, [...current, role]);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
async removeTenantRole(userId, tenantId, role) {
|
|
205
|
+
const key = `${userId}:${tenantId}`;
|
|
206
|
+
const current = _tenantRoles.get(key);
|
|
207
|
+
if (current) {
|
|
208
|
+
_tenantRoles.set(key, current.filter((r) => r !== role));
|
|
209
|
+
}
|
|
210
|
+
},
|
|
135
211
|
};
|
|
136
212
|
// ---------------------------------------------------------------------------
|
|
137
213
|
// Session helpers (used by src/lib/session.ts)
|
|
@@ -160,8 +236,16 @@ export const memoryDeleteSession = (sessionId) => {
|
|
|
160
236
|
const entry = _sessions.get(sessionId);
|
|
161
237
|
if (!entry)
|
|
162
238
|
return;
|
|
239
|
+
// Clean up refresh token reverse-lookup keys
|
|
240
|
+
if (entry.refreshToken)
|
|
241
|
+
_refreshTokenIndex.delete(entry.refreshToken);
|
|
242
|
+
if (entry.prevRefreshToken)
|
|
243
|
+
_refreshTokenIndex.delete(entry.prevRefreshToken);
|
|
163
244
|
if (getPersistSessionMetadata()) {
|
|
164
245
|
entry.token = null;
|
|
246
|
+
entry.refreshToken = null;
|
|
247
|
+
entry.prevRefreshToken = null;
|
|
248
|
+
entry.prevTokenExpiresAt = null;
|
|
165
249
|
}
|
|
166
250
|
else {
|
|
167
251
|
_sessions.delete(sessionId);
|
|
@@ -231,6 +315,51 @@ export const memoryUpdateSessionLastActive = (sessionId) => {
|
|
|
231
315
|
if (entry)
|
|
232
316
|
entry.lastActiveAt = Date.now();
|
|
233
317
|
};
|
|
318
|
+
export const memorySetRefreshToken = (sessionId, refreshToken) => {
|
|
319
|
+
const entry = _sessions.get(sessionId);
|
|
320
|
+
if (!entry)
|
|
321
|
+
return;
|
|
322
|
+
entry.refreshToken = refreshToken;
|
|
323
|
+
_refreshTokenIndex.set(refreshToken, sessionId);
|
|
324
|
+
};
|
|
325
|
+
import { getRotationGraceSeconds } from "../lib/appConfig";
|
|
326
|
+
export const memoryGetSessionByRefreshToken = (refreshToken) => {
|
|
327
|
+
const sessionId = _refreshTokenIndex.get(refreshToken);
|
|
328
|
+
if (!sessionId)
|
|
329
|
+
return null;
|
|
330
|
+
const entry = _sessions.get(sessionId);
|
|
331
|
+
if (!entry)
|
|
332
|
+
return null;
|
|
333
|
+
// Current refresh token matches
|
|
334
|
+
if (entry.refreshToken === refreshToken) {
|
|
335
|
+
return { sessionId: entry.sessionId, userId: entry.userId, newRefreshToken: refreshToken };
|
|
336
|
+
}
|
|
337
|
+
// Check grace window
|
|
338
|
+
if (entry.prevRefreshToken === refreshToken && entry.prevTokenExpiresAt && entry.prevTokenExpiresAt > Date.now()) {
|
|
339
|
+
return { sessionId: entry.sessionId, userId: entry.userId, newRefreshToken: entry.refreshToken };
|
|
340
|
+
}
|
|
341
|
+
// Grace window expired — theft detected, invalidate session
|
|
342
|
+
if (entry.prevRefreshToken === refreshToken) {
|
|
343
|
+
memoryDeleteSession(sessionId);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
};
|
|
348
|
+
export const memoryRotateRefreshToken = (sessionId, newRefreshToken, newAccessToken) => {
|
|
349
|
+
const entry = _sessions.get(sessionId);
|
|
350
|
+
if (!entry)
|
|
351
|
+
return;
|
|
352
|
+
const graceSeconds = getRotationGraceSeconds();
|
|
353
|
+
// Move current to prev
|
|
354
|
+
const oldRefreshToken = entry.refreshToken;
|
|
355
|
+
entry.prevRefreshToken = oldRefreshToken;
|
|
356
|
+
entry.prevTokenExpiresAt = Date.now() + graceSeconds * 1000;
|
|
357
|
+
entry.refreshToken = newRefreshToken;
|
|
358
|
+
entry.token = newAccessToken;
|
|
359
|
+
// Update reverse-lookup index
|
|
360
|
+
_refreshTokenIndex.set(newRefreshToken, sessionId);
|
|
361
|
+
// Old token stays in index during grace window — cleaned up on next lookup or session delete
|
|
362
|
+
};
|
|
234
363
|
// ---------------------------------------------------------------------------
|
|
235
364
|
// OAuth state helpers (used by src/lib/oauth.ts)
|
|
236
365
|
// ---------------------------------------------------------------------------
|
|
@@ -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,59 @@ 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 getTenantRoles(userId, tenantId) {
|
|
137
|
+
const doc = await TenantRole.findOne({ userId, tenantId }, "roles").lean();
|
|
138
|
+
return doc?.roles ?? [];
|
|
139
|
+
},
|
|
140
|
+
async setTenantRoles(userId, tenantId, roles) {
|
|
141
|
+
await TenantRole.findOneAndUpdate({ userId, tenantId }, { roles }, { upsert: true });
|
|
142
|
+
},
|
|
143
|
+
async addTenantRole(userId, tenantId, role) {
|
|
144
|
+
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $addToSet: { roles: role } }, { upsert: true });
|
|
145
|
+
},
|
|
146
|
+
async removeTenantRole(userId, tenantId, role) {
|
|
147
|
+
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $pull: { roles: role } });
|
|
148
|
+
},
|
|
93
149
|
};
|
|
@@ -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;
|
|
@@ -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,13 @@ 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)");
|
|
74
112
|
}
|
|
75
113
|
// ---------------------------------------------------------------------------
|
|
76
114
|
// Auth adapter
|
|
@@ -177,6 +215,74 @@ export const sqliteAuthAdapter = {
|
|
|
177
215
|
const row = getDb().query("SELECT emailVerified FROM users WHERE id = ?").get(userId);
|
|
178
216
|
return row?.emailVerified === 1;
|
|
179
217
|
},
|
|
218
|
+
async deleteUser(userId) {
|
|
219
|
+
getDb().run("DELETE FROM users WHERE id = ?", [userId]);
|
|
220
|
+
},
|
|
221
|
+
async hasPassword(userId) {
|
|
222
|
+
const row = getDb().query("SELECT passwordHash FROM users WHERE id = ?").get(userId);
|
|
223
|
+
return !!row?.passwordHash;
|
|
224
|
+
},
|
|
225
|
+
async setMfaSecret(userId, secret) {
|
|
226
|
+
getDb().run("UPDATE users SET mfaSecret = ? WHERE id = ?", [secret, userId]);
|
|
227
|
+
},
|
|
228
|
+
async getMfaSecret(userId) {
|
|
229
|
+
const row = getDb().query("SELECT mfaSecret FROM users WHERE id = ?").get(userId);
|
|
230
|
+
return row?.mfaSecret ?? null;
|
|
231
|
+
},
|
|
232
|
+
async isMfaEnabled(userId) {
|
|
233
|
+
const row = getDb().query("SELECT mfaEnabled FROM users WHERE id = ?").get(userId);
|
|
234
|
+
return row?.mfaEnabled === 1;
|
|
235
|
+
},
|
|
236
|
+
async setMfaEnabled(userId, enabled) {
|
|
237
|
+
getDb().run("UPDATE users SET mfaEnabled = ? WHERE id = ?", [enabled ? 1 : 0, userId]);
|
|
238
|
+
},
|
|
239
|
+
async setRecoveryCodes(userId, codes) {
|
|
240
|
+
getDb().run("UPDATE users SET recoveryCodes = ? WHERE id = ?", [JSON.stringify(codes), userId]);
|
|
241
|
+
},
|
|
242
|
+
async getRecoveryCodes(userId) {
|
|
243
|
+
const row = getDb().query("SELECT recoveryCodes FROM users WHERE id = ?").get(userId);
|
|
244
|
+
return row?.recoveryCodes ? JSON.parse(row.recoveryCodes) : [];
|
|
245
|
+
},
|
|
246
|
+
async removeRecoveryCode(userId, code) {
|
|
247
|
+
const current = await sqliteAuthAdapter.getRecoveryCodes(userId);
|
|
248
|
+
const idx = current.indexOf(code);
|
|
249
|
+
if (idx !== -1) {
|
|
250
|
+
current.splice(idx, 1);
|
|
251
|
+
await sqliteAuthAdapter.setRecoveryCodes(userId, current);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
async getMfaMethods(userId) {
|
|
255
|
+
const row = getDb().query("SELECT mfaMethods, mfaEnabled FROM users WHERE id = ?").get(userId);
|
|
256
|
+
const methods = row?.mfaMethods ? JSON.parse(row.mfaMethods) : [];
|
|
257
|
+
// Backward compat: if mfaEnabled but no methods recorded, assume TOTP
|
|
258
|
+
if (methods.length === 0 && row?.mfaEnabled === 1)
|
|
259
|
+
return ["totp"];
|
|
260
|
+
return methods;
|
|
261
|
+
},
|
|
262
|
+
async setMfaMethods(userId, methods) {
|
|
263
|
+
getDb().run("UPDATE users SET mfaMethods = ? WHERE id = ?", [JSON.stringify(methods), userId]);
|
|
264
|
+
},
|
|
265
|
+
async getTenantRoles(userId, tenantId) {
|
|
266
|
+
const rows = getDb().query("SELECT role FROM tenant_roles WHERE userId = ? AND tenantId = ?").all(userId, tenantId);
|
|
267
|
+
return rows.map((r) => r.role);
|
|
268
|
+
},
|
|
269
|
+
async setTenantRoles(userId, tenantId, roles) {
|
|
270
|
+
const db = getDb();
|
|
271
|
+
db.run("DELETE FROM tenant_roles WHERE userId = ? AND tenantId = ?", [userId, tenantId]);
|
|
272
|
+
const stmt = db.prepare("INSERT INTO tenant_roles (userId, tenantId, role) VALUES (?, ?, ?)");
|
|
273
|
+
for (const role of roles) {
|
|
274
|
+
stmt.run(userId, tenantId, role);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
async addTenantRole(userId, tenantId, role) {
|
|
278
|
+
try {
|
|
279
|
+
getDb().run("INSERT INTO tenant_roles (userId, tenantId, role) VALUES (?, ?, ?)", [userId, tenantId, role]);
|
|
280
|
+
}
|
|
281
|
+
catch { /* already exists */ }
|
|
282
|
+
},
|
|
283
|
+
async removeTenantRole(userId, tenantId, role) {
|
|
284
|
+
getDb().run("DELETE FROM tenant_roles WHERE userId = ? AND tenantId = ? AND role = ?", [userId, tenantId, role]);
|
|
285
|
+
},
|
|
180
286
|
};
|
|
181
287
|
import { getPersistSessionMetadata, getIncludeInactiveSessions } from "../lib/appConfig";
|
|
182
288
|
const SESSION_TTL_MS = 60 * 60 * 24 * 7 * 1000; // 7 days
|
|
@@ -193,7 +299,7 @@ export const sqliteGetSession = (sessionId) => {
|
|
|
193
299
|
};
|
|
194
300
|
export const sqliteDeleteSession = (sessionId) => {
|
|
195
301
|
if (getPersistSessionMetadata()) {
|
|
196
|
-
getDb().run("UPDATE sessions SET token = NULL WHERE sessionId = ?", [sessionId]);
|
|
302
|
+
getDb().run("UPDATE sessions SET token = NULL, refreshToken = NULL, prevRefreshToken = NULL, prevTokenExpiresAt = NULL WHERE sessionId = ?", [sessionId]);
|
|
197
303
|
}
|
|
198
304
|
else {
|
|
199
305
|
getDb().run("DELETE FROM sessions WHERE sessionId = ?", [sessionId]);
|
|
@@ -236,6 +342,35 @@ export const sqliteEvictOldestSession = (userId) => {
|
|
|
236
342
|
export const sqliteUpdateSessionLastActive = (sessionId) => {
|
|
237
343
|
getDb().run("UPDATE sessions SET lastActiveAt = ? WHERE sessionId = ?", [Date.now(), sessionId]);
|
|
238
344
|
};
|
|
345
|
+
import { getRotationGraceSeconds } from "../lib/appConfig";
|
|
346
|
+
export const sqliteSetRefreshToken = (sessionId, refreshToken) => {
|
|
347
|
+
getDb().run("UPDATE sessions SET refreshToken = ? WHERE sessionId = ?", [refreshToken, sessionId]);
|
|
348
|
+
};
|
|
349
|
+
export const sqliteGetSessionByRefreshToken = (refreshToken) => {
|
|
350
|
+
const db = getDb();
|
|
351
|
+
// Check current refresh token
|
|
352
|
+
let row = db.query("SELECT sessionId, userId, refreshToken FROM sessions WHERE refreshToken = ?").get(refreshToken);
|
|
353
|
+
if (row) {
|
|
354
|
+
return { sessionId: row.sessionId, userId: row.userId, newRefreshToken: refreshToken };
|
|
355
|
+
}
|
|
356
|
+
// Check previous refresh token (grace window)
|
|
357
|
+
row = db.query("SELECT sessionId, userId, refreshToken, prevTokenExpiresAt FROM sessions WHERE prevRefreshToken = ?").get(refreshToken);
|
|
358
|
+
if (!row)
|
|
359
|
+
return null;
|
|
360
|
+
const prevExpiry = row.prevTokenExpiresAt;
|
|
361
|
+
if (prevExpiry && prevExpiry > Date.now()) {
|
|
362
|
+
// Within grace window — return current refresh token
|
|
363
|
+
return { sessionId: row.sessionId, userId: row.userId, newRefreshToken: row.refreshToken };
|
|
364
|
+
}
|
|
365
|
+
// Grace window expired — theft detected, invalidate session
|
|
366
|
+
sqliteDeleteSession(row.sessionId);
|
|
367
|
+
return null;
|
|
368
|
+
};
|
|
369
|
+
export const sqliteRotateRefreshToken = (sessionId, newRefreshToken, newAccessToken) => {
|
|
370
|
+
const graceSeconds = getRotationGraceSeconds();
|
|
371
|
+
const prevTokenExpiresAt = Date.now() + graceSeconds * 1000;
|
|
372
|
+
getDb().run("UPDATE sessions SET prevRefreshToken = refreshToken, prevTokenExpiresAt = ?, refreshToken = ?, token = ? WHERE sessionId = ?", [prevTokenExpiresAt, newRefreshToken, newAccessToken, sessionId]);
|
|
373
|
+
};
|
|
239
374
|
// ---------------------------------------------------------------------------
|
|
240
375
|
// OAuth state helpers (used by src/lib/oauth.ts)
|
|
241
376
|
// ---------------------------------------------------------------------------
|
package/dist/app.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { MiddlewareHandler } from "hono";
|
|
3
3
|
import type { AppEnv } from "./lib/context";
|
|
4
|
-
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "./lib/appConfig";
|
|
4
|
+
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig } from "./lib/appConfig";
|
|
5
5
|
import type { AuthAdapter } from "./lib/authAdapter";
|
|
6
6
|
import type { OAuthProviderConfig } from "./lib/oauth";
|
|
7
7
|
type StoreType = "redis" | "mongo" | "sqlite" | "memory";
|
|
@@ -92,6 +92,11 @@ export interface AuthRateLimitConfig {
|
|
|
92
92
|
windowMs?: number;
|
|
93
93
|
max?: number;
|
|
94
94
|
};
|
|
95
|
+
/** Max account deletion attempts per user per window. Default: 3 per hour. */
|
|
96
|
+
deleteAccount?: {
|
|
97
|
+
windowMs?: number;
|
|
98
|
+
max?: number;
|
|
99
|
+
};
|
|
95
100
|
/**
|
|
96
101
|
* Store backend for auth rate limit counters.
|
|
97
102
|
* Defaults to "redis" when Redis is enabled, otherwise "memory".
|
|
@@ -136,6 +141,32 @@ export interface AuthConfig {
|
|
|
136
141
|
rateLimit?: AuthRateLimitConfig;
|
|
137
142
|
/** Session concurrency and metadata persistence policy. */
|
|
138
143
|
sessionPolicy?: AuthSessionPolicyConfig;
|
|
144
|
+
/** Account deletion configuration. Enables DELETE /auth/me when the adapter supports deleteUser. */
|
|
145
|
+
accountDeletion?: AccountDeletionConfig;
|
|
146
|
+
/**
|
|
147
|
+
* Refresh token configuration. When set, login/register return short-lived access tokens
|
|
148
|
+
* (default 15 min) alongside long-lived refresh tokens (default 30 days). Mounts POST /auth/refresh.
|
|
149
|
+
* When not configured, the existing 7-day JWT behavior is unchanged.
|
|
150
|
+
*/
|
|
151
|
+
refreshTokens?: RefreshTokenConfig;
|
|
152
|
+
/**
|
|
153
|
+
* MFA/TOTP configuration. When set, enables MFA setup/verify/disable routes under /auth/mfa/*.
|
|
154
|
+
* Login returns { mfaRequired: true, mfaToken } when MFA is enabled for the user.
|
|
155
|
+
* OAuth logins skip MFA (the OAuth provider is treated as the second factor).
|
|
156
|
+
*/
|
|
157
|
+
mfa?: MfaConfig;
|
|
158
|
+
}
|
|
159
|
+
export interface AccountDeletionConfig {
|
|
160
|
+
/** Called before deletion. Throw to abort (e.g., active subscription check). */
|
|
161
|
+
onBeforeDelete?: (userId: string) => Promise<void>;
|
|
162
|
+
/** Called after auth data is deleted. Runs at execution time — query current state, not a snapshot. */
|
|
163
|
+
onAfterDelete?: (userId: string) => Promise<void>;
|
|
164
|
+
/** When true, deletion is queued as a BullMQ job instead of running synchronously. Requires Redis + BullMQ. */
|
|
165
|
+
queued?: boolean;
|
|
166
|
+
/** Grace period in seconds before queued deletion executes. Default: 0 (immediate). */
|
|
167
|
+
gracePeriod?: number;
|
|
168
|
+
/** Called when deletion is scheduled (queued + gracePeriod > 0). Use to send a confirmation/cancel email. */
|
|
169
|
+
onDeletionScheduled?: (userId: string, email: string, cancelToken: string) => Promise<void>;
|
|
139
170
|
}
|
|
140
171
|
export interface AuthSessionPolicyConfig {
|
|
141
172
|
/** Max simultaneous active sessions per user. Oldest is evicted when exceeded. Default: 6. */
|
|
@@ -156,7 +187,7 @@ export interface AuthSessionPolicyConfig {
|
|
|
156
187
|
*/
|
|
157
188
|
trackLastActive?: boolean;
|
|
158
189
|
}
|
|
159
|
-
export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig };
|
|
190
|
+
export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig };
|
|
160
191
|
export interface BotProtectionConfig {
|
|
161
192
|
/**
|
|
162
193
|
* List of IPv4 CIDRs (e.g. "198.51.100.0/24"), IPv4 addresses, or IPv6 addresses to block outright.
|
|
@@ -217,6 +248,46 @@ export interface ModelSchemasConfig {
|
|
|
217
248
|
*/
|
|
218
249
|
registration?: "auto" | "explicit";
|
|
219
250
|
}
|
|
251
|
+
export interface JobsConfig {
|
|
252
|
+
/** Enable the job status endpoint. Default: false. */
|
|
253
|
+
statusEndpoint?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Auth protection for job endpoints.
|
|
256
|
+
* - `"userAuth"` — requires authenticated user session (cookie/token).
|
|
257
|
+
* - `"none"` — no auth (not recommended for production).
|
|
258
|
+
* - `MiddlewareHandler[]` — custom middleware stack (e.g., `[userAuth, requireRole("admin")]`).
|
|
259
|
+
*
|
|
260
|
+
* Default: `"none"`. You must explicitly configure auth.
|
|
261
|
+
*/
|
|
262
|
+
auth?: "userAuth" | "none" | import("hono").MiddlewareHandler<AppEnv>[];
|
|
263
|
+
/** Required roles for accessing job endpoints. Only works when auth includes userAuth. */
|
|
264
|
+
roles?: string[];
|
|
265
|
+
/** Whitelist of queue names exposed. Default: [] (nothing exposed). */
|
|
266
|
+
allowedQueues?: string[];
|
|
267
|
+
/** When using userAuth, restrict job visibility to the user who created it. Default: false. */
|
|
268
|
+
scopeToUser?: boolean;
|
|
269
|
+
}
|
|
270
|
+
export interface TenantConfig {
|
|
271
|
+
[key: string]: unknown;
|
|
272
|
+
}
|
|
273
|
+
export interface TenancyConfig {
|
|
274
|
+
/** How tenant is identified. */
|
|
275
|
+
resolution: "header" | "subdomain" | "path";
|
|
276
|
+
/** Header name when resolution is "header". Default: "x-tenant-id". */
|
|
277
|
+
headerName?: string;
|
|
278
|
+
/** Path segment index when resolution is "path". Default: 0. */
|
|
279
|
+
pathSegment?: number;
|
|
280
|
+
/** Callback to validate/load tenant. Return null to reject. */
|
|
281
|
+
onResolve?: (tenantId: string) => Promise<TenantConfig | null>;
|
|
282
|
+
/** TTL in ms for caching onResolve results (LRU cache). Default: 60_000. Set 0 to disable. */
|
|
283
|
+
cacheTtlMs?: number;
|
|
284
|
+
/** Max entries in tenant resolution cache. Default: 500. */
|
|
285
|
+
cacheMaxSize?: number;
|
|
286
|
+
/** Paths that skip tenant resolution. Uses startsWith matching. Default: ["/health", "/docs", "/openapi.json"]. */
|
|
287
|
+
exemptPaths?: string[];
|
|
288
|
+
/** HTTP status when onResolve returns null. Default: 403. */
|
|
289
|
+
rejectionStatus?: 403 | 404;
|
|
290
|
+
}
|
|
220
291
|
export interface CreateAppConfig {
|
|
221
292
|
/** Absolute path to the service's routes directory (use import.meta.dir + "/routes") */
|
|
222
293
|
routesDir: string;
|
|
@@ -237,5 +308,9 @@ export interface CreateAppConfig {
|
|
|
237
308
|
middleware?: MiddlewareHandler<AppEnv>[];
|
|
238
309
|
/** Database connection and store routing configuration */
|
|
239
310
|
db?: DbConfig;
|
|
311
|
+
/** Job status endpoint configuration. Requires BullMQ + Redis. */
|
|
312
|
+
jobs?: JobsConfig;
|
|
313
|
+
/** Multi-tenancy configuration. When set, tenant middleware resolves tenant on each request. */
|
|
314
|
+
tenancy?: TenancyConfig;
|
|
240
315
|
}
|
|
241
316
|
export declare const createApp: (config: CreateAppConfig) => Promise<OpenAPIHono<AppEnv>>;
|
package/dist/app.js
CHANGED
|
@@ -7,8 +7,8 @@ import { HttpError } from "./lib/HttpError";
|
|
|
7
7
|
import { rateLimit } from "./middleware/rateLimit";
|
|
8
8
|
import { bearerAuth } from "./middleware/bearerAuth";
|
|
9
9
|
import { identify } from "./middleware/identify";
|
|
10
|
-
import { HEADER_USER_TOKEN } from "./lib/constants";
|
|
11
|
-
import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive } from "./lib/appConfig";
|
|
10
|
+
import { HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN } from "./lib/constants";
|
|
11
|
+
import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig } from "./lib/appConfig";
|
|
12
12
|
import { setEmailVerificationStore } from "./lib/emailVerification";
|
|
13
13
|
import { setPasswordResetStore } from "./lib/resetPassword";
|
|
14
14
|
import { setAuthRateLimitStore } from "./lib/authRateLimit";
|
|
@@ -110,6 +110,8 @@ export const createApp = async (config) => {
|
|
|
110
110
|
setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
|
|
111
111
|
setIncludeInactiveSessions(sessionPolicy.includeInactiveSessions ?? false);
|
|
112
112
|
setTrackLastActive(sessionPolicy.trackLastActive ?? false);
|
|
113
|
+
setRefreshTokenConfig(authConfig.refreshTokens ?? null);
|
|
114
|
+
setMfaConfig(authConfig.mfa ?? null);
|
|
113
115
|
if (oauthProviders)
|
|
114
116
|
initOAuthProviders(oauthProviders);
|
|
115
117
|
const configuredOAuth = getConfiguredOAuthProviders();
|
|
@@ -125,7 +127,7 @@ export const createApp = async (config) => {
|
|
|
125
127
|
const app = new OpenAPIHono();
|
|
126
128
|
app.use(logger());
|
|
127
129
|
app.use(secureHeaders());
|
|
128
|
-
app.use(cors({ origin: corsOrigins, allowHeaders: ["Content-Type", "Authorization", HEADER_USER_TOKEN], exposeHeaders: ["x-cache"], credentials: true }));
|
|
130
|
+
app.use(cors({ origin: corsOrigins, allowHeaders: ["Content-Type", "Authorization", HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN], exposeHeaders: ["x-cache"], credentials: true }));
|
|
129
131
|
if ((botCfg.blockList?.length ?? 0) > 0) {
|
|
130
132
|
const { botProtection } = await import("./middleware/botProtection");
|
|
131
133
|
app.use(botProtection({ blockList: botCfg.blockList }));
|
|
@@ -141,6 +143,11 @@ export const createApp = async (config) => {
|
|
|
141
143
|
});
|
|
142
144
|
}
|
|
143
145
|
app.use(identify);
|
|
146
|
+
// Tenant resolution middleware (after identify, before user middleware + routes)
|
|
147
|
+
if (config.tenancy) {
|
|
148
|
+
const { createTenantMiddleware } = await import("./middleware/tenant");
|
|
149
|
+
app.use(createTenantMiddleware(config.tenancy));
|
|
150
|
+
}
|
|
144
151
|
for (const mw of middleware)
|
|
145
152
|
app.use(mw);
|
|
146
153
|
setAppName(appName);
|
|
@@ -188,17 +195,35 @@ export const createApp = async (config) => {
|
|
|
188
195
|
continue; // mounted separately below via createAuthRouter
|
|
189
196
|
if (file === "oauth.ts")
|
|
190
197
|
continue; // mounted separately below
|
|
198
|
+
if (file === "mfa.ts")
|
|
199
|
+
continue; // mounted separately below when mfa is configured
|
|
200
|
+
if (file === "jobs.ts")
|
|
201
|
+
continue; // mounted separately below when jobs.statusEndpoint is true
|
|
191
202
|
const mod = await import(`${coreRoutesDir}/${file}`);
|
|
192
203
|
if (mod.router)
|
|
193
204
|
app.route("/", mod.router);
|
|
194
205
|
}
|
|
195
206
|
if (enableAuthRoutes) {
|
|
196
207
|
const { createAuthRouter } = await import(`${coreRoutesDir}/auth`);
|
|
197
|
-
app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit }));
|
|
208
|
+
app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit, accountDeletion: authConfig.accountDeletion, refreshTokens: authConfig.refreshTokens }));
|
|
198
209
|
}
|
|
199
210
|
if (configuredOAuth.length > 0) {
|
|
200
211
|
app.route("/", createOAuthRouter(configuredOAuth, postOAuthRedirect));
|
|
201
212
|
}
|
|
213
|
+
if (authConfig.mfa && enableAuthRoutes) {
|
|
214
|
+
const { setMfaChallengeStore, setMfaChallengeSqliteDb } = await import("./lib/mfaChallenge");
|
|
215
|
+
setMfaChallengeStore(sessions);
|
|
216
|
+
if (sessions === "sqlite") {
|
|
217
|
+
const { getDb } = await import("./adapters/sqliteAuth");
|
|
218
|
+
setMfaChallengeSqliteDb(getDb());
|
|
219
|
+
}
|
|
220
|
+
const { createMfaRouter } = await import(`${coreRoutesDir}/mfa`);
|
|
221
|
+
app.route("/", createMfaRouter());
|
|
222
|
+
}
|
|
223
|
+
if (config.jobs?.statusEndpoint) {
|
|
224
|
+
const { createJobsRouter } = await import(`${coreRoutesDir}/jobs`);
|
|
225
|
+
app.route("/", createJobsRouter(config.jobs));
|
|
226
|
+
}
|
|
202
227
|
// Service routes — collect all, sort by optional exported `priority`, then mount
|
|
203
228
|
const serviceGlob = new Bun.Glob("**/*.ts");
|
|
204
229
|
const serviceFiles = [];
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createQueue, createWorker } from "../lib/queue";
|
|
2
|
-
export type { Job } from "../lib/queue";
|
|
1
|
+
export { createQueue, createWorker, createCronWorker, cleanupStaleSchedulers, getRegisteredCronNames, createDLQHandler } from "../lib/queue";
|
|
2
|
+
export type { Job, CronSchedule, DLQOptions } from "../lib/queue";
|