@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/example.ts CHANGED
@@ -3,11 +3,17 @@ import mongoose, {model, Schema} from "mongoose";
3
3
  import passportLocalMongoose from "passport-local-mongoose";
4
4
 
5
5
  import {type ModelRouterOptions, modelRouter} from "./api";
6
- import {addAuthRoutes, setupAuth} from "./auth";
6
+ import {addAuthRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
7
7
  import {setupServer} from "./expressServer";
8
8
  import {logger} from "./logger";
9
9
  import {Permissions} from "./permissions";
10
- import {baseUserPlugin, createdUpdatedPlugin} from "./plugins";
10
+ import {
11
+ baseUserPlugin,
12
+ createdUpdatedPlugin,
13
+ findExactlyOne,
14
+ findOneOrNone,
15
+ isDeletedPlugin,
16
+ } from "./plugins";
11
17
 
12
18
  mongoose
13
19
  .connect("mongodb://localhost:27017/example")
@@ -31,27 +37,43 @@ interface Food {
31
37
  hidden?: boolean;
32
38
  }
33
39
 
34
- const userSchema = new Schema<User>({
35
- admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
36
- username: {description: "The user's username", type: String},
37
- });
40
+ const userSchema = new Schema<User>(
41
+ {
42
+ admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
43
+ username: {description: "The user's username", type: String},
44
+ },
45
+ {strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
46
+ );
38
47
 
48
+ // biome-ignore lint/suspicious/noExplicitAny: passport-local-mongoose's plugin type is incompatible with mongoose Schema generics
39
49
  userSchema.plugin(passportLocalMongoose as any, {usernameField: "email"});
40
50
  userSchema.plugin(createdUpdatedPlugin);
41
51
  userSchema.plugin(baseUserPlugin);
42
52
  const UserModel = model<User>("User", userSchema);
43
53
 
44
- const schema = new Schema<Food>({
45
- calories: {description: "Number of calories in the food", type: Number},
46
- created: {description: "When this food was created", type: Date},
47
- hidden: {default: false, description: "Whether this food is hidden from listings", type: Boolean},
48
- name: {description: "The name of the food", type: String},
49
- ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
50
- });
54
+ const schema = new Schema<Food>(
55
+ {
56
+ calories: {description: "Number of calories in the food", type: Number},
57
+ created: {description: "When this food was created", type: Date},
58
+ hidden: {
59
+ default: false,
60
+ description: "Whether this food is hidden from listings",
61
+ type: Boolean,
62
+ },
63
+ name: {description: "The name of the food", type: String},
64
+ ownerId: {description: "The user who owns this food entry", ref: "User", type: "ObjectId"},
65
+ },
66
+ {strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
67
+ );
68
+
69
+ schema.plugin(createdUpdatedPlugin);
70
+ schema.plugin(isDeletedPlugin);
71
+ schema.plugin(findOneOrNone);
72
+ schema.plugin(findExactlyOne);
51
73
 
52
74
  const FoodModel = model<Food>("Food", schema);
53
75
 
54
- function getBaseServer() {
76
+ const getBaseServer = () => {
55
77
  const app = express();
56
78
 
57
79
  app.use((req, res, next) => {
@@ -65,14 +87,17 @@ function getBaseServer() {
65
87
  }
66
88
  });
67
89
  app.use(express.json());
68
- setupAuth(app, UserModel as any);
69
- addAuthRoutes(app, UserModel as any);
90
+ setupAuth(app, UserModel as unknown as UserMongooseModel);
91
+ addAuthRoutes(app, UserModel as unknown as UserMongooseModel);
70
92
 
71
- function addRoutes(router: express.Router, options?: Partial<ModelRouterOptions<any>>): void {
93
+ const addRoutes = (
94
+ router: express.Router,
95
+ options?: Partial<ModelRouterOptions<unknown>>
96
+ ): void => {
72
97
  router.use(
73
98
  "/food",
74
99
  modelRouter(FoodModel, {
75
- ...options,
100
+ ...(options as Partial<ModelRouterOptions<Food>>),
76
101
  openApiOverwrite: {
77
102
  get: {responses: {200: {description: "Get all the food"}}},
78
103
  },
@@ -86,14 +111,14 @@ function getBaseServer() {
86
111
  queryFields: ["name", "calories", "created", "ownerId", "hidden"],
87
112
  })
88
113
  );
89
- }
114
+ };
90
115
 
91
116
  return setupServer({
92
117
  addRoutes,
93
118
  loggingOptions: {
94
119
  level: "debug",
95
120
  },
96
- userModel: UserModel as any,
121
+ userModel: UserModel as unknown as UserMongooseModel,
97
122
  });
98
- }
123
+ };
99
124
  getBaseServer();
package/src/express.d.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  declare namespace Express {
2
2
  export interface Request {
3
- user?: {_id: string | ObjectId; id: string; admin: boolean};
3
+ authTokenPayload?: {
4
+ sid?: string;
5
+ sessionId?: string;
6
+ [key: string]: unknown;
7
+ };
8
+ jobId?: string;
9
+ requestId?: string;
10
+ sessionId?: string;
11
+ user?: {
12
+ _id: string | ObjectId;
13
+ id: string;
14
+ admin: boolean;
15
+ disabled?: boolean;
16
+ type?: string;
17
+ testUser?: boolean;
18
+ email?: string;
19
+ [key: string]: unknown;
20
+ };
4
21
  }
5
22
  }
@@ -1,6 +1,9 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
3
+ import {Writable} from "node:stream";
2
4
  import express from "express";
3
5
  import supertest from "supertest";
6
+ import winston from "winston";
4
7
 
5
8
  import {
6
9
  createRouter,
@@ -11,6 +14,7 @@ import {
11
14
  setupServer,
12
15
  wrapScript,
13
16
  } from "./expressServer";
17
+ import {logger, winstonLogger} from "./logger";
14
18
  import {UserModel} from "./tests";
15
19
 
16
20
  describe("expressServer", () => {
@@ -58,6 +62,52 @@ describe("expressServer", () => {
58
62
  });
59
63
 
60
64
  describe("logRequests", () => {
65
+ it("attaches request and session context to route logs", async () => {
66
+ const logs: string[] = [];
67
+ const logStream = new Writable({
68
+ write(chunk, _encoding, callback) {
69
+ logs.push(chunk.toString());
70
+ callback();
71
+ },
72
+ });
73
+ const transport = new winston.transports.Stream({
74
+ format: winston.format.json(),
75
+ stream: logStream,
76
+ });
77
+
78
+ const app = setupServer({
79
+ addRoutes: (router) => {
80
+ router.get("/context-test", (req, res) => {
81
+ logger.info("context route log");
82
+ return res.json({requestId: req.requestId, sessionId: req.sessionId});
83
+ });
84
+ },
85
+ logRequests: false,
86
+ skipListen: true,
87
+ userModel: UserModel as any,
88
+ });
89
+ winstonLogger.add(transport);
90
+
91
+ const res = await supertest(app)
92
+ .get("/context-test")
93
+ .set("X-Request-ID", "req-123")
94
+ .set("X-Session-ID", "session-123")
95
+ .expect(200);
96
+
97
+ expect(res.headers["x-request-id"]).toBe("req-123");
98
+ expect(res.headers["x-session-id"]).toBe("session-123");
99
+ expect(res.body).toEqual({requestId: "req-123", sessionId: "session-123"});
100
+
101
+ const parsedLog = logs
102
+ .map((entry) => JSON.parse(entry))
103
+ .find((entry) => entry.message === "context route log");
104
+ expect(parsedLog).toBeDefined();
105
+ expect(parsedLog.requestId).toBe("req-123");
106
+ expect(parsedLog.sessionId).toBe("session-123");
107
+
108
+ winstonLogger.remove(transport);
109
+ });
110
+
61
111
  it("logs request with admin user type", () => {
62
112
  const req = {
63
113
  body: {},
@@ -653,7 +703,6 @@ describe("expressServer", () => {
653
703
  const timerIds: ReturnType<typeof setTimeout>[] = [];
654
704
 
655
705
  beforeEach(() => {
656
- // biome-ignore lint/suspicious/noExplicitAny: Mock requires type override for process.exit.
657
706
  process.exit = mock(() => {
658
707
  throw new Error("process.exit called");
659
708
  }) as unknown as typeof process.exit;
@@ -750,7 +799,6 @@ describe("expressServer", () => {
750
799
  setupServer({
751
800
  addRoutes,
752
801
  skipListen: true,
753
- // biome-ignore lint/suspicious/noExplicitAny: Test mock for UserModel.
754
802
  userModel: UserModel as any,
755
803
  })
756
804
  ).toThrow("route initialization failed");
@@ -9,19 +9,28 @@ import passport from "passport";
9
9
  import qs from "qs";
10
10
  import type {ModelRouterOptions} from "./api";
11
11
  import {addAuthRoutes, addMeRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
12
- import {apiErrorMiddleware, apiUnauthorizedMiddleware} from "./errors";
12
+ import {
13
+ apiErrorMiddleware,
14
+ apiFallthroughErrorMiddleware,
15
+ apiUnauthorizedMiddleware,
16
+ } from "./errors";
13
17
  import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
14
18
  import {type LoggingOptions, logger, setupLogging} from "./logger";
15
19
  import {sendToSlack} from "./notifiers";
16
20
  import {openApiCompatMiddleware, patchAppUse} from "./openApiCompat";
17
21
  import {openApiEtagMiddleware} from "./openApiEtag";
22
+ import {
23
+ getCurrentRequestContext,
24
+ requestContextMiddleware,
25
+ updateRequestContextFromRequest,
26
+ } from "./requestContext";
18
27
  import openapi from "./vendor/wesleytodd-openapi/index";
19
28
 
20
29
  const SLOW_READ_MAX = 200;
21
30
  const SLOW_WRITE_MAX = 500;
22
31
  const IS_JEST = process.env.JEST_WORKER_ID !== undefined;
23
32
 
24
- export function setupEnvironment(): void {
33
+ export const setupEnvironment = (): void => {
25
34
  if (!process.env.TOKEN_ISSUER) {
26
35
  throw new Error("TOKEN_ISSUER must be set in env.");
27
36
  }
@@ -40,12 +49,13 @@ export function setupEnvironment(): void {
40
49
  if (!process.env.REFRESH_TOKEN_EXPIRES_IN && !IS_JEST) {
41
50
  logger.warn("REFRESH_TOKEN_EXPIRES_IN not set so using default.");
42
51
  }
43
- }
52
+ };
44
53
 
45
54
  export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<unknown>>) => void;
46
55
 
56
+ // biome-ignore lint/suspicious/noExplicitAny: also called from tests with mock request/response objects
47
57
  const logRequestsFinished = (req: any, res: any, startTime: bigint) => {
48
- const options = (res.locals.loggingOptions ?? {}) as LoggingOptions;
58
+ const options = (res.locals?.loggingOptions ?? {}) as LoggingOptions;
49
59
 
50
60
  const slowReadMs = options.logSlowRequestsReadMs ?? SLOW_READ_MAX;
51
61
  const slowWriteMs = options.logSlowRequestsWriteMs ?? SLOW_WRITE_MAX;
@@ -84,7 +94,8 @@ const logRequestsFinished = (req: any, res: any, startTime: bigint) => {
84
94
  }
85
95
  };
86
96
 
87
- export function logRequests(req: any, res: any, next: any) {
97
+ // biome-ignore lint/suspicious/noExplicitAny: also called from tests with mock request/response objects
98
+ export const logRequests = (req: any, res: any, next: express.NextFunction): void => {
88
99
  const startTime = process.hrtime.bigint();
89
100
 
90
101
  let userString = "";
@@ -114,37 +125,48 @@ export function logRequests(req: any, res: any, next: any) {
114
125
  }
115
126
  onFinished(res, () => logRequestsFinished(req, res, startTime));
116
127
  next();
117
- }
128
+ };
118
129
 
119
- export function createRouter(rootPath: string, addRoutes: AddRoutes, middleware: any[] = []) {
120
- function routePathMiddleware(req: any, _res: any, next: any) {
130
+ export const createRouter = (
131
+ rootPath: string,
132
+ addRoutes: AddRoutes,
133
+ middleware: express.RequestHandler[] = []
134
+ ): Array<string | express.RequestHandler | Router> => {
135
+ const routePathMiddleware = (
136
+ req: express.Request & {routeMount?: string[]},
137
+ _res: express.Response,
138
+ next: express.NextFunction
139
+ ): void => {
121
140
  if (!req.routeMount) {
122
141
  req.routeMount = [];
123
142
  }
124
143
  req.routeMount.push(rootPath);
125
144
  next();
126
- }
145
+ };
127
146
 
128
147
  const router = express.Router();
129
148
  router.use(routePathMiddleware);
130
149
  addRoutes(router);
131
150
  return [rootPath, ...middleware, router];
132
- }
151
+ };
133
152
 
134
- export function createRouterWithAuth(
153
+ export const createRouterWithAuth = (
135
154
  rootPath: string,
136
155
  addRoutes: (router: Router) => void,
137
- middleware: any[] = []
138
- ) {
156
+ middleware: express.RequestHandler[] = []
157
+ ): Array<string | express.RequestHandler | Router> => {
139
158
  return createRouter(rootPath, addRoutes, [
140
159
  passport.authenticate("firebase-jwt", {session: false}),
141
160
  ...middleware,
142
161
  ]);
143
- }
162
+ };
144
163
 
145
164
  export interface AuthOptions {
146
- generateJWTPayload?: (user: any) => Record<string, any>;
165
+ // biome-ignore lint/suspicious/noExplicitAny: user shape is provided by the consumer's User model — any preserves the loose-binding contract
166
+ generateJWTPayload?: (user: any) => Record<string, unknown>;
167
+ // biome-ignore lint/suspicious/noExplicitAny: see above
147
168
  generateTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
169
+ // biome-ignore lint/suspicious/noExplicitAny: see above
148
170
  generateRefreshTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
149
171
  }
150
172
 
@@ -173,19 +195,20 @@ interface InitializeRoutesOptions {
173
195
  githubAuth?: GitHubAuthOptions;
174
196
  }
175
197
 
176
- function initializeRoutes(
198
+ const initializeRoutes = (
177
199
  UserModel: UserMongooseModel,
178
200
  addRoutes: AddRoutes,
179
201
  options: InitializeRoutesOptions = {}
180
- ): express.Application {
202
+ ): express.Application => {
181
203
  const app = express();
182
204
 
183
205
  // Record mount paths on layers for Express 5 → OpenAPI compat
184
206
  patchAppUse(app);
185
207
 
186
- // TODO: Log a warning when we hit the array limit.
187
208
  app.set("query parser", (str: string) => qs.parse(str, {arrayLimit: options.arrayLimit ?? 200}));
188
209
 
210
+ app.use(requestContextMiddleware);
211
+
189
212
  app.use(
190
213
  cors({
191
214
  origin: options.corsOrigin ?? "*",
@@ -199,8 +222,12 @@ function initializeRoutes(
199
222
  app.use(express.json({limit: "50mb"}));
200
223
 
201
224
  // Add login/signup/refresh_token before the JWT/auth middlewares
202
- addAuthRoutes(app, UserModel as any, options?.authOptions);
203
- setupAuth(app as any, UserModel as any);
225
+ addAuthRoutes(app, UserModel, options?.authOptions);
226
+ setupAuth(app, UserModel);
227
+ app.use((req, res, next) => {
228
+ updateRequestContextFromRequest(req, res);
229
+ next();
230
+ });
204
231
 
205
232
  if (options.logRequests !== false) {
206
233
  app.use(logRequests);
@@ -213,9 +240,13 @@ function initializeRoutes(
213
240
  });
214
241
 
215
242
  // Add Sentry scopes for session, transaction, and userId if any are set
216
- app.use((req: any, _res: any, next: any) => {
243
+ app.use((req: express.Request, _res: express.Response, next: express.NextFunction) => {
244
+ const context = getCurrentRequestContext();
217
245
  const transactionId = req.header("X-Transaction-ID");
218
- const sessionId = req.header("X-Session-ID");
246
+ const sessionId = context?.sessionId ?? req.header("X-Session-ID");
247
+ if (context?.requestId) {
248
+ Sentry.getCurrentScope().setTag("request_id", context.requestId);
249
+ }
219
250
  if (transactionId) {
220
251
  Sentry.getCurrentScope().setTag("transaction_id", transactionId);
221
252
  }
@@ -223,7 +254,7 @@ function initializeRoutes(
223
254
  Sentry.getCurrentScope().setTag("session_id", sessionId);
224
255
  }
225
256
  if (req.user?._id) {
226
- Sentry.getCurrentScope().setTag("user", req.user._id);
257
+ Sentry.getCurrentScope().setTag("user", String(req.user._id));
227
258
  }
228
259
  next();
229
260
  });
@@ -246,12 +277,12 @@ function initializeRoutes(
246
277
  app.use("/swagger", oapi.swaggerui());
247
278
  }
248
279
 
249
- addMeRoutes(app, UserModel as any, options?.authOptions);
280
+ addMeRoutes(app, UserModel, options?.authOptions);
250
281
 
251
282
  // Set up GitHub OAuth if configured (works with JWT auth)
252
283
  if (options.githubAuth) {
253
- setupGitHubAuth(app, UserModel as any, options.githubAuth);
254
- addGitHubAuthRoutes(app, UserModel as any, options.githubAuth, options.authOptions);
284
+ setupGitHubAuth(app, UserModel, options.githubAuth);
285
+ addGitHubAuthRoutes(app, UserModel, options.githubAuth, options.authOptions);
255
286
  }
256
287
 
257
288
  addRoutes(app, {openApi: oapi});
@@ -262,20 +293,17 @@ function initializeRoutes(
262
293
  app.use(apiUnauthorizedMiddleware);
263
294
  app.use(apiErrorMiddleware);
264
295
 
265
- app.use(function onError(err: any, _req: any, res: any, _next: any) {
266
- logger.error(`Fallthrough error: ${err}${err?.stack ? `\n${err.stack}` : ""}}`);
267
- Sentry.captureException(err);
268
- res.statusCode = 500;
269
- res.end(`${res.sentry}\n`);
270
- });
296
+ app.use(apiFallthroughErrorMiddleware);
271
297
 
272
298
  return app;
273
- }
299
+ };
274
300
 
275
301
  export interface SetupServerOptions {
276
302
  userModel: UserMongooseModel;
277
303
  addRoutes: AddRoutes;
278
304
  loggingOptions?: LoggingOptions;
305
+ // Whether requests should be logged. Defaults to true.
306
+ logRequests?: boolean;
279
307
  authOptions?: AuthOptions;
280
308
  /**
281
309
  * GitHub OAuth configuration. When provided, enables GitHub authentication.
@@ -300,8 +328,7 @@ export interface SetupServerOptions {
300
328
  sentryOptions?: Sentry.BunOptions;
301
329
  }
302
330
 
303
- // Sets up the routes and returns the app.
304
- export function setupServer(options: SetupServerOptions): express.Application {
331
+ export const setupServer = (options: SetupServerOptions): express.Application => {
305
332
  const UserModel = options.userModel;
306
333
  const addRoutes = options.addRoutes;
307
334
 
@@ -314,9 +341,12 @@ export function setupServer(options: SetupServerOptions): express.Application {
314
341
  authOptions: options.authOptions,
315
342
  corsOrigin: options.corsOrigin,
316
343
  githubAuth: options.githubAuth,
344
+ loggingOptions: options.loggingOptions,
345
+ logRequests: options.logRequests,
317
346
  });
318
- } catch (error: any) {
319
- logger.error(`Error initializing routes: ${error.stack}`);
347
+ } catch (error: unknown) {
348
+ const stack = error instanceof Error && error.stack ? error.stack : String(error);
349
+ logger.error(`Error initializing routes: ${stack}`);
320
350
  throw error;
321
351
  }
322
352
 
@@ -327,19 +357,19 @@ export function setupServer(options: SetupServerOptions): express.Application {
327
357
  logger.info(`Listening on port ${port}`);
328
358
  });
329
359
  } catch (error) {
330
- logger.error(`Error trying to start HTTP server: ${error}\n${(error as any).stack}`);
360
+ const stack = error instanceof Error ? error.stack : String(error);
361
+ logger.error(`Error trying to start HTTP server: ${error}\n${stack}`);
331
362
  process.exit(1);
332
363
  }
333
364
  }
334
365
  return app;
335
- }
366
+ };
336
367
 
337
- // Convenience method to execute cronjobs with an always-running server.
338
- export function cronjob(
368
+ export const cronjob = (
339
369
  name: string,
340
370
  schedule: "hourly" | "minutely" | string,
341
371
  callback: () => void
342
- ) {
372
+ ): void => {
343
373
  const cronSchedule =
344
374
  schedule === "hourly" ? "0 * * * *" : schedule === "minutely" ? "* * * * *" : schedule;
345
375
  logger.info(`Adding cronjob ${name}, running at: ${cronSchedule}`);
@@ -348,16 +378,17 @@ export function cronjob(
348
378
  } catch (error) {
349
379
  throw new Error(`Failed to create cronjob: ${error}`);
350
380
  }
351
- }
381
+ };
352
382
 
353
383
  export interface WrapScriptOptions {
354
- onFinish?: (result?: any) => void | Promise<void>;
384
+ onFinish?: (result?: unknown) => void | Promise<void>;
355
385
  terminateTimeout?: number; // in seconds, defaults to 300. Set to 0 to have no termination timeout.
356
386
  slackChannel?: string;
357
387
  }
358
- // Wrap up a script with some helpers, such as catching errors, reporting them to sentry, notifying
359
- // Slack of runs, etc. Also supports timeouts.
360
- export async function wrapScript(func: () => Promise<any>, options: WrapScriptOptions = {}) {
388
+ export const wrapScript = async (
389
+ func: () => Promise<unknown>,
390
+ options: WrapScriptOptions = {}
391
+ ): Promise<void> => {
361
392
  const name = require.main?.filename.split("/").slice(-1)[0].replace(".ts", "");
362
393
  logger.info(`Running script ${name}`);
363
394
  await sendToSlack(`Running script ${name}`, {
@@ -383,7 +414,7 @@ export async function wrapScript(func: () => Promise<any>, options: WrapScriptOp
383
414
  }, closeTime);
384
415
  }
385
416
 
386
- let result: any;
417
+ let result: unknown;
387
418
  try {
388
419
  result = await func();
389
420
  if (options.onFinish) {
@@ -397,6 +428,5 @@ export async function wrapScript(func: () => Promise<any>, options: WrapScriptOp
397
428
  process.exit(1);
398
429
  }
399
430
  await sendToSlack(`Success running script ${name}: ${result}`);
400
- // Unclear why we have to exit here to prevent the script for continuing to run.
401
431
  process.exit(0);
402
- }
432
+ };
@@ -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 mongoose, {model, Schema} from "mongoose";