@terreno/api 0.13.2 → 0.14.0

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 (175) hide show
  1. package/dist/__tests__/versionCheckPlugin.test.js +53 -3
  2. package/dist/api.arrayOperations.test.js +1 -0
  3. package/dist/api.asyncHandler.test.d.ts +1 -0
  4. package/dist/api.asyncHandler.test.js +236 -0
  5. package/dist/api.d.ts +15 -4
  6. package/dist/api.errors.test.js +1 -0
  7. package/dist/api.hooks.test.js +1 -0
  8. package/dist/api.js +153 -104
  9. package/dist/api.query.test.js +1 -0
  10. package/dist/api.test.js +174 -0
  11. package/dist/auth.d.ts +10 -5
  12. package/dist/auth.js +163 -90
  13. package/dist/auth.test.js +159 -0
  14. package/dist/betterAuthApp.test.js +1 -0
  15. package/dist/betterAuthSetup.d.ts +5 -6
  16. package/dist/betterAuthSetup.js +17 -14
  17. package/dist/betterAuthSetup.test.js +1 -0
  18. package/dist/config.d.ts +48 -0
  19. package/dist/config.js +248 -0
  20. package/dist/config.test.d.ts +1 -0
  21. package/dist/config.test.js +328 -0
  22. package/dist/configuration.test.js +1 -0
  23. package/dist/configurationApp.d.ts +1 -1
  24. package/dist/configurationApp.js +17 -13
  25. package/dist/configurationPlugin.test.js +1 -0
  26. package/dist/consentApp.test.js +1 -0
  27. package/dist/envConfigurationPlugin.d.ts +2 -0
  28. package/dist/envConfigurationPlugin.js +173 -0
  29. package/dist/envConfigurationPlugin.test.d.ts +1 -0
  30. package/dist/envConfigurationPlugin.test.js +322 -0
  31. package/dist/errors.d.ts +18 -7
  32. package/dist/errors.js +106 -10
  33. package/dist/errors.test.js +16 -1
  34. package/dist/example.js +16 -7
  35. package/dist/expressServer.d.ts +10 -9
  36. package/dist/expressServer.js +62 -53
  37. package/dist/expressServer.test.js +53 -2
  38. package/dist/githubAuth.d.ts +2 -1
  39. package/dist/githubAuth.js +41 -26
  40. package/dist/githubAuth.test.js +1 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +4 -0
  43. package/dist/logger.d.ts +1 -1
  44. package/dist/logger.js +42 -20
  45. package/dist/models/versionConfig.d.ts +2 -0
  46. package/dist/models/versionConfig.js +8 -0
  47. package/dist/notifiers/googleChatNotifier.js +14 -16
  48. package/dist/notifiers/googleChatNotifier.test.js +1 -0
  49. package/dist/notifiers/slackNotifier.js +16 -14
  50. package/dist/notifiers/slackNotifier.test.js +41 -3
  51. package/dist/notifiers/zoomNotifier.js +7 -10
  52. package/dist/notifiers/zoomNotifier.test.js +1 -0
  53. package/dist/openApi.d.ts +1 -1
  54. package/dist/openApi.test.js +1 -0
  55. package/dist/openApiBuilder.d.ts +39 -6
  56. package/dist/openApiBuilder.js +1 -31
  57. package/dist/openApiBuilder.test.js +1 -0
  58. package/dist/openApiValidator.js +1 -0
  59. package/dist/openApiValidator.test.js +65 -0
  60. package/dist/permissions.d.ts +4 -4
  61. package/dist/permissions.js +67 -65
  62. package/dist/permissions.middleware.test.js +1 -0
  63. package/dist/permissions.test.js +1 -0
  64. package/dist/plugins.d.ts +5 -5
  65. package/dist/plugins.js +18 -9
  66. package/dist/plugins.test.js +1 -1
  67. package/dist/populate.d.ts +15 -8
  68. package/dist/populate.js +23 -24
  69. package/dist/populate.test.js +1 -0
  70. package/dist/realtime/changeStreamWatcher.d.ts +73 -0
  71. package/dist/realtime/changeStreamWatcher.js +720 -0
  72. package/dist/realtime/index.d.ts +6 -0
  73. package/dist/realtime/index.js +27 -0
  74. package/dist/realtime/queryMatcher.d.ts +14 -0
  75. package/dist/realtime/queryMatcher.js +250 -0
  76. package/dist/realtime/queryStore.d.ts +37 -0
  77. package/dist/realtime/queryStore.js +195 -0
  78. package/dist/realtime/realtime.test.d.ts +10 -0
  79. package/dist/realtime/realtime.test.js +2158 -0
  80. package/dist/realtime/realtimeApp.d.ts +93 -0
  81. package/dist/realtime/realtimeApp.js +560 -0
  82. package/dist/realtime/registry.d.ts +40 -0
  83. package/dist/realtime/registry.js +38 -0
  84. package/dist/realtime/socketUser.d.ts +10 -0
  85. package/dist/realtime/socketUser.js +17 -0
  86. package/dist/realtime/types.d.ts +100 -0
  87. package/dist/realtime/types.js +2 -0
  88. package/dist/requestContext.d.ts +37 -0
  89. package/dist/requestContext.js +344 -0
  90. package/dist/requestContext.test.d.ts +1 -0
  91. package/dist/requestContext.test.js +241 -0
  92. package/dist/terrenoApp.d.ts +8 -0
  93. package/dist/terrenoApp.js +50 -13
  94. package/dist/terrenoApp.test.js +194 -21
  95. package/dist/terrenoPlugin.d.ts +11 -0
  96. package/dist/tests/bunSetup.js +1 -0
  97. package/dist/tests.js +1 -1
  98. package/dist/transformers.d.ts +2 -2
  99. package/dist/transformers.js +5 -3
  100. package/dist/transformers.test.js +90 -0
  101. package/dist/types/consentResponse.d.ts +6 -3
  102. package/dist/versionCheckPlugin.d.ts +2 -0
  103. package/dist/versionCheckPlugin.js +18 -12
  104. package/package.json +4 -2
  105. package/src/__tests__/versionCheckPlugin.test.ts +37 -3
  106. package/src/api.arrayOperations.test.ts +1 -0
  107. package/src/api.asyncHandler.test.ts +177 -0
  108. package/src/api.errors.test.ts +1 -0
  109. package/src/api.hooks.test.ts +1 -0
  110. package/src/api.query.test.ts +1 -0
  111. package/src/api.test.ts +132 -0
  112. package/src/api.ts +199 -84
  113. package/src/auth.test.ts +160 -0
  114. package/src/auth.ts +120 -50
  115. package/src/betterAuthApp.test.ts +1 -0
  116. package/src/betterAuthSetup.test.ts +1 -0
  117. package/src/betterAuthSetup.ts +46 -19
  118. package/src/config.test.ts +255 -0
  119. package/src/config.ts +206 -0
  120. package/src/configuration.test.ts +1 -0
  121. package/src/configurationApp.ts +59 -24
  122. package/src/configurationPlugin.test.ts +1 -0
  123. package/src/consentApp.test.ts +1 -0
  124. package/src/envConfigurationPlugin.test.ts +143 -0
  125. package/src/envConfigurationPlugin.ts +100 -0
  126. package/src/errors.test.ts +19 -1
  127. package/src/errors.ts +94 -20
  128. package/src/example.ts +46 -21
  129. package/src/express.d.ts +18 -1
  130. package/src/expressServer.test.ts +50 -2
  131. package/src/expressServer.ts +80 -50
  132. package/src/githubAuth.test.ts +1 -0
  133. package/src/githubAuth.ts +59 -38
  134. package/src/index.ts +4 -0
  135. package/src/logger.ts +47 -17
  136. package/src/models/versionConfig.ts +13 -2
  137. package/src/notifiers/googleChatNotifier.test.ts +1 -0
  138. package/src/notifiers/googleChatNotifier.ts +7 -9
  139. package/src/notifiers/slackNotifier.test.ts +29 -3
  140. package/src/notifiers/slackNotifier.ts +9 -7
  141. package/src/notifiers/zoomNotifier.test.ts +1 -0
  142. package/src/notifiers/zoomNotifier.ts +8 -11
  143. package/src/openApi.test.ts +1 -0
  144. package/src/openApi.ts +4 -4
  145. package/src/openApiBuilder.test.ts +1 -0
  146. package/src/openApiBuilder.ts +14 -11
  147. package/src/openApiValidator.test.ts +59 -0
  148. package/src/openApiValidator.ts +3 -2
  149. package/src/permissions.middleware.test.ts +1 -0
  150. package/src/permissions.test.ts +1 -0
  151. package/src/permissions.ts +30 -25
  152. package/src/plugins.test.ts +1 -1
  153. package/src/plugins.ts +21 -14
  154. package/src/populate.test.ts +1 -0
  155. package/src/populate.ts +44 -36
  156. package/src/realtime/changeStreamWatcher.ts +568 -0
  157. package/src/realtime/index.ts +34 -0
  158. package/src/realtime/queryMatcher.ts +179 -0
  159. package/src/realtime/queryStore.ts +132 -0
  160. package/src/realtime/realtime.test.ts +1755 -0
  161. package/src/realtime/realtimeApp.ts +478 -0
  162. package/src/realtime/registry.ts +64 -0
  163. package/src/realtime/socketUser.ts +25 -0
  164. package/src/realtime/types.ts +112 -0
  165. package/src/requestContext.test.ts +196 -0
  166. package/src/requestContext.ts +368 -0
  167. package/src/terrenoApp.test.ts +137 -11
  168. package/src/terrenoApp.ts +64 -17
  169. package/src/terrenoPlugin.ts +12 -0
  170. package/src/tests/bunSetup.ts +1 -0
  171. package/src/tests.ts +7 -2
  172. package/src/transformers.test.ts +70 -2
  173. package/src/transformers.ts +15 -7
  174. package/src/types/consentResponse.ts +8 -10
  175. package/src/versionCheckPlugin.ts +15 -7
