@terreno/api 0.13.3 → 0.14.1

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 (172) hide show
  1. package/dist/__tests__/versionCheckPlugin.test.js +136 -3
  2. package/dist/api.arrayOperations.test.js +1 -0
  3. package/dist/api.d.ts +15 -4
  4. package/dist/api.errors.test.js +1 -0
  5. package/dist/api.hooks.test.js +1 -0
  6. package/dist/api.js +153 -104
  7. package/dist/api.query.test.js +1 -0
  8. package/dist/api.test.js +174 -0
  9. package/dist/auth.d.ts +10 -5
  10. package/dist/auth.js +163 -90
  11. package/dist/auth.test.js +159 -0
  12. package/dist/betterAuthApp.test.js +1 -0
  13. package/dist/betterAuthSetup.d.ts +5 -6
  14. package/dist/betterAuthSetup.js +30 -17
  15. package/dist/betterAuthSetup.test.js +1 -0
  16. package/dist/config.d.ts +48 -0
  17. package/dist/config.js +257 -0
  18. package/dist/config.test.d.ts +1 -0
  19. package/dist/config.test.js +328 -0
  20. package/dist/configuration.test.js +1 -0
  21. package/dist/configurationApp.d.ts +1 -1
  22. package/dist/configurationApp.js +17 -13
  23. package/dist/configurationPlugin.test.js +1 -0
  24. package/dist/consentApp.test.js +1 -0
  25. package/dist/envConfigurationPlugin.d.ts +2 -0
  26. package/dist/envConfigurationPlugin.js +173 -0
  27. package/dist/envConfigurationPlugin.test.d.ts +1 -0
  28. package/dist/envConfigurationPlugin.test.js +322 -0
  29. package/dist/errors.d.ts +18 -7
  30. package/dist/errors.js +111 -12
  31. package/dist/errors.test.js +16 -1
  32. package/dist/example.js +19 -7
  33. package/dist/expressServer.d.ts +10 -9
  34. package/dist/expressServer.js +62 -53
  35. package/dist/expressServer.test.js +165 -2
  36. package/dist/githubAuth.d.ts +2 -1
  37. package/dist/githubAuth.js +41 -26
  38. package/dist/githubAuth.test.js +1 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +4 -0
  41. package/dist/logger.d.ts +1 -1
  42. package/dist/logger.js +42 -20
  43. package/dist/models/versionConfig.d.ts +2 -0
  44. package/dist/models/versionConfig.js +8 -0
  45. package/dist/notifiers/googleChatNotifier.js +14 -16
  46. package/dist/notifiers/googleChatNotifier.test.js +1 -0
  47. package/dist/notifiers/slackNotifier.js +16 -14
  48. package/dist/notifiers/slackNotifier.test.js +41 -3
  49. package/dist/notifiers/zoomNotifier.js +7 -10
  50. package/dist/notifiers/zoomNotifier.test.js +1 -0
  51. package/dist/openApi.d.ts +1 -1
  52. package/dist/openApi.test.js +1 -0
  53. package/dist/openApiBuilder.d.ts +39 -6
  54. package/dist/openApiBuilder.js +1 -31
  55. package/dist/openApiBuilder.test.js +1 -0
  56. package/dist/openApiValidator.js +1 -0
  57. package/dist/openApiValidator.test.js +1 -0
  58. package/dist/permissions.d.ts +4 -4
  59. package/dist/permissions.js +67 -65
  60. package/dist/permissions.middleware.test.js +1 -0
  61. package/dist/permissions.test.js +1 -0
  62. package/dist/plugins.d.ts +5 -5
  63. package/dist/plugins.js +18 -9
  64. package/dist/plugins.test.js +1 -1
  65. package/dist/populate.d.ts +15 -8
  66. package/dist/populate.js +23 -24
  67. package/dist/populate.test.js +1 -0
  68. package/dist/realtime/changeStreamWatcher.d.ts +73 -0
  69. package/dist/realtime/changeStreamWatcher.js +724 -0
  70. package/dist/realtime/index.d.ts +6 -0
  71. package/dist/realtime/index.js +27 -0
  72. package/dist/realtime/queryMatcher.d.ts +14 -0
  73. package/dist/realtime/queryMatcher.js +250 -0
  74. package/dist/realtime/queryStore.d.ts +37 -0
  75. package/dist/realtime/queryStore.js +195 -0
  76. package/dist/realtime/realtime.test.d.ts +10 -0
  77. package/dist/realtime/realtime.test.js +3066 -0
  78. package/dist/realtime/realtimeApp.d.ts +93 -0
  79. package/dist/realtime/realtimeApp.js +560 -0
  80. package/dist/realtime/registry.d.ts +40 -0
  81. package/dist/realtime/registry.js +38 -0
  82. package/dist/realtime/socketUser.d.ts +10 -0
  83. package/dist/realtime/socketUser.js +17 -0
  84. package/dist/realtime/types.d.ts +100 -0
  85. package/dist/realtime/types.js +2 -0
  86. package/dist/requestContext.d.ts +37 -0
  87. package/dist/requestContext.js +344 -0
  88. package/dist/requestContext.test.d.ts +1 -0
  89. package/dist/requestContext.test.js +384 -0
  90. package/dist/terrenoApp.d.ts +8 -0
  91. package/dist/terrenoApp.js +50 -13
  92. package/dist/terrenoApp.test.js +194 -21
  93. package/dist/terrenoPlugin.d.ts +11 -0
  94. package/dist/tests/bunSetup.js +1 -0
  95. package/dist/tests.js +1 -1
  96. package/dist/transformers.d.ts +2 -2
  97. package/dist/transformers.js +5 -3
  98. package/dist/transformers.test.js +90 -0
  99. package/dist/types/consentResponse.d.ts +6 -3
  100. package/dist/versionCheckPlugin.d.ts +2 -0
  101. package/dist/versionCheckPlugin.js +18 -12
  102. package/package.json +4 -2
  103. package/src/__tests__/versionCheckPlugin.test.ts +94 -3
  104. package/src/api.arrayOperations.test.ts +1 -0
  105. package/src/api.errors.test.ts +1 -0
  106. package/src/api.hooks.test.ts +1 -0
  107. package/src/api.query.test.ts +1 -0
  108. package/src/api.test.ts +132 -0
  109. package/src/api.ts +199 -84
  110. package/src/auth.test.ts +160 -0
  111. package/src/auth.ts +120 -50
  112. package/src/betterAuthApp.test.ts +1 -0
  113. package/src/betterAuthSetup.test.ts +1 -0
  114. package/src/betterAuthSetup.ts +59 -22
  115. package/src/config.test.ts +255 -0
  116. package/src/config.ts +216 -0
  117. package/src/configuration.test.ts +1 -0
  118. package/src/configurationApp.ts +59 -24
  119. package/src/configurationPlugin.test.ts +1 -0
  120. package/src/consentApp.test.ts +1 -0
  121. package/src/envConfigurationPlugin.test.ts +143 -0
  122. package/src/envConfigurationPlugin.ts +100 -0
  123. package/src/errors.test.ts +19 -1
  124. package/src/errors.ts +118 -38
  125. package/src/example.ts +49 -21
  126. package/src/express.d.ts +18 -1
  127. package/src/expressServer.test.ts +147 -2
  128. package/src/expressServer.ts +80 -50
  129. package/src/githubAuth.test.ts +1 -0
  130. package/src/githubAuth.ts +59 -38
  131. package/src/index.ts +4 -0
  132. package/src/logger.ts +47 -17
  133. package/src/models/versionConfig.ts +13 -2
  134. package/src/notifiers/googleChatNotifier.test.ts +1 -0
  135. package/src/notifiers/googleChatNotifier.ts +7 -9
  136. package/src/notifiers/slackNotifier.test.ts +29 -3
  137. package/src/notifiers/slackNotifier.ts +9 -7
  138. package/src/notifiers/zoomNotifier.test.ts +1 -0
  139. package/src/notifiers/zoomNotifier.ts +8 -11
  140. package/src/openApi.test.ts +1 -0
  141. package/src/openApi.ts +4 -4
  142. package/src/openApiBuilder.test.ts +1 -0
  143. package/src/openApiBuilder.ts +14 -11
  144. package/src/openApiValidator.test.ts +1 -0
  145. package/src/openApiValidator.ts +3 -2
  146. package/src/permissions.middleware.test.ts +1 -0
  147. package/src/permissions.test.ts +1 -0
  148. package/src/permissions.ts +30 -25
  149. package/src/plugins.test.ts +1 -1
  150. package/src/plugins.ts +21 -14
  151. package/src/populate.test.ts +1 -0
  152. package/src/populate.ts +44 -36
  153. package/src/realtime/changeStreamWatcher.ts +572 -0
  154. package/src/realtime/index.ts +34 -0
  155. package/src/realtime/queryMatcher.ts +179 -0
  156. package/src/realtime/queryStore.ts +132 -0
  157. package/src/realtime/realtime.test.ts +2465 -0
  158. package/src/realtime/realtimeApp.ts +478 -0
  159. package/src/realtime/registry.ts +64 -0
  160. package/src/realtime/socketUser.ts +25 -0
  161. package/src/realtime/types.ts +112 -0
  162. package/src/requestContext.test.ts +321 -0
  163. package/src/requestContext.ts +368 -0
  164. package/src/terrenoApp.test.ts +137 -11
  165. package/src/terrenoApp.ts +64 -17
  166. package/src/terrenoPlugin.ts +12 -0
  167. package/src/tests/bunSetup.ts +1 -0
  168. package/src/tests.ts +7 -2
  169. package/src/transformers.test.ts +70 -2
  170. package/src/transformers.ts +15 -7
  171. package/src/types/consentResponse.ts +8 -10
  172. package/src/versionCheckPlugin.ts +15 -7
