@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.
- package/dist/__tests__/versionCheckPlugin.test.js +136 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +30 -17
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +257 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +111 -12
- package/dist/errors.test.js +16 -1
- package/dist/example.js +19 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +165 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +1 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +724 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +3066 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +384 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +94 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +59 -22
- package/src/config.test.ts +255 -0
- package/src/config.ts +216 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +118 -38
- package/src/example.ts +49 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +147 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +1 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +572 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +2465 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +321 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
package/src/expressServer.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
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
|
|
153
|
+
export const createRouterWithAuth = (
|
|
135
154
|
rootPath: string,
|
|
136
155
|
addRoutes: (router: Router) => void,
|
|
137
|
-
middleware:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
203
|
-
setupAuth(app
|
|
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:
|
|
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
|
|
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
|
|
254
|
-
addGitHubAuthRoutes(app, UserModel
|
|
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(
|
|
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
|
-
|
|
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:
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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:
|
|
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
|
+
};
|
package/src/githubAuth.test.ts
CHANGED
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
|
-
|
|
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: {
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
93
|
-
req:
|
|
103
|
+
async (
|
|
104
|
+
req: express.Request,
|
|
94
105
|
accessToken: string,
|
|
95
106
|
refreshToken: string,
|
|
96
107
|
profile: Profile,
|
|
97
|
-
done: (error:
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
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",
|
|
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:
|
|
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(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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";
|