@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.
- package/dist/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.asyncHandler.test.d.ts +1 -0
- package/dist/api.asyncHandler.test.js +236 -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 +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -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 +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -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 +65 -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 +720 -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 +2158 -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 +241 -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 +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.asyncHandler.test.ts +177 -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 +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -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 +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -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 +59 -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 +568 -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 +1755 -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 +196 -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/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";
|
package/src/logger.ts
CHANGED
|
@@ -2,38 +2,62 @@ import fs from "node:fs";
|
|
|
2
2
|
import {inspect} from "node:util";
|
|
3
3
|
import * as Sentry from "@sentry/bun";
|
|
4
4
|
import winston from "winston";
|
|
5
|
+
import {getCurrentLogContext} from "./requestContext";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const isPrimitive = (val: unknown) => {
|
|
7
8
|
return val === null || (typeof val !== "object" && typeof val !== "function");
|
|
8
|
-
}
|
|
9
|
+
};
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const formatWithInspect = (val: unknown) => {
|
|
11
12
|
const prefix = isPrimitive(val) ? "" : "\n";
|
|
12
13
|
const shouldFormat = typeof val !== "string";
|
|
13
14
|
|
|
14
15
|
return prefix + (shouldFormat ? inspect(val, {colors: true, depth: null}) : val);
|
|
15
|
-
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const addRequestContextFormat = winston.format((info) => {
|
|
19
|
+
const context = getCurrentLogContext();
|
|
20
|
+
return {...context, ...info};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const formatContext = (info: winston.Logform.TransformableInfo): string => {
|
|
24
|
+
const contextParts = [
|
|
25
|
+
info.requestId ? `requestId=${info.requestId}` : undefined,
|
|
26
|
+
info.jobId ? `jobId=${info.jobId}` : undefined,
|
|
27
|
+
info.sessionId ? `sessionId=${info.sessionId}` : undefined,
|
|
28
|
+
info.userId ? `userId=${info.userId}` : undefined,
|
|
29
|
+
info.traceId ? `traceId=${info.traceId}` : undefined,
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
|
|
32
|
+
if (contextParts.length === 0) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
return ` ${contextParts.join(" ")}`;
|
|
36
|
+
};
|
|
16
37
|
|
|
17
38
|
// Winston doesn't operate like console.log by default, e.g. `logger.error('error',
|
|
18
39
|
// error)` only prints the message and no args. Add handling for all the args,
|
|
19
40
|
// while also supporting splat logging.
|
|
20
|
-
|
|
41
|
+
const printf = (timestamp = false) => {
|
|
21
42
|
return (info: winston.Logform.TransformableInfo) => {
|
|
22
43
|
const msg = formatWithInspect(info.message);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
44
|
+
const splatKey = Symbol.for("splat") as unknown as keyof winston.Logform.TransformableInfo;
|
|
45
|
+
const splatArgs = (info[splatKey] || []) as unknown[];
|
|
46
|
+
const rest = splatArgs.map((data) => formatWithInspect(data)).join(" ");
|
|
47
|
+
const context = formatContext(info);
|
|
25
48
|
if (timestamp) {
|
|
26
|
-
return `${info.timestamp} - ${info.level}: ${msg} ${rest}`;
|
|
49
|
+
return `${info.timestamp} - ${info.level}: ${msg}${context} ${rest}`;
|
|
27
50
|
}
|
|
28
|
-
return `${info.level}: ${msg} ${rest}`;
|
|
51
|
+
return `${info.level}: ${msg}${context} ${rest}`;
|
|
29
52
|
};
|
|
30
|
-
}
|
|
53
|
+
};
|
|
31
54
|
|
|
32
55
|
// Setup a global, default rejection handler.
|
|
33
56
|
winston.add(
|
|
34
57
|
new winston.transports.Console({
|
|
35
58
|
debugStdout: true,
|
|
36
59
|
format: winston.format.combine(
|
|
60
|
+
addRequestContextFormat(),
|
|
37
61
|
winston.format.colorize(),
|
|
38
62
|
winston.format.simple(),
|
|
39
63
|
winston.format.printf(printf(false))
|
|
@@ -46,11 +70,13 @@ winston.add(
|
|
|
46
70
|
|
|
47
71
|
// Setup a default console logger.
|
|
48
72
|
export const winstonLogger = winston.createLogger({
|
|
73
|
+
format: addRequestContextFormat(),
|
|
49
74
|
level: "debug",
|
|
50
75
|
transports: [
|
|
51
76
|
new winston.transports.Console({
|
|
52
77
|
debugStdout: true,
|
|
53
78
|
format: winston.format.combine(
|
|
79
|
+
addRequestContextFormat(),
|
|
54
80
|
winston.format.colorize(),
|
|
55
81
|
winston.format.simple(),
|
|
56
82
|
winston.format.printf(printf(false))
|
|
@@ -63,11 +89,15 @@ export const winstonLogger = winston.createLogger({
|
|
|
63
89
|
});
|
|
64
90
|
|
|
65
91
|
// Helper function to send logs to Sentry if enabled
|
|
66
|
-
|
|
92
|
+
const sendToSentry = (message: string, level: "debug" | "info" | "warn" | "error"): void => {
|
|
67
93
|
if (process.env.USE_SENTRY_LOGGING === "true" && Sentry.logger) {
|
|
68
|
-
Sentry.logger[level](
|
|
94
|
+
const logWithContext = Sentry.logger[level] as (
|
|
95
|
+
message: string,
|
|
96
|
+
attributes?: Record<string, unknown>
|
|
97
|
+
) => void;
|
|
98
|
+
logWithContext(message, getCurrentLogContext());
|
|
69
99
|
}
|
|
70
|
-
}
|
|
100
|
+
};
|
|
71
101
|
|
|
72
102
|
export const logger = {
|
|
73
103
|
// simple way to log a caught exception. e.g. promise().catch(logger.catch)
|
|
@@ -117,10 +147,10 @@ export interface LoggingOptions {
|
|
|
117
147
|
logSlowRequestsWriteMs?: number;
|
|
118
148
|
}
|
|
119
149
|
|
|
120
|
-
export
|
|
150
|
+
export const setupLogging = (options?: LoggingOptions): void => {
|
|
121
151
|
winstonLogger.clear();
|
|
122
152
|
if (!options?.disableConsoleLogging) {
|
|
123
|
-
const formats:
|
|
153
|
+
const formats: winston.Logform.Format[] = [addRequestContextFormat(), winston.format.simple()];
|
|
124
154
|
if (!options?.disableConsoleColors) {
|
|
125
155
|
formats.push(winston.format.colorize());
|
|
126
156
|
}
|
|
@@ -143,7 +173,7 @@ export function setupLogging(options?: LoggingOptions) {
|
|
|
143
173
|
colorize: false,
|
|
144
174
|
compress: true,
|
|
145
175
|
dirname: logDirectory,
|
|
146
|
-
format: winston.format.simple(),
|
|
176
|
+
format: winston.format.combine(addRequestContextFormat(), winston.format.simple()),
|
|
147
177
|
// 30 days of retention
|
|
148
178
|
maxFiles: 30,
|
|
149
179
|
// 50MB max file size
|
|
@@ -187,4 +217,4 @@ export function setupLogging(options?: LoggingOptions) {
|
|
|
187
217
|
winstonLogger.add(transport);
|
|
188
218
|
}
|
|
189
219
|
}
|
|
190
|
-
}
|
|
220
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import mongoose, {type Document} from "mongoose";
|
|
2
2
|
|
|
3
3
|
import type {APIErrorConstructor} from "../errors";
|
|
4
|
-
import {createdUpdatedPlugin, findOneOrNone} from "../plugins";
|
|
4
|
+
import {createdUpdatedPlugin, findExactlyOne, findOneOrNone, isDeletedPlugin} from "../plugins";
|
|
5
5
|
|
|
6
6
|
export interface VersionConfigDocument extends mongoose.Document {
|
|
7
7
|
webWarningVersion: number;
|
|
@@ -11,6 +11,8 @@ export interface VersionConfigDocument extends mongoose.Document {
|
|
|
11
11
|
warningMessage: string;
|
|
12
12
|
requiredMessage: string;
|
|
13
13
|
updateUrl?: string;
|
|
14
|
+
/** How often clients should poll for version updates, in minutes. Defaults to 1440 (24 hours). */
|
|
15
|
+
pollingIntervalMinutes: number;
|
|
14
16
|
created?: Date;
|
|
15
17
|
updated?: Date;
|
|
16
18
|
}
|
|
@@ -22,7 +24,7 @@ export interface VersionConfigModel extends mongoose.Model<VersionConfigDocument
|
|
|
22
24
|
): Promise<(Document & VersionConfigDocument) | null>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const versionConfigSchema = new mongoose.Schema<VersionConfigDocument>(
|
|
27
|
+
const versionConfigSchema = new mongoose.Schema<VersionConfigDocument, VersionConfigModel>(
|
|
26
28
|
{
|
|
27
29
|
mobileRequiredVersion: {
|
|
28
30
|
default: 0,
|
|
@@ -36,6 +38,13 @@ const versionConfigSchema = new mongoose.Schema<VersionConfigDocument>(
|
|
|
36
38
|
min: 0,
|
|
37
39
|
type: Number,
|
|
38
40
|
},
|
|
41
|
+
pollingIntervalMinutes: {
|
|
42
|
+
default: 1440,
|
|
43
|
+
description:
|
|
44
|
+
"How often clients poll for version updates, in minutes (default: 1440 = 24 hours)",
|
|
45
|
+
min: 1,
|
|
46
|
+
type: Number,
|
|
47
|
+
},
|
|
39
48
|
requiredMessage: {
|
|
40
49
|
default: "This version is no longer supported. Please update to continue.",
|
|
41
50
|
description: "Message shown on the blocking screen",
|
|
@@ -84,7 +93,9 @@ const versionConfigSchema = new mongoose.Schema<VersionConfigDocument>(
|
|
|
84
93
|
versionConfigSchema.index({_singleton: 1}, {unique: true});
|
|
85
94
|
|
|
86
95
|
versionConfigSchema.plugin(createdUpdatedPlugin);
|
|
96
|
+
versionConfigSchema.plugin(isDeletedPlugin);
|
|
87
97
|
versionConfigSchema.plugin(findOneOrNone);
|
|
98
|
+
versionConfigSchema.plugin(findExactlyOne);
|
|
88
99
|
|
|
89
100
|
export const VersionConfig = mongoose.model<VersionConfigDocument, VersionConfigModel>(
|
|
90
101
|
"VersionConfig",
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
|
-
import {APIError} from "../errors";
|
|
4
|
+
import {APIError, errorMessage} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
|
|
7
7
|
export const sendToGoogleChat = async (
|
|
8
8
|
messageText: string,
|
|
9
9
|
{channel, shouldThrow = false, env}: {channel?: string; shouldThrow?: boolean; env?: string} = {}
|
|
10
|
-
) => {
|
|
10
|
+
): Promise<void> => {
|
|
11
11
|
const chatWebhooksString = process.env.GOOGLE_CHAT_WEBHOOKS;
|
|
12
12
|
if (!chatWebhooksString) {
|
|
13
13
|
const msg = "GOOGLE_CHAT_WEBHOOKS not set. Google Chat message not sent";
|
|
14
|
-
Sentry.captureException(new
|
|
15
|
-
logger.error(msg);
|
|
14
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
16
15
|
return;
|
|
17
16
|
}
|
|
18
17
|
const chatWebhooks = JSON.parse(chatWebhooksString ?? "{}");
|
|
@@ -22,8 +21,7 @@ export const sendToGoogleChat = async (
|
|
|
22
21
|
|
|
23
22
|
if (!chatWebhookUrl) {
|
|
24
23
|
const msg = `No webhook url set in env for ${chatChannel}. Google Chat message not sent`;
|
|
25
|
-
Sentry.captureException(new
|
|
26
|
-
logger.error(msg);
|
|
24
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
27
25
|
return;
|
|
28
26
|
}
|
|
29
27
|
|
|
@@ -35,13 +33,13 @@ export const sendToGoogleChat = async (
|
|
|
35
33
|
try {
|
|
36
34
|
await axios.post(chatWebhookUrl, {text: formattedMessageText});
|
|
37
35
|
} catch (error: unknown) {
|
|
38
|
-
const
|
|
39
|
-
logger.error(`Error posting to Google Chat: ${
|
|
36
|
+
const message = errorMessage(error);
|
|
37
|
+
logger.error(`Error posting to Google Chat: ${message}`);
|
|
40
38
|
Sentry.captureException(error);
|
|
41
39
|
if (shouldThrow) {
|
|
42
40
|
throw new APIError({
|
|
43
41
|
status: 500,
|
|
44
|
-
title: `Error posting to Google Chat: ${
|
|
42
|
+
title: `Error posting to Google Chat: ${message}`,
|
|
45
43
|
});
|
|
46
44
|
}
|
|
47
45
|
}
|
|
@@ -2,6 +2,7 @@ import {afterAll, afterEach, beforeEach, describe, expect, it, type Mock, spyOn}
|
|
|
2
2
|
import * as Sentry from "@sentry/bun";
|
|
3
3
|
import axios from "axios";
|
|
4
4
|
|
|
5
|
+
import {APIError} from "../errors";
|
|
5
6
|
import {sendToSlack} from "./slackNotifier";
|
|
6
7
|
|
|
7
8
|
describe("sendToSlack", () => {
|
|
@@ -10,7 +11,7 @@ describe("sendToSlack", () => {
|
|
|
10
11
|
const ORIGINAL_ENV = process.env;
|
|
11
12
|
|
|
12
13
|
beforeEach(() => {
|
|
13
|
-
mockAxiosPost = spyOn(axios, "post").mockResolvedValue({status: 200}
|
|
14
|
+
mockAxiosPost = spyOn(axios, "post").mockResolvedValue({status: 200});
|
|
14
15
|
process.env = {...ORIGINAL_ENV};
|
|
15
16
|
process.env.SLACK_WEBHOOKS = undefined;
|
|
16
17
|
(Sentry.captureException as Mock<typeof Sentry.captureException>).mockClear();
|
|
@@ -85,6 +86,30 @@ describe("sendToSlack", () => {
|
|
|
85
86
|
expect(payload).toEqual({text: "[STG] status ok"});
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
it("reports to Sentry and returns early when channel has no webhook and no default", async () => {
|
|
90
|
+
process.env.SLACK_WEBHOOKS = JSON.stringify({ops: "https://slack.example/ops"});
|
|
91
|
+
|
|
92
|
+
await sendToSlack("orphan message", {slackChannel: "alerts"});
|
|
93
|
+
expect(mockAxiosPost.mock.calls.length).toBe(0);
|
|
94
|
+
expect(
|
|
95
|
+
(Sentry.captureException as Mock<typeof Sentry.captureException>).mock.calls.length
|
|
96
|
+
).toBe(1);
|
|
97
|
+
const captured = (Sentry.captureException as Mock<typeof Sentry.captureException>).mock
|
|
98
|
+
.calls[0][0] as APIError;
|
|
99
|
+
expect(captured).toBeInstanceOf(APIError);
|
|
100
|
+
expect(captured.title).toContain("alerts");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("posts directly using the url parameter without env lookup", async () => {
|
|
104
|
+
mockAxiosPost.mockResolvedValue({status: 200});
|
|
105
|
+
|
|
106
|
+
await sendToSlack("direct msg", {url: "https://direct.example/hook"});
|
|
107
|
+
expect(mockAxiosPost.mock.calls.length).toBe(1);
|
|
108
|
+
const [url, payload] = mockAxiosPost.mock.calls[0];
|
|
109
|
+
expect(url).toBe("https://direct.example/hook");
|
|
110
|
+
expect(payload).toEqual({text: "direct msg"});
|
|
111
|
+
});
|
|
112
|
+
|
|
88
113
|
it("captures error and throws APIError when shouldThrow=true", async () => {
|
|
89
114
|
process.env.SLACK_WEBHOOKS = JSON.stringify({
|
|
90
115
|
default: "https://slack.example/default",
|
|
@@ -95,8 +120,9 @@ describe("sendToSlack", () => {
|
|
|
95
120
|
await sendToSlack("err", {shouldThrow: true});
|
|
96
121
|
throw new Error("Expected sendToSlack to throw APIError");
|
|
97
122
|
} catch (error) {
|
|
98
|
-
|
|
99
|
-
expect(
|
|
123
|
+
const apiError = error as APIError;
|
|
124
|
+
expect(apiError.name).toBe("APIError");
|
|
125
|
+
expect(apiError.title).toMatch(/Error posting to slack/i);
|
|
100
126
|
}
|
|
101
127
|
expect(mockAxiosPost.mock.calls.length).toBe(1);
|
|
102
128
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
|
-
import {APIError} from "../errors";
|
|
4
|
+
import {APIError, errorMessage} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
// Convenience method to send data to a Slack webhook.
|
|
7
7
|
// If `url` is provided, it will be used directly instead of looking up from environment.
|
|
@@ -15,7 +15,7 @@ export const sendToSlack = async (
|
|
|
15
15
|
env,
|
|
16
16
|
url,
|
|
17
17
|
}: {slackChannel?: string; shouldThrow?: boolean; env?: string; url?: string} = {}
|
|
18
|
-
) => {
|
|
18
|
+
): Promise<void> => {
|
|
19
19
|
let slackWebhookUrl = url;
|
|
20
20
|
|
|
21
21
|
if (!slackWebhookUrl) {
|
|
@@ -37,9 +37,11 @@ export const sendToSlack = async (
|
|
|
37
37
|
|
|
38
38
|
if (!slackWebhookUrl) {
|
|
39
39
|
Sentry.captureException(
|
|
40
|
-
new
|
|
40
|
+
new APIError({
|
|
41
|
+
status: 500,
|
|
42
|
+
title: `No webhook url set in env for ${channel}. Slack message not sent`,
|
|
43
|
+
})
|
|
41
44
|
);
|
|
42
|
-
logger.debug(`No webhook url set in env for ${channel}.`);
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -54,13 +56,13 @@ export const sendToSlack = async (
|
|
|
54
56
|
text: formattedText,
|
|
55
57
|
});
|
|
56
58
|
} catch (error: unknown) {
|
|
57
|
-
const
|
|
58
|
-
logger.error(`Error posting to slack: ${
|
|
59
|
+
const message = errorMessage(error);
|
|
60
|
+
logger.error(`Error posting to slack: ${message}`);
|
|
59
61
|
Sentry.captureException(error);
|
|
60
62
|
if (shouldThrow) {
|
|
61
63
|
throw new APIError({
|
|
62
64
|
status: 500,
|
|
63
|
-
title: `Error posting to slack: ${
|
|
65
|
+
title: `Error posting to slack: ${message}`,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
|
-
import {APIError} from "../errors";
|
|
4
|
+
import {APIError, errorMessage} from "../errors";
|
|
5
5
|
import {logger} from "../logger";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -32,12 +32,11 @@ import {logger} from "../logger";
|
|
|
32
32
|
export const sendToZoom = async (
|
|
33
33
|
{header, body, subheader}: {header: string; body: string; subheader?: string},
|
|
34
34
|
{channel, shouldThrow = false, env}: {channel: string; shouldThrow?: boolean; env?: string}
|
|
35
|
-
) => {
|
|
35
|
+
): Promise<void> => {
|
|
36
36
|
const zoomWebhooksString = process.env.ZOOM_CHAT_WEBHOOKS;
|
|
37
37
|
if (!zoomWebhooksString) {
|
|
38
38
|
const msg = "ZOOM_CHAT_WEBHOOKS not set. Zoom message not sent";
|
|
39
|
-
Sentry.captureException(new
|
|
40
|
-
logger.error(msg);
|
|
39
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
const zoomWebhooks: Record<string, {channel: string; verificationToken: string}> = JSON.parse(
|
|
@@ -49,8 +48,7 @@ export const sendToZoom = async (
|
|
|
49
48
|
|
|
50
49
|
if (!zoomWebhookUrl) {
|
|
51
50
|
const msg = `No webhook url set in env for ${zoomChannel}. Zoom message not sent`;
|
|
52
|
-
Sentry.captureException(new
|
|
53
|
-
logger.error(msg);
|
|
51
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
54
52
|
return;
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -58,8 +56,7 @@ export const sendToZoom = async (
|
|
|
58
56
|
zoomWebhooks[zoomChannel]?.verificationToken ?? zoomWebhooks.default?.verificationToken;
|
|
59
57
|
if (!zoomToken) {
|
|
60
58
|
const msg = `No verification token set in env for ${zoomChannel}. Zoom message not sent`;
|
|
61
|
-
Sentry.captureException(new
|
|
62
|
-
logger.error(msg);
|
|
59
|
+
Sentry.captureException(new APIError({status: 500, title: msg}));
|
|
63
60
|
return;
|
|
64
61
|
}
|
|
65
62
|
|
|
@@ -96,13 +93,13 @@ export const sendToZoom = async (
|
|
|
96
93
|
}
|
|
97
94
|
);
|
|
98
95
|
} catch (error: unknown) {
|
|
99
|
-
const
|
|
100
|
-
logger.error(`Error posting to Zoom: ${
|
|
96
|
+
const message = errorMessage(error);
|
|
97
|
+
logger.error(`Error posting to Zoom: ${message}`);
|
|
101
98
|
Sentry.captureException(error);
|
|
102
99
|
if (shouldThrow) {
|
|
103
100
|
throw new APIError({
|
|
104
101
|
status: 500,
|
|
105
|
-
title: `Error posting to Zoom: ${
|
|
102
|
+
title: `Error posting to Zoom: ${message}`,
|
|
106
103
|
});
|
|
107
104
|
}
|
|
108
105
|
}
|
package/src/openApi.test.ts
CHANGED