@@ -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";
package/src/githubAuth.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type express from "express";
2
2
  import {Router} from "express";
3
+ import type {Schema} from "mongoose";
3
4
  import passport from "passport";
4
5
  import {Strategy as GitHubStrategy, type Profile} from "passport-github2";
5
- import {generateTokens, type UserModel} from "./auth";
6
+ import {generateTokens, type User, type UserModel} from "./auth";
6
7
  import {APIError} from "./errors";
7
8
  import type {AuthOptions} from "./expressServer";
8
9
  import {logger} from "./logger";
@@ -32,7 +33,9 @@ export interface GitHubAuthOptions {
32
33
  profile: Profile,
33
34
  accessToken: string,
34
35
  refreshToken: string,
36
+ // biome-ignore lint/suspicious/noExplicitAny: user shape varies per consumer's User model
35
37
  existingUser?: any
38
+ // biome-ignore lint/suspicious/noExplicitAny: passport user value remains untyped
36
39
  ) => Promise<any>;
37
40
  }
38
41
 
@@ -59,12 +62,19 @@ export interface GitHubUserFields {
59
62
  * userSchema.plugin(githubUserPlugin);
60
63
  * ```
61
64
  */
62
- export const githubUserPlugin = (schema: any): void => {
65
+ // biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
66
+ export const githubUserPlugin = (schema: Schema<any, any, any, any>): void => {
63
67
  schema.add({
64
- githubAvatarUrl: {type: String},
65
- githubId: {index: true, sparse: true, type: String, unique: true},
66
- githubProfileUrl: {type: String},
67
- githubUsername: {type: String},
68
+ githubAvatarUrl: {description: "GitHub avatar image URL", type: String},
69
+ githubId: {
70
+ description: "GitHub user ID",
71
+ index: true,
72
+ sparse: true,
73
+ type: String,
74
+ unique: true,
75
+ },
76
+ githubProfileUrl: {description: "GitHub profile URL", type: String},
77
+ githubUsername: {description: "GitHub username", type: String},
68
78
  });
69
79
  };
70
80
 
@@ -81,6 +91,7 @@ export const setupGitHubAuth = (
81
91
 
82
92
  passport.use(
83
93
  "github",
94
+ // biome-ignore lint/suspicious/noExplicitAny: passport-github2's typed constructor overloads don't match passReqToCallback variant
84
95
  new (GitHubStrategy as any)(
85
96
  {
86
97
  callbackURL: githubOptions.callbackURL,
@@ -89,12 +100,12 @@ export const setupGitHubAuth = (
89
100
  passReqToCallback: true,
90
101
  scope,
91
102
  },
92
- (async (
93
- req: any,
103
+ async (
104
+ req: express.Request,
94
105
  accessToken: string,
95
106
  refreshToken: string,
96
107
  profile: Profile,
97
- done: (error: any, user?: any) => void
108
+ done: (error: unknown, user?: unknown) => void
98
109
  ) => {
99
110
  try {
100
111
  const existingUser = req.user;
@@ -137,10 +148,11 @@ export const setupGitHubAuth = (
137
148
  // Link GitHub to existing user
138
149
  const user = await userModel.findById(existingUser._id);
139
150
  if (user) {
140
- (user as any).githubId = githubId;
141
- (user as any).githubUsername = profile.username;
142
- (user as any).githubProfileUrl = profile.profileUrl;
143
- (user as any).githubAvatarUrl = profile.photos?.[0]?.value;
151
+ const userWithGitHub = user as unknown as GitHubUserFields;
152
+ userWithGitHub.githubId = githubId;
153
+ userWithGitHub.githubUsername = profile.username;
154
+ userWithGitHub.githubProfileUrl = profile.profileUrl;
155
+ userWithGitHub.githubAvatarUrl = profile.photos?.[0]?.value;
144
156
  await user.save();
145
157
  return done(null, user);
146
158
  }
@@ -161,10 +173,11 @@ export const setupGitHubAuth = (
161
173
  if (existingEmailUser) {
162
174
  // If account linking is allowed, link GitHub to existing email account
163
175
  if (githubOptions.allowAccountLinking !== false) {
164
- (existingEmailUser as any).githubId = githubId;
165
- (existingEmailUser as any).githubUsername = profile.username;
166
- (existingEmailUser as any).githubProfileUrl = profile.profileUrl;
167
- (existingEmailUser as any).githubAvatarUrl = profile.photos?.[0]?.value;
176
+ const emailUserWithGitHub = existingEmailUser as unknown as GitHubUserFields;
177
+ emailUserWithGitHub.githubId = githubId;
178
+ emailUserWithGitHub.githubUsername = profile.username;
179
+ emailUserWithGitHub.githubProfileUrl = profile.profileUrl;
180
+ emailUserWithGitHub.githubAvatarUrl = profile.photos?.[0]?.value;
168
181
  await existingEmailUser.save();
169
182
  return done(null, existingEmailUser);
170
183
  }
@@ -186,7 +199,7 @@ export const setupGitHubAuth = (
186
199
  githubId,
187
200
  githubProfileUrl: profile.profileUrl,
188
201
  githubUsername: profile.username,
189
- } as any);
202
+ } as unknown as Partial<User>);
190
203
 
191
204
  await newUser.save();
192
205
  return done(null, newUser);
@@ -194,7 +207,7 @@ export const setupGitHubAuth = (
194
207
  logger.error(`GitHub auth error: ${error}`);
195
208
  return done(error);
196
209
  }
197
- }) as any
210
+ }
198
211
  ) as passport.Strategy
199
212
  );
200
213
  };
@@ -223,8 +236,9 @@ export const addGitHubAuthRoutes = (
223
236
  // Store the return URL in session or query for redirect after auth
224
237
  const returnTo = req.query.returnTo as string | undefined;
225
238
  if (returnTo) {
226
- (req as any).session = (req as any).session || {};
227
- (req as any).session.returnTo = returnTo;
239
+ const reqWithSession = req as express.Request & {session?: {returnTo?: string}};
240
+ reqWithSession.session = reqWithSession.session ?? {};
241
+ reqWithSession.session.returnTo = returnTo;
228
242
  }
229
243
  next();
230
244
  },
@@ -241,16 +255,17 @@ export const addGitHubAuthRoutes = (
241
255
  async (req: express.Request, res: express.Response) => {
242
256
  try {
243
257
  const tokens = await generateTokens(req.user, authOptions);
244
- const returnTo = (req as any).session?.returnTo;
258
+ const returnTo = (req as express.Request & {session?: {returnTo?: string}}).session
259
+ ?.returnTo;
245
260
 
246
261
  // If there's a return URL, redirect with tokens as query params
247
262
  if (returnTo) {
248
263
  const url = new URL(returnTo);
249
- url.searchParams.set("token", tokens.token || "");
264
+ url.searchParams.set("token", tokens.token ?? "");
250
265
  if (tokens.refreshToken) {
251
266
  url.searchParams.set("refreshToken", tokens.refreshToken);
252
267
  }
253
- url.searchParams.set("userId", (req.user as any)?._id?.toString() || "");
268
+ url.searchParams.set("userId", req.user?._id ? String(req.user._id) : "");
254
269
  return res.redirect(url.toString());
255
270
  }
256
271
 
@@ -259,7 +274,7 @@ export const addGitHubAuthRoutes = (
259
274
  data: {
260
275
  refreshToken: tokens.refreshToken,
261
276
  token: tokens.token,
262
- userId: (req.user as any)?._id,
277
+ userId: req.user?._id,
263
278
  },
264
279
  });
265
280
  } catch (error) {
@@ -280,14 +295,18 @@ export const addGitHubAuthRoutes = (
280
295
  "/github/link",
281
296
  (req: express.Request, res: express.Response, next: express.NextFunction): void => {
282
297
  // Require JWT authentication for linking
283
- passport.authenticate("jwt", {session: false}, (err: any, user: any) => {
284
- if (err || !user) {
285
- res.status(401).json({message: "Authentication required to link GitHub account"});
286
- return;
298
+ passport.authenticate(
299
+ "jwt",
300
+ {session: false},
301
+ (err: unknown, user: User | false | null) => {
302
+ if (err || !user) {
303
+ res.status(401).json({message: "Authentication required to link GitHub account"});
304
+ return;
305
+ }
306
+ req.user = user as unknown as express.Request["user"];
307
+ next();
287
308
  }
288
- req.user = user;
289
- next();
290
- })(req, res, next);
309
+ )(req, res, next);
291
310
  },
292
311
  passport.authenticate("github", {session: false})
293
312
  );
@@ -303,14 +322,15 @@ export const addGitHubAuthRoutes = (
303
322
 
304
323
  try {
305
324
  // Explicitly select hash and salt fields which may be hidden by default
306
- const user = await userModel.findById((req.user as any)._id).select("+hash +salt");
325
+ const user = await userModel.findById(req.user._id).select("+hash +salt");
307
326
  if (!user) {
308
327
  return res.status(404).json({message: "User not found"});
309
328
  }
310
329
 
311
330
  // Check if user has other authentication methods before unlinking
312
331
  // passport-local-mongoose stores password in hash and salt fields
313
- const hasPassword = !!(user as any).hash || !!(user as any).salt;
332
+ const userWithAuth = user as unknown as {hash?: string; salt?: string};
333
+ const hasPassword = !!userWithAuth.hash || !!userWithAuth.salt;
314
334
  if (!hasPassword) {
315
335
  return res.status(400).json({
316
336
  message:
@@ -318,10 +338,11 @@ export const addGitHubAuthRoutes = (
318
338
  });
319
339
  }
320
340
 
321
- (user as any).githubId = undefined;
322
- (user as any).githubUsername = undefined;
323
- (user as any).githubProfileUrl = undefined;
324
- (user as any).githubAvatarUrl = undefined;
341
+ const userWithGitHub = user as unknown as GitHubUserFields;
342
+ userWithGitHub.githubId = undefined;
343
+ userWithGitHub.githubUsername = undefined;
344
+ userWithGitHub.githubProfileUrl = undefined;
345
+ userWithGitHub.githubAvatarUrl = undefined;
325
346
  await user.save();
326
347
 
327
348
  return res.json({data: {message: "GitHub account unlinked successfully"}});
package/src/index.ts CHANGED
@@ -3,9 +3,11 @@ export * from "./auth";
3
3
  export * from "./betterAuth";
4
4
  export * from "./betterAuthApp";
5
5
  export * from "./betterAuthSetup";
6
+ export * from "./config";
6
7
  export * from "./configurationApp";
7
8
  export * from "./configurationPlugin";
8
9
  export * from "./consentApp";
10
+ export * from "./envConfigurationPlugin";
9
11
  export * from "./errors";
10
12
  export * from "./expressServer";
11
13
  export * from "./githubAuth";
@@ -22,6 +24,8 @@ export * from "./openApiValidator";
22
24
  export * from "./permissions";
23
25
  export * from "./plugins";
24
26
  export * from "./populate";
27
+ export * from "./realtime";
28
+ export * from "./requestContext";
25
29
  export * from "./scriptRunner";
26
30
  export * from "./secretProviders";
27
31
  export * from "./syncConsents";