Updates to Express v5

Updates the Express dependency to version 5.2.1 and its corresponding types.

This change also adapts the request validator middleware to extract validated query parameters and store them in `res.locals` instead of modifying `req.query` to align with Express v5's intended usage and prevent potential conflicts.

Includes a fix for a server startup error, logging the error and exiting the process, and adds a type definition file for Express locals.
This commit is contained in:
CSantosM 2026-02-23 17:15:10 +01:00
parent 02a9774d72
commit f204376c20
14 changed files with 93 additions and 133 deletions

View File

@ -68,7 +68,7 @@
"cors": "2.8.6",
"cron": "4.4.0",
"dotenv": "16.6.1",
"express": "4.22.1",
"express": "5.2.1",
"express-rate-limit": "7.5.1",
"inversify": "6.2.2",
"ioredis": "5.6.1",
@ -87,7 +87,7 @@
"@types/bcrypt": "5.0.2",
"@types/cookie-parser": "1.4.9",
"@types/cors": "2.8.19",
"@types/express": "4.17.25",
"@types/express": "5.0.6",
"@types/jest": "29.5.14",
"@types/lodash.merge": "4.6.9",
"@types/ms": "2.1.0",

View File

@ -19,7 +19,7 @@ export const startRecording = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const { roomId, config } = req.body;
const { fields } = req.query as { fields?: MeetRecordingField[] };
const { fields } = res.locals.validatedQuery as { fields?: MeetRecordingField[] };
logger.info(`Starting recording in room '${roomId}'`);
try {
@ -39,7 +39,7 @@ export const startRecording = async (req: Request, res: Response) => {
export const stopRecording = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingId = req.params.recordingId;
const { fields } = req.query as { fields?: MeetRecordingField[] };
const { fields } = res.locals.validatedQuery as { fields?: MeetRecordingField[] };
try {
logger.info(`Stopping recording '${recordingId}'`);
@ -58,7 +58,7 @@ export const stopRecording = async (req: Request, res: Response) => {
export const getRecordings = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const queryParams = req.query;
const queryParams = res.locals.validatedQuery ?? {};
logger.info('Getting all recordings');
@ -82,7 +82,7 @@ export const getRecordings = async (req: Request, res: Response) => {
export const bulkDeleteRecordings = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const { recordingIds } = req.query as { recordingIds: string[] };
const { recordingIds } = res.locals.validatedQuery as { recordingIds: string[] };
logger.info(`Deleting recordings: ${recordingIds}`);
@ -105,7 +105,7 @@ export const getRecording = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const recordingId = req.params.recordingId;
const { fields } = req.query as { fields?: MeetRecordingField[] };
const { fields } = res.locals.validatedQuery as { fields?: MeetRecordingField[] };
logger.info(`Getting recording '${recordingId}'`);
@ -220,13 +220,14 @@ export const getRecordingUrl = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const recordingId = req.params.recordingId;
const privateAccess = req.query.privateAccess === 'true';
const { privateAccess } = res.locals.validatedQuery as { privateAccess: string };
const isPrivateAccess = privateAccess === 'true';
logger.info(`Getting URL for recording '${recordingId}'`);
try {
const recordingSecrets = await recordingService.getRecordingAccessSecrets(recordingId);
const secret = privateAccess ? recordingSecrets.privateAccessSecret : recordingSecrets.publicAccessSecret;
const secret = isPrivateAccess ? recordingSecrets.privateAccessSecret : recordingSecrets.publicAccessSecret;
const recordingUrl = `${getBaseUrl()}/recording/${recordingId}?secret=${secret}`;
return res.status(200).json({ url: recordingUrl });
@ -239,7 +240,7 @@ export const downloadRecordingsZip = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const { recordingIds } = req.query as { recordingIds: string[] };
const { recordingIds } = res.locals.validatedQuery as { recordingIds: string[] };
const validRecordings: MeetRecordingInfo[] = [];
logger.info(`Preparing ZIP download for recordings: ${recordingIds}`);

View File

@ -32,7 +32,7 @@ export const getRoomMembers = async (req: Request, res: Response) => {
const roomMemberService = container.get(RoomMemberService);
const { roomId } = req.params;
const filters = req.query as MeetRoomMemberFilters;
const filters = res.locals.validatedQuery as MeetRoomMemberFilters;
try {
logger.verbose(`Getting members for room '${roomId}'`);
@ -100,7 +100,7 @@ export const bulkDeleteRoomMembers = async (req: Request, res: Response) => {
const roomMemberService = container.get(RoomMemberService);
const { roomId } = req.params;
const { memberIds } = req.query as { memberIds: string[] };
const { memberIds } = res.locals.validatedQuery as { memberIds: string[] };
try {
logger.verbose(`Deleting members from room '${roomId}' with IDs: ${memberIds}`);

View File

@ -22,7 +22,7 @@ export const createRoom = async (req: Request, res: Response) => {
const roomService = container.get(RoomService);
const options: MeetRoomOptions = req.body;
// Fields are merged from headers into req.query by the middleware
const { fields, extraFields } = req.query as {
const { fields, extraFields } = res.locals.validatedQuery as {
fields?: MeetRoomField[];
extraFields?: MeetRoomExtraField[];
};
@ -46,7 +46,7 @@ export const createRoom = async (req: Request, res: Response) => {
export const getRooms = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomService = container.get(RoomService);
const queryParams = req.query as MeetRoomFilters;
const queryParams = res.locals.validatedQuery as MeetRoomFilters;
logger.verbose(`Getting all rooms with filters: ${JSON.stringify(queryParams)}`);
@ -71,7 +71,7 @@ export const getRoom = async (req: Request, res: Response) => {
const { roomId } = req.params;
// Zod already validated and transformed to typed arrays
const { fields, extraFields } = req.query as {
const { fields, extraFields } = res.locals.validatedQuery as {
fields?: MeetRoomField[];
extraFields?: MeetRoomExtraField[];
};
@ -101,7 +101,7 @@ export const deleteRoom = async (req: Request, res: Response) => {
const roomService = container.get(RoomService);
const { roomId } = req.params;
const { fields, extraFields, withMeeting, withRecordings } = req.query as {
const { fields, extraFields, withMeeting, withRecordings } = res.locals.validatedQuery as {
fields?: MeetRoomField[];
extraFields?: MeetRoomExtraField[];
withMeeting: MeetRoomDeletionPolicyWithMeeting;
@ -141,7 +141,7 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomService = container.get(RoomService);
const { roomIds, fields, extraFields, withMeeting, withRecordings } = req.query as {
const { roomIds, fields, extraFields, withMeeting, withRecordings } = res.locals.validatedQuery as {
roomIds: string[];
fields?: MeetRoomField[];
extraFields?: MeetRoomExtraField[];
@ -150,7 +150,7 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => {
};
try {
logger.verbose(`Deleting rooms: ${roomIds} with options: ${JSON.stringify(req.query)}`);
logger.verbose(`Deleting rooms: ${roomIds} with options: ${JSON.stringify(res.locals.validatedQuery)}`);
const deleteOpts: MeetRoomDeletionOptions = {
withMeeting,

View File

@ -10,9 +10,9 @@ import {
} from '../models/error.model.js';
import { LoggerService } from '../services/logger.service.js';
import { RequestSessionService } from '../services/request-session.service.js';
import { TokenService } from '../services/token.service.js';
import { UserService } from '../services/user.service.js';
import { getBaseUrl } from '../utils/url.utils.js';
import { TokenService } from '../services/token.service.js';
export const createUser = async (req: Request, res: Response) => {
const userOptions = req.body as MeetUserOptions;
@ -31,7 +31,7 @@ export const createUser = async (req: Request, res: Response) => {
};
export const getUsers = async (req: Request, res: Response) => {
const queryParams = req.query as MeetUserFilters;
const queryParams = res.locals.validatedQuery as MeetUserFilters;
const logger = container.get(LoggerService);
logger.verbose(`Getting all users`);
@ -186,7 +186,7 @@ export const deleteUser = async (req: Request, res: Response) => {
};
export const bulkDeleteUsers = async (req: Request, res: Response) => {
const { userIds } = req.query as { userIds: string[] };
const { userIds } = res.locals.validatedQuery as { userIds: string[] };
const logger = container.get(LoggerService);
logger.verbose(`Deleting users: ${userIds}`);

View File

@ -15,7 +15,8 @@ import {
export const validateStartRecordingReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields header into query params before validation
mergeRecordingHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeRecordingHeaderFieldsIntoQuery(req.headers, query);
const bodyResult = StartRecordingReqSchema.safeParse(req.body);
@ -25,27 +26,28 @@ export const validateStartRecordingReq = (req: Request, res: Response, next: Nex
req.body = bodyResult.data;
const queryResult = RecordingQueryFieldsSchema.safeParse(req.query);
const queryResult = RecordingQueryFieldsSchema.safeParse(query);
if (!queryResult.success) {
return rejectUnprocessableRequest(res, queryResult.error);
}
req.query = queryResult.data;
res.locals.validatedQuery = queryResult.data;
next();
};
export const validateGetRecordingsReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields header into query params before validation
mergeRecordingHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeRecordingHeaderFieldsIntoQuery(req.headers, query);
const { success, error, data } = RecordingFiltersSchema.safeParse(req.query);
const { success, error, data } = RecordingFiltersSchema.safeParse(query);
if (!success) {
return rejectUnprocessableRequest(res, error);
}
req.query = {
res.locals.validatedQuery = {
...data,
maxItems: data.maxItems?.toString()
};
@ -59,7 +61,7 @@ export const validateBulkDeleteRecordingsReq = (req: Request, res: Response, nex
return rejectUnprocessableRequest(res, error);
}
req.query = data;
res.locals.validatedQuery = data;
next();
};
@ -77,11 +79,12 @@ export const withValidRecordingId = (req: Request, res: Response, next: NextFunc
export const validateGetRecordingReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields header into query params before validation
mergeRecordingHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeRecordingHeaderFieldsIntoQuery(req.headers, query);
const { success, error, data } = GetRecordingReqSchema.safeParse({
params: req.params,
query: req.query
query
});
if (!success) {
@ -89,17 +92,18 @@ export const validateGetRecordingReq = (req: Request, res: Response, next: NextF
}
req.params.recordingId = data.params.recordingId;
req.query = data.query;
res.locals.validatedQuery = data.query;
next();
};
export const validateStopRecordingReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields header into query params before validation
mergeRecordingHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeRecordingHeaderFieldsIntoQuery(req.headers, query);
const { success, error, data } = StopRecordingReqSchema.safeParse({
params: req.params,
query: req.query
query
});
if (!success) {
@ -107,7 +111,7 @@ export const validateStopRecordingReq = (req: Request, res: Response, next: Next
}
req.params.recordingId = data.params.recordingId;
req.query = data.query;
res.locals.validatedQuery = data.query;
next();
};
@ -139,6 +143,6 @@ export const validateGetRecordingUrlReq = (req: Request, res: Response, next: Ne
}
req.params.recordingId = data.params.recordingId;
req.query.privateAccess = data.query.privateAccess ? 'true' : 'false';
res.locals.validatedQuery = { privateAccess: data.query.privateAccess ? 'true' : 'false' };
next();
};

View File

@ -28,7 +28,7 @@ export const validateGetRoomMembersReq = (req: Request, res: Response, next: Nex
return rejectUnprocessableRequest(res, error);
}
req.query = {
res.locals.validatedQuery = {
...data,
maxItems: data.maxItems?.toString()
};
@ -42,7 +42,7 @@ export const validateBulkDeleteRoomMembersReq = (req: Request, res: Response, ne
return rejectUnprocessableRequest(res, error);
}
req.query = data;
res.locals.validatedQuery = data;
next();
};

View File

@ -15,7 +15,8 @@ import {
} from '../../models/zod-schemas/room.schema.js';
export const validateCreateRoomReq = (req: Request, res: Response, next: NextFunction) => {
mergeHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeHeaderFieldsIntoQuery(req.headers, query);
const bodyResult = RoomOptionsSchema.safeParse(req.body);
@ -25,27 +26,28 @@ export const validateCreateRoomReq = (req: Request, res: Response, next: NextFun
req.body = bodyResult.data;
const { success, error, data } = RoomQueryFieldsSchema.safeParse(req.query);
const { success, error, data } = RoomQueryFieldsSchema.safeParse(query);
if (!success) {
return rejectUnprocessableRequest(res, error);
}
req.query = data;
res.locals.validatedQuery = data;
next();
};
export const validateGetRoomsReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields and X-ExtraFields headers into query params before validation
mergeHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeHeaderFieldsIntoQuery(req.headers, query);
const { success, error, data } = RoomFiltersSchema.safeParse(req.query);
const { success, error, data } = RoomFiltersSchema.safeParse(query);
if (!success) {
return rejectUnprocessableRequest(res, error);
}
req.query = {
res.locals.validatedQuery = {
...data,
maxItems: data.maxItems?.toString()
};
@ -54,15 +56,16 @@ export const validateGetRoomsReq = (req: Request, res: Response, next: NextFunct
export const validateBulkDeleteRoomsReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields and X-ExtraFields headers into query params before validation
mergeHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeHeaderFieldsIntoQuery(req.headers, query);
const { success, error, data } = BulkDeleteRoomsReqSchema.safeParse(req.query);
const { success, error, data } = BulkDeleteRoomsReqSchema.safeParse(query);
if (!success) {
return rejectUnprocessableRequest(res, error);
}
req.query = data;
res.locals.validatedQuery = data;
next();
};
@ -80,15 +83,16 @@ export const withValidRoomId = (req: Request, res: Response, next: NextFunction)
export const validateGetRoomReq = (req: Request, res: Response, next: NextFunction) => {
// Merge X-Fields and X-ExtraFields headers into query params before validation
mergeHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeHeaderFieldsIntoQuery(req.headers, query);
const { success, error, data } = RoomQueryFieldsSchema.safeParse(req.query);
const { success, error, data } = RoomQueryFieldsSchema.safeParse(query);
if (!success) {
return rejectUnprocessableRequest(res, error);
}
req.query = data;
res.locals.validatedQuery = data;
next();
};
@ -102,15 +106,16 @@ export const validateDeleteRoomReq = (req: Request, res: Response, next: NextFun
req.params.roomId = roomIdResult.data;
// Merge X-Fields and X-ExtraFields headers into query params before validation
mergeHeaderFieldsIntoQuery(req.headers, req.query);
const query = req.query;
mergeHeaderFieldsIntoQuery(req.headers, query);
const queryParamsResult = DeleteRoomReqSchema.safeParse(req.query);
const queryParamsResult = DeleteRoomReqSchema.safeParse(query);
if (!queryParamsResult.success) {
return rejectUnprocessableRequest(res, queryParamsResult.error);
}
req.query = queryParamsResult.data;
res.locals.validatedQuery = queryParamsResult.data;
next();
};

View File

@ -27,7 +27,7 @@ export const validateGetUsersReq = (req: Request, res: Response, next: NextFunct
return rejectUnprocessableRequest(res, error);
}
req.query = {
res.locals.validatedQuery = {
...data,
maxItems: data.maxItems?.toString()
};
@ -41,7 +41,7 @@ export const validateBulkDeleteUsersReq = (req: Request, res: Response, next: Ne
return rejectUnprocessableRequest(res, error);
}
req.query = data;
res.locals.validatedQuery = data;
next();
};

View File

@ -127,7 +127,7 @@ const startServer = (app: express.Application) => {
const basePath = getBasePath();
const basePathDisplay = basePath === '/' ? '' : basePath.slice(0, -1);
app.listen(MEET_ENV.SERVER_PORT, async () => {
const server = app.listen(MEET_ENV.SERVER_PORT, () => {
console.log(' ');
console.log('---------------------------------------------------------');
console.log(' ');
@ -145,6 +145,11 @@ const startServer = (app: express.Application) => {
);
logEnvVars();
});
server.on('error', (error: Error) => {
console.error('Server failed to start:', error.message);
process.exit(1);
});
};
/**

View File

@ -0,0 +1,9 @@
declare global {
namespace Express {
interface Locals {
validatedQuery?: Record<string, unknown>;
}
}
}
export {};

View File

@ -380,11 +380,6 @@ describe('Recordings API Tests', () => {
expectValidationError(response, 'maxItems', 'must be a positive number');
});
it('should fail when fields is not a string', async () => {
const response = await getAllRecordings({ fields: { invalid: 'object' } });
expectValidationError(response, 'fields', 'Expected string');
});
it('should fail when sortField is invalid', async () => {
const response = await getAllRecordings({ sortField: 'invalidField' });
expectValidationError(response, 'sortField', 'Invalid enum value');

View File

@ -229,11 +229,6 @@ describe('Room API Tests', () => {
expectValidationError(response, 'maxItems', 'must be a positive number');
});
it('should fail when fields is not a string', async () => {
const response = await getRooms({ fields: { invalid: 'data' } });
expectValidationError(response, 'fields', 'Expected string');
});
it('should fail when sortField is invalid', async () => {
const response = await getRooms({ sortField: 'invalidField' });
expectValidationError(response, 'sortField', 'Invalid enum value');

82
pnpm-lock.yaml generated
View File

@ -105,11 +105,11 @@ importers:
specifier: 16.6.1
version: 16.6.1
express:
specifier: 4.22.1
version: 4.22.1
specifier: 5.2.1
version: 5.2.1
express-rate-limit:
specifier: 7.5.1
version: 7.5.1(express@4.22.1)
version: 7.5.1(express@5.2.1)
inversify:
specifier: 6.2.2
version: 6.2.2(reflect-metadata@0.2.2)
@ -152,13 +152,13 @@ importers:
version: 5.0.2
'@types/cookie-parser':
specifier: 1.4.9
version: 1.4.9(@types/express@4.17.25)
version: 1.4.9(@types/express@5.0.6)
'@types/cors':
specifier: 2.8.19
version: 2.8.19
'@types/express':
specifier: 4.17.25
version: 4.17.25
specifier: 5.0.6
version: 5.0.6
'@types/jest':
specifier: 29.5.14
version: 29.5.14
@ -4133,9 +4133,6 @@ packages:
'@types/express@4.17.23':
resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==}
'@types/express@4.17.25':
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
'@types/express@5.0.6':
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
@ -6080,10 +6077,6 @@ packages:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'}
express@4.22.1:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
express@5.2.1:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'}
@ -14834,9 +14827,9 @@ snapshots:
dependencies:
'@types/express': 4.17.23
'@types/cookie-parser@1.4.9(@types/express@4.17.25)':
'@types/cookie-parser@1.4.9(@types/express@5.0.6)':
dependencies:
'@types/express': 4.17.25
'@types/express': 5.0.6
'@types/cookiejar@2.1.5': {}
@ -14887,13 +14880,6 @@ snapshots:
'@types/qs': 6.14.0
'@types/serve-static': 2.2.0
'@types/express@4.17.25':
dependencies:
'@types/body-parser': 1.19.6
'@types/express-serve-static-core': 4.19.7
'@types/qs': 6.14.0
'@types/serve-static': 1.15.10
'@types/express@5.0.6':
dependencies:
'@types/body-parser': 1.19.6
@ -17375,10 +17361,6 @@ snapshots:
dependencies:
express: 4.21.2
express-rate-limit@7.5.1(express@4.22.1):
dependencies:
express: 4.22.1
express-rate-limit@7.5.1(express@5.2.1):
dependencies:
express: 5.2.1
@ -17419,42 +17401,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
express@4.22.1:
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.4
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.3.1
fresh: 0.5.2
http-errors: 2.0.1
merge-descriptors: 1.0.3
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.12
proxy-addr: 2.0.7
qs: 6.14.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.19.2
serve-static: 1.16.3
setprototypeof: 1.2.0
statuses: 2.0.2
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
express@5.2.1:
dependencies:
accepts: 2.0.0
@ -17477,7 +17423,7 @@ snapshots:
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
qs: 6.14.0
qs: 6.15.0
range-parser: 1.2.1
router: 2.2.0
send: 1.2.1
@ -18028,7 +17974,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
http-proxy-middleware@2.0.9(@types/express@4.17.25):
http-proxy-middleware@2.0.9(@types/express@4.17.23):
dependencies:
'@types/http-proxy': 1.17.17
http-proxy: 1.18.1(debug@4.4.3)
@ -18036,7 +17982,7 @@ snapshots:
is-plain-obj: 3.0.0
micromatch: 4.0.8
optionalDependencies:
'@types/express': 4.17.25
'@types/express': 4.17.23
transitivePeerDependencies:
- debug
@ -22109,7 +22055,7 @@ snapshots:
dependencies:
'@types/bonjour': 3.5.13
'@types/connect-history-api-fallback': 1.5.4
'@types/express': 4.17.25
'@types/express': 4.17.23
'@types/express-serve-static-core': 4.19.7
'@types/serve-index': 1.9.4
'@types/serve-static': 1.15.10
@ -22121,9 +22067,9 @@ snapshots:
colorette: 2.0.20
compression: 1.8.1
connect-history-api-fallback: 2.0.0
express: 4.22.1
express: 4.21.2
graceful-fs: 4.2.11
http-proxy-middleware: 2.0.9(@types/express@4.17.25)
http-proxy-middleware: 2.0.9(@types/express@4.17.23)
ipaddr.js: 2.3.0
launch-editor: 2.12.0
open: 10.2.0