package/src/auth.test.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterEach, beforeEach, describe, expect, it, setSystemTime} from "bun:test";
2
3
  import type express from "express";
3
4
  import type jwt from "jsonwebtoken";
@@ -8,13 +9,26 @@ import {modelRouter} from "./api";
8
9
  import {addAuthRoutes, addMeRoutes, generateTokens, setupAuth} from "./auth";
9
10
  import {setupServer} from "./expressServer";
10
11
  import {Permissions} from "./permissions";
12
+ import {getCurrentRequestContext} from "./requestContext";
11
13
  import {type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
12
14
  import {AdminOwnerTransformer} from "./transformers";
13
15
  import {timeout} from "./utils";
14
16
 
17
+ const decodeTokenPayload = <T extends Record<string, unknown>>(token: string): T => {
18
+ const encodedPayload = token.split(".")[1];
19
+ return JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")) as T;
20
+ };
21
+
15
22
  describe("auth tests", () => {
16
23
  let app: express.Application;
17
24
  let admin: any;
25
+ let contextEvents: Array<{
26
+ currentSessionId?: string;
27
+ requestId?: string;
28
+ sessionId?: string;
29
+ stage: string;
30
+ userId?: string;
31
+ }>;
18
32
  let notAdmin: any;
19
33
  let agent: TestAgent;
20
34
 
@@ -23,6 +37,7 @@ describe("auth tests", () => {
23
37
  // lockout mechanism needs real time to progress
24
38
  setSystemTime();
25
39
  [admin, notAdmin] = await setupDb();
40
+ contextEvents = [];
26
41
 
27
42
  await Promise.all([
28
43
  FoodModel.create({
@@ -76,6 +91,65 @@ describe("auth tests", () => {
76
91
  }),
77
92
  })
78
93
  );
94
+ router.use(
95
+ "/context-food",
96
+ modelRouter(FoodModel, {
97
+ permissions: {
98
+ create: [Permissions.IsAuthenticated],
99
+ delete: [],
100
+ list: [],
101
+ read: [],
102
+ update: [],
103
+ },
104
+ postCreate: async (_value, req) => {
105
+ contextEvents.push({
106
+ currentSessionId: getCurrentRequestContext()?.sessionId,
107
+ requestId: req.requestId,
108
+ sessionId: req.sessionId,
109
+ stage: "postCreate",
110
+ userId: req.user?.id,
111
+ });
112
+ },
113
+ preCreate: (body, req) => {
114
+ contextEvents.push({
115
+ currentSessionId: getCurrentRequestContext()?.sessionId,
116
+ requestId: req.requestId,
117
+ sessionId: req.sessionId,
118
+ stage: "preCreate",
119
+ userId: req.user?.id,
120
+ });
121
+
122
+ return {
123
+ ...(body as Partial<Food>),
124
+ categories: [],
125
+ eatenBy: [req.user?._id],
126
+ expiration: "2026-01-01",
127
+ lastEatenWith: {},
128
+ likesIds: [],
129
+ ownerId: req.user?._id,
130
+ source: {name: "context-test"},
131
+ tags: [],
132
+ } as unknown as Food;
133
+ },
134
+ responseHandler: async (value, method, req) => {
135
+ contextEvents.push({
136
+ currentSessionId: getCurrentRequestContext()?.sessionId,
137
+ requestId: req.requestId,
138
+ sessionId: req.sessionId,
139
+ stage: `responseHandler:${method}`,
140
+ userId: req.user?.id,
141
+ });
142
+
143
+ return {
144
+ id: String((value as {_id: unknown})._id),
145
+ requestId: req.requestId ?? null,
146
+ sessionContext: getCurrentRequestContext()?.sessionId ?? null,
147
+ sessionId: req.sessionId ?? null,
148
+ userId: req.user?.id ?? null,
149
+ };
150
+ },
151
+ })
152
+ );
79
153
  }
80
154
  app = setupServer({
81
155
  addRoutes,
@@ -201,6 +275,92 @@ describe("auth tests", () => {
201
275
  expect(res.body.data.token).toBeDefined();
202
276
  });
203
277
 
278
+ it("passes request and session context through modelRouter hooks", async () => {
279
+ const loginRes = await agent
280
+ .post("/auth/login")
281
+ .send({email: "admin@example.com", password: "securePassword"})
282
+ .expect(200);
283
+ const loginTokenPayload = decodeTokenPayload<{sid?: string}>(loginRes.body.data.token);
284
+
285
+ const createRes = await agent
286
+ .post("/context-food")
287
+ .set("authorization", `Bearer ${loginRes.body.data.token}`)
288
+ .set("X-Request-ID", "model-router-request-1")
289
+ .send({calories: 10, name: "Context Apple"})
290
+ .expect(201);
291
+
292
+ expect(loginTokenPayload.sid).toBeDefined();
293
+ const sessionId = loginTokenPayload.sid;
294
+ if (!sessionId) {
295
+ throw new Error("Expected login token to include a session id");
296
+ }
297
+ expect(createRes.headers["x-request-id"]).toBe("model-router-request-1");
298
+ expect(createRes.headers["x-session-id"]).toBe(sessionId);
299
+ expect(createRes.body.data.requestId).toBe("model-router-request-1");
300
+ expect(createRes.body.data.sessionId).toBe(sessionId);
301
+ expect(createRes.body.data.sessionContext).toBe(sessionId);
302
+ expect(createRes.body.data.userId).toBe(String(admin._id));
303
+ expect(contextEvents).toEqual([
304
+ {
305
+ currentSessionId: sessionId,
306
+ requestId: "model-router-request-1",
307
+ sessionId,
308
+ stage: "preCreate",
309
+ userId: String(admin._id),
310
+ },
311
+ {
312
+ currentSessionId: sessionId,
313
+ requestId: "model-router-request-1",
314
+ sessionId,
315
+ stage: "postCreate",
316
+ userId: String(admin._id),
317
+ },
318
+ {
319
+ currentSessionId: sessionId,
320
+ requestId: "model-router-request-1",
321
+ sessionId,
322
+ stage: "responseHandler:create",
323
+ userId: String(admin._id),
324
+ },
325
+ ]);
326
+ });
327
+
328
+ it("preserves JWT session id across refresh and request context", async () => {
329
+ const loginRes = await agent
330
+ .post("/auth/login")
331
+ .send({email: "admin@example.com", password: "securePassword"})
332
+ .expect(200);
333
+ const loginTokenPayload = decodeTokenPayload<{sid?: string}>(loginRes.body.data.token);
334
+ const loginRefreshPayload = decodeTokenPayload<{sid?: string}>(loginRes.body.data.refreshToken);
335
+
336
+ expect(loginTokenPayload.sid).toBeDefined();
337
+ const loginSessionId = loginTokenPayload.sid;
338
+ if (!loginSessionId) {
339
+ throw new Error("Expected login token to include a session id");
340
+ }
341
+ expect(loginRefreshPayload.sid).toBe(loginSessionId);
342
+ expect(loginRes.headers["x-session-id"]).toBe(loginSessionId);
343
+
344
+ const refreshRes = await agent
345
+ .post("/auth/refresh_token")
346
+ .send({refreshToken: loginRes.body.data.refreshToken})
347
+ .expect(200);
348
+ const refreshedTokenPayload = decodeTokenPayload<{sid?: string}>(refreshRes.body.data.token);
349
+ const refreshedRefreshPayload = decodeTokenPayload<{sid?: string}>(
350
+ refreshRes.body.data.refreshToken
351
+ );
352
+
353
+ expect(refreshedTokenPayload.sid).toBe(loginSessionId);
354
+ expect(refreshedRefreshPayload.sid).toBe(loginSessionId);
355
+ expect(refreshRes.headers["x-session-id"]).toBe(loginSessionId);
356
+
357
+ const foodRes = await agent
358
+ .get("/food")
359
+ .set("authorization", `Bearer ${refreshRes.body.data.token}`)
360
+ .expect(200);
361
+ expect(foodRes.headers["x-session-id"]).toBe(loginSessionId);
362
+ });
363
+
204
364
  it("completes token login e2e", async () => {
205
365
  const res = await agent
206
366
  .post("/auth/login")
package/src/auth.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import {randomUUID} from "node:crypto";
1
2
  import express from "express";
2
3
  import jwt, {type JwtPayload} from "jsonwebtoken";
3
4
  import type {Model, ObjectId} from "mongoose";
@@ -11,9 +12,15 @@ import {
11
12
  } from "passport-jwt";
12
13
  import {Strategy as LocalStrategy} from "passport-local";
13
14
 
14
- import {APIError, apiErrorMiddleware} from "./errors";
15
+ import {APIError, apiErrorMiddleware, errorMessage} from "./errors";
15
16
  import type {AuthOptions} from "./expressServer";
16
17
  import {logger} from "./logger";
18
+ import {
19
+ getSessionIdFromJwtPayload,
20
+ type JwtSessionPayload,
21
+ setRequestContext,
22
+ updateRequestContextFromRequest,
23
+ } from "./requestContext";
17
24
 
18
25
  export interface User {
19
26
  _id: ObjectId | string;
@@ -31,14 +38,22 @@ export interface User {
31
38
  export interface UserModel extends Model<User> {
32
39
  createAnonymousUser?: (id?: string) => Promise<User>;
33
40
  // Allows additional setup during signup. This will be passed the rest of req.body from the signup
34
- postCreate?: (body: any) => Promise<void>;
41
+ postCreate?: (body: Record<string, unknown>) => Promise<void>;
35
42
 
43
+ // biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
36
44
  createStrategy(): any;
45
+ // biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
37
46
  serializeUser(): any;
47
+ // biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
38
48
  deserializeUser(): any;
49
+ // biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose return types are untyped
39
50
  findByUsername(username: string, findOpts: any): any;
40
51
  }
41
52
 
53
+ export interface GenerateTokensOptions {
54
+ sessionId?: string;
55
+ }
56
+
42
57
  export function authenticateMiddleware(anonymous = false) {
43
58
  const strategies = ["jwt"];
44
59
  if (anonymous) {
@@ -49,7 +64,7 @@ export function authenticateMiddleware(anonymous = false) {
49
64
  failWithError: true,
50
65
  session: false,
51
66
  });
52
- return (req: any, res: any, next: any) => {
67
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
53
68
  if (req.user) {
54
69
  return next();
55
70
  }
@@ -61,27 +76,29 @@ export async function signupUser(
61
76
  userModel: UserModel,
62
77
  email: string,
63
78
  password: string,
64
- body?: any
79
+ body?: Record<string, unknown>
65
80
  ) {
66
81
  // Strip email and password from the body. They can cause mongoose to throw an error if strict is
67
82
  // set.
68
- const {email: _email, password: _password, ...bodyRest} = body;
83
+ const {email: _email, password: _password, ...bodyRest} = body ?? {};
69
84
 
70
85
  try {
86
+ // biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose's register() is untyped
71
87
  const user = await (userModel as any).register({email, ...bodyRest}, password);
72
88
 
73
89
  if (user.postCreate) {
74
90
  try {
75
91
  await user.postCreate(bodyRest);
76
- } catch (error: any) {
92
+ } catch (error: unknown) {
77
93
  logger.error(`Error in user.postCreate: ${error}`);
78
94
  throw error;
79
95
  }
80
96
  }
81
97
  await user.save();
82
98
  return user;
83
- } catch (error: any) {
84
- throw new APIError({title: error.message});
99
+ } catch (error: unknown) {
100
+ const message = errorMessage(error);
101
+ throw new APIError({title: message});
85
102
  }
86
103
  }
87
104
 
@@ -102,16 +119,22 @@ export async function signupUser(
102
119
  * authentication providers) to reuse and customize the same token generation logic.
103
120
  * This ensures consistent and secure token issuance across different authentication flows.
104
121
  */
105
- export const generateTokens = async (user: any, authOptions?: AuthOptions) => {
122
+ export const generateTokens = async (
123
+ user: unknown,
124
+ authOptions?: AuthOptions,
125
+ options: GenerateTokensOptions = {}
126
+ ) => {
106
127
  const tokenSecretOrKey = process.env.TOKEN_SECRET;
107
128
  if (!tokenSecretOrKey) {
108
129
  throw new Error("TOKEN_SECRET must be set in env.");
109
130
  }
110
- if (!user?._id) {
131
+ const tokenUser = user as {_id?: ObjectId | string} | null | undefined;
132
+ if (!tokenUser?._id) {
111
133
  logger.warn("No user found for token generation");
112
134
  return {refreshToken: null, token: null};
113
135
  }
114
- let payload: Record<string, any> = {id: user._id.toString()};
136
+ const sessionId = options.sessionId ?? randomUUID();
137
+ let payload: Record<string, unknown> = {id: String(tokenUser._id), sid: sessionId};
115
138
  if (authOptions?.generateJWTPayload) {
116
139
  payload = {...authOptions.generateJWTPayload(user), ...payload};
117
140
  }
@@ -137,7 +160,7 @@ export const generateTokens = async (user: any, authOptions?: AuthOptions) => {
137
160
 
138
161
  const token = jwt.sign(payload, tokenSecretOrKey, tokenOptions);
139
162
  const refreshTokenSecretOrKey = process.env.REFRESH_TOKEN_SECRET;
140
- let refreshToken;
163
+ let refreshToken: string | undefined;
141
164
  if (refreshTokenSecretOrKey) {
142
165
  const refreshTokenOptions: jwt.SignOptions = {
143
166
  expiresIn: "30d",
@@ -159,7 +182,7 @@ export const generateTokens = async (user: any, authOptions?: AuthOptions) => {
159
182
  } else {
160
183
  logger.info("REFRESH_TOKEN_SECRET not set so refresh tokens will not be issued");
161
184
  }
162
- return {refreshToken, token};
185
+ return {refreshToken, sessionId, token};
163
186
  };
164
187
 
165
188
  // TODO allow customization
@@ -215,7 +238,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
215
238
  passport.use(
216
239
  "jwt",
217
240
  new JwtStrategy(jwtOpts, async (jwtPayload: JwtPayload, done) => {
218
- let user;
241
+ let user: User | null = null;
219
242
  if (!jwtPayload) {
220
243
  return done(null, false);
221
244
  }
@@ -241,7 +264,11 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
241
264
 
242
265
  // Adds req.user to the request. This may wind up duplicating requests with passport,
243
266
  // but passport doesn't give us req.user early enough.
244
- async function decodeJWTMiddleware(req, res, next) {
267
+ async function decodeJWTMiddleware(
268
+ req: express.Request,
269
+ res: express.Response,
270
+ next: express.NextFunction
271
+ ) {
245
272
  if (!process.env.TOKEN_SECRET) {
246
273
  return next();
247
274
  }
@@ -260,21 +287,34 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
260
287
  return next();
261
288
  }
262
289
 
263
- let decoded;
290
+ let decoded: jwt.JwtPayload | undefined;
264
291
 
265
292
  try {
266
293
  decoded = jwt.verify(token, process.env.TOKEN_SECRET, {
267
294
  issuer: process.env.TOKEN_ISSUER,
268
295
  }) as jwt.JwtPayload;
269
- } catch (error: any) {
296
+ } catch (error: unknown) {
270
297
  const userText = req.user?._id ? ` for user ${req.user._id} ` : "";
271
- const details = `[jwt] Error decoding token${userText}: ${error}, expired at ${error?.expiredAt}, current time: ${Date.now()}`;
298
+ const expiredAt =
299
+ error && typeof error === "object" && "expiredAt" in error
300
+ ? (error as {expiredAt?: unknown}).expiredAt
301
+ : undefined;
302
+ const message = errorMessage(error);
303
+ const details = `[jwt] Error decoding token${userText}: ${error}, expired at ${expiredAt}, current time: ${Date.now()}`;
272
304
  logger.debug(details);
273
- return res.status(401).json({details, message: error?.message});
305
+ return res.status(401).json({details, message});
274
306
  }
275
- if (decoded.id) {
307
+ if (decoded?.id) {
308
+ const sessionId = getSessionIdFromJwtPayload(decoded as JwtSessionPayload);
309
+ req.authTokenPayload = decoded as JwtSessionPayload;
310
+ if (sessionId) {
311
+ req.sessionId = sessionId;
312
+ setRequestContext({sessionId});
313
+ }
276
314
  try {
277
- req.user = await userModel.findById(decoded.id);
315
+ const user = await userModel.findById(decoded.id);
316
+ req.user = user as unknown as express.Request["user"];
317
+ updateRequestContextFromRequest(req, res);
278
318
  if (req.user?.disabled) {
279
319
  logger.warn(`[jwt] User ${req.user.id} is disabled`);
280
320
  return res.status(401).json({status: 401, title: "User is disabled"});
@@ -286,6 +326,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
286
326
  return next();
287
327
  }
288
328
  app.use(decodeJWTMiddleware);
329
+ // biome-ignore lint/suspicious/noExplicitAny: express 5 type for urlencoded doesn't match RequestHandler
289
330
  app.use(express.urlencoded({extended: false}) as any);
290
331
  }
291
332
 
@@ -296,23 +337,35 @@ export function addAuthRoutes(
296
337
  ): void {
297
338
  const router = express.Router();
298
339
  router.post("/login", async (req, res, next) => {
299
- passport.authenticate("local", {session: false}, async (err: any, user: any, info: any) => {
300
- if (err) {
301
- logger.error(`Error logging in: ${err}`);
302
- return next(err);
303
- }
304
- if (!user) {
305
- logger.warn(`Invalid login: ${info}`);
306
- return res.status(401).json({message: info?.message});
307
- }
308
- if (process.env.NODE_ENV !== "test") {
309
- logger.info(`User logged in: ${user._id}, type: ${(user as any).type || "N/A"}`);
340
+ passport.authenticate(
341
+ "local",
342
+ {session: false},
343
+ async (
344
+ err: Error | null,
345
+ user: (User & {type?: string}) | false | null,
346
+ info: {message?: string} | undefined
347
+ ) => {
348
+ if (err) {
349
+ logger.error(`Error logging in: ${err}`);
350
+ return next(err);
351
+ }
352
+ if (!user) {
353
+ logger.warn(`Invalid login: ${info}`);
354
+ return res.status(401).json({message: info?.message});
355
+ }
356
+ if (process.env.NODE_ENV !== "test") {
357
+ logger.info(`User logged in: ${user._id}, type: ${user.type || "N/A"}`);
358
+ }
359
+ const tokens = await generateTokens(user, authOptions);
360
+ if (tokens.sessionId) {
361
+ setRequestContext({sessionId: tokens.sessionId, userId: String(user._id)});
362
+ res.setHeader("X-Session-ID", tokens.sessionId);
363
+ }
364
+ return res.json({
365
+ data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: user?._id},
366
+ });
310
367
  }
311
- const tokens = await generateTokens(user, authOptions);
312
- return res.json({
313
- data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: user?._id},
314
- });
315
- })(req, res, next);
368
+ )(req, res, next);
316
369
  });
317
370
 
318
371
  router.post("/refresh_token", async (req, res) => {
@@ -329,16 +382,25 @@ export function addAuthRoutes(
329
382
  return res.status(401).json({message: "No REFRESH_TOKEN_SECRET set, cannot refresh token"});
330
383
  }
331
384
  const refreshTokenSecretOrKey = process.env.REFRESH_TOKEN_SECRET;
332
- let decoded;
385
+ let decoded: JwtPayload;
333
386
  try {
334
387
  decoded = jwt.verify(req.body.refreshToken, refreshTokenSecretOrKey) as JwtPayload;
335
- } catch (error: any) {
388
+ } catch (error: unknown) {
336
389
  logger.error(`Error refreshing token for user ${req.user?.id}: ${error}`);
337
- return res.status(401).json({message: error?.message});
390
+ const message = errorMessage(error);
391
+ return res.status(401).json({message});
338
392
  }
339
393
  if (decoded?.id) {
340
394
  const user = await userModel.findById(decoded.id);
341
- const tokens = await generateTokens(user, authOptions);
395
+ const sessionId = getSessionIdFromJwtPayload(decoded as JwtSessionPayload);
396
+ const tokens = await generateTokens(user, authOptions, {sessionId});
397
+ if (tokens.sessionId) {
398
+ setRequestContext({
399
+ sessionId: tokens.sessionId,
400
+ userId: user?._id ? String(user._id) : undefined,
401
+ });
402
+ res.setHeader("X-Session-ID", tokens.sessionId);
403
+ }
342
404
  logger.debug(`Refreshed token for ${user?.id}`);
343
405
  return res.json({data: {refreshToken: tokens.refreshToken, token: tokens.token}});
344
406
  }
@@ -351,10 +413,17 @@ export function addAuthRoutes(
351
413
  router.post(
352
414
  "/signup",
353
415
  passport.authenticate("signup", {failWithError: true, session: false}),
354
- async (req: any, res: any) => {
416
+ async (req: express.Request, res: express.Response) => {
355
417
  const tokens = await generateTokens(req.user, authOptions);
418
+ if (tokens.sessionId) {
419
+ setRequestContext({
420
+ sessionId: tokens.sessionId,
421
+ userId: req.user?._id ? String(req.user._id) : undefined,
422
+ });
423
+ res.setHeader("X-Session-ID", tokens.sessionId);
424
+ }
356
425
  return res.json({
357
- data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: req.user._id},
426
+ data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: req.user?._id},
358
427
  });
359
428
  }
360
429
  );
@@ -379,8 +448,8 @@ export function addMeRoutes(
379
448
  logger.debug("Not user data found for /me");
380
449
  return res.sendStatus(404);
381
450
  }
382
- const dataObject = data.toObject();
383
- (dataObject as any).id = data._id;
451
+ const dataObject = data.toObject() as unknown as Record<string, unknown>;
452
+ dataObject.id = data._id;
384
453
  return res.json({data: dataObject});
385
454
  });
386
455
 
@@ -396,17 +465,18 @@ export function addMeRoutes(
396
465
  // try {
397
466
  // body = transform(req.body, "update", req.user);
398
467
  // } catch (e) {
399
- // return res.status(403).send({message: (e as any).message});
468
+ // return res.status(403).send({message: (e as Error).message});
400
469
  // }
401
470
  try {
402
471
  Object.assign(doc, req.body);
403
472
  await doc.save();
404
473
 
405
- const dataObject = doc.toObject();
406
- (dataObject as any).id = doc._id;
474
+ const dataObject = doc.toObject() as unknown as Record<string, unknown>;
475
+ dataObject.id = doc._id;
407
476
  return res.json({data: dataObject});
408
- } catch (error) {
409
- return res.status(403).send({message: (error as any).message});
477
+ } catch (error: unknown) {
478
+ const message = errorMessage(error);
479
+ return res.status(403).send({message});
410
480
  }
411
481
  });
412
482
 
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterAll, afterEach, describe, expect, it} from "bun:test";
2
3
  import express from "express";
3
4
  import {MongoMemoryServer} from "mongodb-memory-server";
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterAll, afterEach, describe, expect, it, mock} from "bun:test";
2
3
  import express from "express";
3
4
  import {MongoMemoryServer} from "mongodb-memory-server";