From 888b399a4f8c659093c6db466449f89b5bf5a82e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 15:29:44 -0500 Subject: [PATCH 01/53] fix: dynamically set Swagger UI server URL from request ## Changes - Swagger UI now prepends the current server URL (protocol + host) to the servers list, so it works correctly in remote/Docker environments instead of always pointing to localhost. ## Closes Fixes #3339 --- server/src/app.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index fea81ba07..f7cff5418 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -74,8 +74,22 @@ export const createApp = ({ }, }) ); - // Swagger UI - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); + // Swagger UI — dynamically set server URL from request + app.use("/api-docs", swaggerUi.serve, (req, res, next) => { + const protocol = req.protocol; + const host = req.get("host"); + const dynamicSpec = { + ...openApiSpec, + servers: [ + { + url: `${protocol}://${host}/api/v1`, + description: "Current Server", + }, + ...openApiSpec.servers, + ], + }; + swaggerUi.setup(dynamicSpec)(req, res, next); + }); app.use("/api/v1/health", (req, res) => { res.json({ From 93b9abd5082c3237b6665ae320e0b42e1949eefc Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 15:38:34 -0500 Subject: [PATCH 02/53] fix: add migration to reset infrastructure thresholds from 5 to 100 ## Changes - Added migration 0004_fixInfrastructureThresholds that finds hardware monitors with threshold values of 5 (the old incorrect default) and updates them to 100 (effectively disabled) - Runs automatically on server startup like other migrations ## Closes Fixes #3327 --- .../0004_fixInfrastructureThresholds.ts | 65 +++++++++++++++++++ server/src/db/migration/index.ts | 2 + 2 files changed, 67 insertions(+) create mode 100644 server/src/db/migration/0004_fixInfrastructureThresholds.ts diff --git a/server/src/db/migration/0004_fixInfrastructureThresholds.ts b/server/src/db/migration/0004_fixInfrastructureThresholds.ts new file mode 100644 index 000000000..041f8a9dc --- /dev/null +++ b/server/src/db/migration/0004_fixInfrastructureThresholds.ts @@ -0,0 +1,65 @@ +import { MonitorModel } from "../models/Monitor.js"; +import { logger } from "@/utils/logger.js"; + +/** + * Fix infrastructure monitors that had thresholds incorrectly set to 5 + * due to the old default value. Updates them to 100 (disabled). + */ +export async function fixInfrastructureThresholds(): Promise { + const SERVICE_NAME = "Migration:FixInfrastructureThresholds"; + + try { + logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" }); + + const thresholdFields = [ + "cpuAlertThreshold", + "memoryAlertThreshold", + "diskAlertThreshold", + "tempAlertThreshold", + ]; + + // Find hardware monitors with any threshold set to 5 + const filter = { + type: "hardware", + $or: thresholdFields.map((field) => ({ [field]: 5 })), + }; + + const monitors = await MonitorModel.find(filter); + + if (monitors.length === 0) { + logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 5 found" }); + return; + } + + logger.info({ + service: SERVICE_NAME, + message: `Found ${monitors.length} monitors with threshold value of 5`, + }); + + let updatedCount = 0; + + for (const monitor of monitors) { + const update: Record = {}; + + for (const field of thresholdFields) { + if ((monitor as any)[field] === 5) { + update[field] = 100; + } + } + + if (Object.keys(update).length > 0) { + await MonitorModel.updateOne({ _id: monitor._id }, { $set: update }); + updatedCount++; + } + } + + logger.info({ + service: SERVICE_NAME, + message: `Fixed ${updatedCount} monitors — thresholds updated from 5 to 100`, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error({ service: SERVICE_NAME, message: `Error fixing thresholds: ${errorMessage}` }); + throw error; + } +} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index 8ea374514..3b7b45ada 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -1,6 +1,7 @@ import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; +import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js"; import MigrationModel from "../models/Migration.js"; type MigrationEntry = { @@ -12,6 +13,7 @@ const migrations: MigrationEntry[] = [ { name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold }, { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, + { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, ]; const runMigrations = async (logger?: { info: Function; error: Function }) => { From b2e2b11ef2146afd5d444cf22c752843f93588cf Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 15:41:01 -0500 Subject: [PATCH 03/53] fix: support IPv6 addresses in ping monitor ## Changes - Fixed ping host sanitization regex that stripped everything after the first colon, which destroyed IPv6 addresses like 2001:db8::1 - New regex properly handles bracketed IPv6 ([::1]:port), bare IPv6, and IPv4/hostname:port formats separately ## Closes Fixes #3053 --- server/src/service/infrastructure/networkService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 702affed2..fe770bd0e 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -261,7 +261,8 @@ class NetworkService implements INetworkService { const sanitizedHost = rawUrl .replace(/^https?:\/\//, "") .replace(/\/.*$/, "") - .replace(/:.*/, ""); + .replace(/^\[(.+)\](?::.*)?$/, "$1") // Extract IPv6 from brackets, strip port + .replace(/^([^[\]:]+):(\d+)$/, "$1"); // Strip port from IPv4/hostname only const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost)); if (!response) { From 75eca743b4316b36aef3ea48a3989e9d16b0c6bf Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 16:30:19 -0500 Subject: [PATCH 04/53] fix: use neutral gray background for dark mode ## Changes - Replace MUI default blue-tinted dark background with neutral grays (#151518 for default, #1c1c21 for paper/cards) - Remove decorative blue/purple SVG blob overlay from dark mode background ## Benefits - Dark mode now has a clean, neutral gray appearance instead of a blue tint --- client/src/Components/layout/AppLayout.tsx | 4 ---- client/src/Utils/Theme/Palette.ts | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Components/layout/AppLayout.tsx b/client/src/Components/layout/AppLayout.tsx index baadd85ca..369bd0726 100644 --- a/client/src/Components/layout/AppLayout.tsx +++ b/client/src/Components/layout/AppLayout.tsx @@ -47,10 +47,6 @@ const AppLayout = ({ children }: AppLayoutProps) => { sx={{ minHeight: "100vh", backgroundColor: theme.palette.background.default, - backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none", - backgroundSize: "100% 100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", }} > diff --git a/client/src/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index d314c8e27..01a9a704b 100644 --- a/client/src/Utils/Theme/Palette.ts +++ b/client/src/Utils/Theme/Palette.ts @@ -31,4 +31,8 @@ export const darkPalette = { secondary: { main: colors.gray700, }, + background: { + default: "#151518", + paper: "#1c1c21", + }, }; From d2f3335b1aad748f3b09044575a95d5f2ad156d4 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 16:32:01 -0500 Subject: [PATCH 05/53] fix: add type annotations to Swagger middleware parameters Fixes TypeScript build error TS7006 for implicit 'any' types on req, res, next. --- server/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/app.ts b/server/src/app.ts index f7cff5418..58684532c 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -75,7 +75,7 @@ export const createApp = ({ }) ); // Swagger UI — dynamically set server URL from request - app.use("/api-docs", swaggerUi.serve, (req, res, next) => { + app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => { const protocol = req.protocol; const host = req.get("host"); const dynamicSpec = { From 8df263fd938a7a2fd2a267414ff0bd04b5993c2d Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 16:44:20 -0500 Subject: [PATCH 06/53] fix: also migrate threshold value of 0 to 100, fix formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback — thresholds set to 0 also caused issues and should be migrated to 100 (disabled) alongside the value of 5. --- .../0004_fixInfrastructureThresholds.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/server/src/db/migration/0004_fixInfrastructureThresholds.ts b/server/src/db/migration/0004_fixInfrastructureThresholds.ts index 041f8a9dc..6d369f311 100644 --- a/server/src/db/migration/0004_fixInfrastructureThresholds.ts +++ b/server/src/db/migration/0004_fixInfrastructureThresholds.ts @@ -2,7 +2,7 @@ import { MonitorModel } from "../models/Monitor.js"; import { logger } from "@/utils/logger.js"; /** - * Fix infrastructure monitors that had thresholds incorrectly set to 5 + * Fix infrastructure monitors that had thresholds incorrectly set to 0 or 5 * due to the old default value. Updates them to 100 (disabled). */ export async function fixInfrastructureThresholds(): Promise { @@ -11,29 +11,24 @@ export async function fixInfrastructureThresholds(): Promise { try { logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" }); - const thresholdFields = [ - "cpuAlertThreshold", - "memoryAlertThreshold", - "diskAlertThreshold", - "tempAlertThreshold", - ]; + const thresholdFields = ["cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", "tempAlertThreshold"]; - // Find hardware monitors with any threshold set to 5 + // Find hardware monitors with any threshold set to 0 or 5 const filter = { type: "hardware", - $or: thresholdFields.map((field) => ({ [field]: 5 })), + $or: thresholdFields.flatMap((field) => [{ [field]: 0 }, { [field]: 5 }]), }; const monitors = await MonitorModel.find(filter); if (monitors.length === 0) { - logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 5 found" }); + logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 0 or 5 found" }); return; } logger.info({ service: SERVICE_NAME, - message: `Found ${monitors.length} monitors with threshold value of 5`, + message: `Found ${monitors.length} monitors with threshold value of 0 or 5`, }); let updatedCount = 0; @@ -42,7 +37,7 @@ export async function fixInfrastructureThresholds(): Promise { const update: Record = {}; for (const field of thresholdFields) { - if ((monitor as any)[field] === 5) { + if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) { update[field] = 100; } } @@ -55,7 +50,7 @@ export async function fixInfrastructureThresholds(): Promise { logger.info({ service: SERVICE_NAME, - message: `Fixed ${updatedCount} monitors — thresholds updated from 5 to 100`, + message: `Fixed ${updatedCount} monitors — thresholds updated from 0/5 to 100`, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); From 3c99809e8e6f043f60fcce81f7f3b679373d312a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Mar 2026 23:24:05 +0000 Subject: [PATCH 07/53] rix reveresed checks --- client/src/Components/common/charts/HeatmapResponseTime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Components/common/charts/HeatmapResponseTime.tsx b/client/src/Components/common/charts/HeatmapResponseTime.tsx index 8178ffb92..f2aed0b7a 100644 --- a/client/src/Components/common/charts/HeatmapResponseTime.tsx +++ b/client/src/Components/common/charts/HeatmapResponseTime.tsx @@ -32,7 +32,7 @@ export const HeatmapResponseTime = ({ if (!checks || checks.length === 0) return null; - const latestChecks = checks.slice(-25).reverse(); + const latestChecks = checks.slice(-25); let data: Array; if (latestChecks.length !== 25) { From 82175f166feedf607c65ccc8ea8f444e86104752 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Mar 2026 23:24:48 +0000 Subject: [PATCH 08/53] pingProvider, httpProvider --- server/src/config/services.ts | 12 ++- .../repositories/checks/IChecksRepository.ts | 11 ++- .../checks/MongoChecksRepistory.ts | 2 +- server/src/service/business/checkService.ts | 11 ++- .../infrastructure/network/AdvancedMatcher.ts | 45 +++++++++ .../infrastructure/network/HttpProvider.ts | 94 +++++++++++++++++++ .../infrastructure/network/IStatusProvider.ts | 8 ++ .../infrastructure/network/PingProvider.ts | 64 +++++++++++++ .../service/infrastructure/network/utils.ts | 85 +++++++++++++++++ .../service/infrastructure/networkService.ts | 66 ++++--------- server/src/types/network.ts | 4 +- 11 files changed, 345 insertions(+), 57 deletions(-) create mode 100644 server/src/service/infrastructure/network/AdvancedMatcher.ts create mode 100644 server/src/service/infrastructure/network/HttpProvider.ts create mode 100644 server/src/service/infrastructure/network/IStatusProvider.ts create mode 100644 server/src/service/infrastructure/network/PingProvider.ts create mode 100644 server/src/service/infrastructure/network/utils.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 56aef2f19..485297fe0 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -75,6 +75,9 @@ import { } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import { EnvConfig } from "@/service/system/settingsService.js"; +import { PingProvider } from "@/service/infrastructure/network/PingProvider.js"; +import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; +import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; export type InitializedServices = { settingsService: any; @@ -144,6 +147,11 @@ export const initializeServices = async ({ const teamsRepository = new MongoTeamsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); + // Providers + + const pingProvider = new PingProvider(ping); + const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); + const networkService = new NetworkService( axios, got, @@ -158,7 +166,9 @@ export const initializeServices = async ({ net, settingsService, grpc, - protoLoader + protoLoader, + pingProvider, + httpProvider ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 31aac0ec9..c89e5d5fc 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -20,12 +20,19 @@ export interface IChecksRepository { monitorId: string, sortOrder: string, dateRange: string, - filter: string, + filter: string | undefined, page: number, rowsPerPage: number, status: boolean | undefined ): Promise; - findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise; + findByTeamId( + sortOrder: string, + dateRange: string, + filter: string | undefined, + page: number, + rowsPerPage: number, + teamId: string + ): Promise; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findByDateRangeAndMonitorId( monitorId: string, diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 66e7f43f9..b019a0430 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -231,7 +231,7 @@ class MongoChecksRepository implements IChecksRepository { monitorId: string, sortOrder: string, dateRange: string, - filter: string, + filter: string | undefined, page: number, rowsPerPage: number, status: boolean | undefined diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 2d2bf0e01..abeb577ed 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -124,8 +124,13 @@ class CheckService implements ICheckService { await this.monitorsRepository.findById(monitorId, teamId); const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; - if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) { - throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); + if (!sortOrder || !dateRange || !page || !rowsPerPage) { + throw new AppError({ + message: `Missing required query parameters sortOrder: ${sortOrder}, dateRange: ${dateRange}, filter: ${filter}, page: ${page}, rowsPerPage: ${rowsPerPage}`, + service: SERVICE_NAME, + method: "getChecksByMonitor", + status: 400, + }); } const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); const parsedPage = page ? parseInt(page) : 0; @@ -138,7 +143,7 @@ class CheckService implements ICheckService { getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record }) => { const { sortOrder, dateRange, filter, page, rowsPerPage } = query; - if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) { + if (!sortOrder || !dateRange || !page || !rowsPerPage) { throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); } diff --git a/server/src/service/infrastructure/network/AdvancedMatcher.ts b/server/src/service/infrastructure/network/AdvancedMatcher.ts new file mode 100644 index 000000000..57bf7022f --- /dev/null +++ b/server/src/service/infrastructure/network/AdvancedMatcher.ts @@ -0,0 +1,45 @@ +import { Monitor } from "@/types/monitor.js"; +import jmespath from "jmespath"; +type JmesPath = typeof jmespath; + +export class AdvancedMatcher { + constructor(private jmespath: JmesPath) {} + + private compare(actual: unknown, expected: string, method?: string): boolean { + if (method === "equal") return String(actual) === expected; + if (method === "include") return String(actual).includes(expected); + if (method === "regex") return new RegExp(expected).test(String(actual)); + return String(actual) === expected; // Default + } + + validate(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } { + const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; + if (!useAdvancedMatching) return { ok: true, message: "Success" }; + + let dataToValidate = payload; + + if (jsonPath) { + try { + dataToValidate = this.jmespath.search(payload, jsonPath); + } catch { + return { ok: false, message: "Error evaluating JSON path" }; + } + } + + if (expectedValue) { + const ok = this.compare(dataToValidate, expectedValue, matchMethod); + return { + ok, + message: ok ? "Success" : "Expected value did not match", + extracted: dataToValidate, + }; + } + + const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null; + return { + ok: !isFalsey, + message: !isFalsey ? "Success" : "Extracted value is falsey", + extracted: dataToValidate, + }; + } +} diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts new file mode 100644 index 000000000..84f290b16 --- /dev/null +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -0,0 +1,94 @@ +import { type Got, HTTPError, RequestError } from "got"; +import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { HttpStatusPayload } from "@/types/network.js"; +import { MonitorStatusResponse } from "@/types/network.js"; +import { Agent as HttpsAgent } from "https"; +import { Monitor } from "@/types/monitor.js"; +import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; + +export class HttpProvider implements IStatusProvider { + readonly type = "http"; + + constructor( + private got: Got, + private advancedMatcher: AdvancedMatcher + ) {} + + supports(type: string) { + return type === "http"; + } + + private handleHttpError(error: unknown, monitor: Monitor): MonitorStatusResponse { + if (error instanceof HTTPError || error instanceof RequestError) { + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: error.response?.statusCode ?? NETWORK_ERROR, + message: error.message, + responseTime: error.timings?.phases?.total ?? 0, + timings: error.timings, + payload: null as T, + }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: NETWORK_ERROR, + message: error instanceof Error ? error.message : String(error), + responseTime: 0, + payload: null as T, + }; + } + + async handle(monitor: Monitor): Promise> { + const { url, secret, ignoreTlsErrors } = monitor; + const options: Record = { + headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined, + }; + + options.agent = { + https: new HttpsAgent({ rejectUnauthorized: !ignoreTlsErrors }), + }; + + try { + const response = await this.got(url, options); + let payload: T; + const isJson = response.headers["content-type"]?.includes("application/json"); + + if (isJson) { + try { + payload = JSON.parse(response.body) as T; + } catch { + payload = response.body as unknown as T; + } + } else { + payload = response.body as unknown as T; + } + + const matchResult = this.advancedMatcher.validate(payload, monitor); + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response.ok && matchResult.ok, + code: response.statusCode, + message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message, + responseTime: response.timings.phases.total ?? 0, + timings: response.timings, + payload, + extracted: matchResult.extracted, + jsonPath: monitor.jsonPath, + matchMethod: monitor.matchMethod, + expectedValue: monitor.expectedValue, + }; + } catch (error: unknown) { + return this.handleHttpError(error, monitor); + } + } +} diff --git a/server/src/service/infrastructure/network/IStatusProvider.ts b/server/src/service/infrastructure/network/IStatusProvider.ts new file mode 100644 index 000000000..bde6a2db0 --- /dev/null +++ b/server/src/service/infrastructure/network/IStatusProvider.ts @@ -0,0 +1,8 @@ +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { MonitorStatusResponse } from "@/types/network.js"; + +export interface IStatusProvider { + type: string; + supports: (type: MonitorType) => boolean; + handle(monitor: Monitor): Promise>; +} diff --git a/server/src/service/infrastructure/network/PingProvider.ts b/server/src/service/infrastructure/network/PingProvider.ts new file mode 100644 index 000000000..2749584f5 --- /dev/null +++ b/server/src/service/infrastructure/network/PingProvider.ts @@ -0,0 +1,64 @@ +import { PingStatusPayload } from "@/types/network.js"; +import { IStatusProvider } from "./IStatusProvider.js"; +import { MonitorType, Monitor } from "@/types/monitor.js"; +import { MonitorStatusResponse } from "@/types/network.js"; +import { AppError } from "@/utils/AppError.js"; +import ping from "ping"; +import { buildStatusResponse, timeRequest } from "@/service/infrastructure/network/utils.js"; +const SERVICE_NAME = "PingProvider"; + +type Ping = typeof ping; + +export class PingProvider implements IStatusProvider { + readonly type = "ping"; + + constructor(private ping: Ping) {} + + supports(type: MonitorType): boolean { + return type === "ping"; + } + + private sanitizeHost(url: string): string { + return url + .replace(/^https?:\/\//, "") + .replace(/\/.*$/, "") + .replace(/:.*/, ""); + } + + async handle(monitor: Monitor): Promise> { + try { + if (!monitor.url) { + throw new Error("URL is required for ping monitor"); + } + + const sanitizedHost = this.sanitizeHost(monitor.url); + const { response, error } = await timeRequest(() => this.ping.promise.probe(sanitizedHost)); + const safeTime = typeof response?.time === "number" ? response.time : parseFloat(String(response?.time)) || 0; + if (error) { + throw error; + } + + if (!response) { + throw new Error(`No response from ping for host: ${sanitizedHost}`); + } + + return buildStatusResponse({ + monitor, + payload: response, + overrides: { + status: response.alive ?? false, + code: 200, + message: "Success", + responseTime: safeTime, + }, + }); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + throw new AppError({ + message, + service: SERVICE_NAME, + method: "handle", + }); + } + } +} diff --git a/server/src/service/infrastructure/network/utils.ts b/server/src/service/infrastructure/network/utils.ts new file mode 100644 index 000000000..c70f06bf8 --- /dev/null +++ b/server/src/service/infrastructure/network/utils.ts @@ -0,0 +1,85 @@ +import { HTTPError, RequestError } from "got"; +import type { Response } from "got"; +import type { Monitor } from "@/types/monitor.js"; +import { MonitorStatusResponse, MonitorStatusResponseOverrides } from "@/types/network.js"; + +export const timeRequest = async (operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> => { + const start = process.hrtime.bigint(); + try { + const response = await operation(); + const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response, responseTime: elapsedMs, error: null }; + } catch (error) { + const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response: null, responseTime: elapsedMs, error }; + } +}; + +export const NETWORK_ERROR = 5000; +export const PING_ERROR = 5001; + +interface BuildStatusResponseArgs { + monitor: Monitor; + response?: Response | null; + error?: Error | RequestError | HTTPError | null; + payload?: T | null; + jsonPath?: string; + matchMethod?: MonitorStatusResponse["matchMethod"]; + expectedValue?: string; + extracted?: unknown; + overrides?: MonitorStatusResponseOverrides; +} + +export const buildStatusResponse = ({ + monitor, + response, + error, + payload, + jsonPath, + matchMethod, + expectedValue, + extracted, + overrides, +}: BuildStatusResponseArgs): MonitorStatusResponse => { + if (error) { + const statusResponse: MonitorStatusResponse = { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: NETWORK_ERROR, + message: error.message ?? "Network error", + responseTime: 0, + timings: undefined, + jsonPath, + matchMethod, + expectedValue, + extracted, + payload, + }; + + if (error instanceof HTTPError || error instanceof RequestError) { + statusResponse.code = error?.response?.statusCode ?? NETWORK_ERROR; + statusResponse.message = error.message; + statusResponse.responseTime = error.timings?.phases?.total ?? 0; + statusResponse.timings = error.timings; + } + return { ...statusResponse, ...(overrides ?? {}) }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response?.ok ?? false, + code: response?.statusCode ?? NETWORK_ERROR, + message: response?.statusMessage ?? "", + responseTime: response?.timings?.phases?.total ?? 0, + timings: response?.timings, + payload: payload ?? response?.body, + jsonPath, + matchMethod, + expectedValue, + extracted, + }; +}; diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 702affed2..2515b6145 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -16,14 +16,14 @@ import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; import path from "path"; import { fileURLToPath } from "url"; +import { MonitorStatusResponseOverrides } from "@/types/index.js"; import CacheableLookup from "cacheable-lookup"; import { ISettingsService } from "../system/settingsService.js"; import { ILogger } from "@/utils/logger.js"; +import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; -type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; - interface BuildStatusResponseArgs { monitor: Monitor; response?: Response | null; @@ -94,6 +94,10 @@ class NetworkService implements INetworkService { private grpc: typeof import("@grpc/grpc-js"); private protoLoader: typeof import("@grpc/proto-loader"); + // New providers + private pingProvider; + private httpProvider; + private buildStatusResponse = ({ monitor, response, @@ -160,7 +164,11 @@ class NetworkService implements INetworkService { net: typeof import("net"), settingsService: ISettingsService, grpc: typeof import("@grpc/grpc-js"), - protoLoader: typeof import("@grpc/proto-loader") + protoLoader: typeof import("@grpc/proto-loader"), + + // New providers + pingProvider: IStatusProvider, + httpProvider: IStatusProvider ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -185,6 +193,10 @@ class NetworkService implements INetworkService { this.grpc = grpc; this.protoLoader = protoLoader; + // New providers + this.pingProvider = pingProvider; + this.httpProvider = httpProvider; + const cacheable = new CacheableLookup(); this.got = got.extend({ @@ -231,9 +243,9 @@ class NetworkService implements INetworkService { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: - return await this.requestPing(monitor); + return await this.pingProvider.handle(monitor); case this.TYPE_HTTP: - return await this.requestHttp(monitor); + return await this.httpProvider.handle(monitor); case this.TYPE_PAGESPEED: return await this.requestPageSpeed(monitor); case this.TYPE_HARDWARE: @@ -251,50 +263,6 @@ class NetworkService implements INetworkService { } } - private async requestPing(monitor: Monitor): Promise> { - try { - if (!monitor?.url) { - throw new Error("Monitor URL is required"); - } - - const rawUrl = monitor.url; - const sanitizedHost = rawUrl - .replace(/^https?:\/\//, "") - .replace(/\/.*$/, "") - .replace(/:.*/, ""); - const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost)); - - if (!response) { - if (error) { - throw error; - } - throw new Error("Ping failed - no result returned"); - } - - const pingResponse = this.buildStatusResponse({ - monitor, - payload: response as PingStatusPayload, - overrides: { - status: (response as { alive?: boolean })?.alive ?? false, - code: 200, - message: "Success", - responseTime: (response as { time?: number })?.time ?? 0, - payload: response as PingStatusPayload, - } as MonitorStatusResponseOverrides, - }); - - return pingResponse; - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing ping", - service: this.SERVICE_NAME, - method: "requestPing", - details: { url: monitor.url }, - }); - } - } - private async requestHttp(monitor: Monitor): Promise> { const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; const httpResponse = this.buildStatusResponse({ diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 2018523cd..083193e06 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -49,7 +49,7 @@ export interface PingStatusPayload { host: string; numeric_host?: string; alive: boolean; - time: number; + time: number | unknown; times?: number[]; output?: string; min?: string; @@ -134,3 +134,5 @@ export type StatusChangeResult = { temp: boolean; }; }; + +export type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; From 8dd846d6cddf4e0365ed60a8c4f8255235cbc634 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Mar 2026 23:36:36 +0000 Subject: [PATCH 09/53] harden http provider --- .../infrastructure/network/HttpProvider.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index 84f290b16..b47160e06 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -47,7 +47,12 @@ export class HttpProvider implements IStatusProvider { } async handle(monitor: Monitor): Promise> { - const { url, secret, ignoreTlsErrors } = monitor; + const { url, secret, jsonPath, ignoreTlsErrors } = monitor; + + if (!url) { + throw new Error("URL is required for HTTP monitor"); + } + const options: Record = { headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined, }; @@ -58,9 +63,24 @@ export class HttpProvider implements IStatusProvider { try { const response = await this.got(url, options); - let payload: T; - const isJson = response.headers["content-type"]?.includes("application/json"); + const contentType = response.headers["content-type"] || ""; + const isJson = contentType.includes("application/json"); + if (jsonPath && !isJson) { + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: response.statusCode, + message: "Response is not JSON", + responseTime: response.timings.phases.total ?? 0, + timings: response.timings, + payload: response.body as unknown as T, + }; + } + + let payload: T; if (isJson) { try { payload = JSON.parse(response.body) as T; @@ -83,9 +103,6 @@ export class HttpProvider implements IStatusProvider { timings: response.timings, payload, extracted: matchResult.extracted, - jsonPath: monitor.jsonPath, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, }; } catch (error: unknown) { return this.handleHttpError(error, monitor); From a1068f1d7ed6648fb89bda756fa7134fd305222e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 4 Mar 2026 19:56:54 -0500 Subject: [PATCH 10/53] refactor: move dark background colors to colors object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback — define gray900 and gray850 in the colors object and reference them in the dark palette. --- client/src/Utils/Theme/Palette.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index 01a9a704b..9c19f0cbf 100644 --- a/client/src/Utils/Theme/Palette.ts +++ b/client/src/Utils/Theme/Palette.ts @@ -12,6 +12,8 @@ export const typographyLevels = { export const colors = { gray200: "#EFEFEF", gray700: "#313131", + gray900: "#151518", + gray850: "#1c1c21", blueBlueWave: "#1570EF", }; @@ -32,7 +34,7 @@ export const darkPalette = { main: colors.gray700, }, background: { - default: "#151518", - paper: "#1c1c21", + default: colors.gray900, + paper: colors.gray850, }, }; From ec82efebb47bd4a34e89c0acc5700b0f29631c83 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 17:54:16 +0000 Subject: [PATCH 11/53] pagespeed provider, hardware provider --- server/src/config/services.ts | 9 ++- .../network/HardwareProvider.ts | 29 ++++++++++ .../infrastructure/network/HttpProvider.ts | 4 +- .../network/PageSpeedProvider.ts | 55 +++++++++++++++++++ .../service/infrastructure/networkService.ts | 11 +++- 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 server/src/service/infrastructure/network/HardwareProvider.ts create mode 100644 server/src/service/infrastructure/network/PageSpeedProvider.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 485297fe0..11e795feb 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -78,6 +78,8 @@ import { EnvConfig } from "@/service/system/settingsService.js"; import { PingProvider } from "@/service/infrastructure/network/PingProvider.js"; import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; +import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; +import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; export type InitializedServices = { settingsService: any; @@ -151,7 +153,8 @@ export const initializeServices = async ({ const pingProvider = new PingProvider(ping); const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); - + const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); + const hardwareProvider = new HardwareProvider(httpProvider); const networkService = new NetworkService( axios, got, @@ -168,7 +171,9 @@ export const initializeServices = async ({ grpc, protoLoader, pingProvider, - httpProvider + httpProvider, + pageSpeedProvider, + hardwareProvider ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/service/infrastructure/network/HardwareProvider.ts b/server/src/service/infrastructure/network/HardwareProvider.ts new file mode 100644 index 000000000..a9180381e --- /dev/null +++ b/server/src/service/infrastructure/network/HardwareProvider.ts @@ -0,0 +1,29 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; +import { AppError } from "@/utils/AppError.js"; + +export class HardwareProvider implements IStatusProvider { + readonly type = "hardware"; + constructor(private httpProvider: HttpProvider) {} + + supports(type: MonitorType) { + return type === "hardware"; + } + + async handle(monitor: Monitor): Promise> { + const { url } = monitor; + try { + if (!url) throw new Error("URL is required for Hardware monitor"); + return await this.httpProvider.handle(monitor); + } catch (err: unknown) { + throw new AppError({ + message: err instanceof Error ? err.message : "Error performing Hardware request", + service: "HardwareProvider", + method: "handle", + details: { url: monitor.url }, + }); + } + } +} diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index b47160e06..e373e2cff 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -4,7 +4,7 @@ import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvide import { HttpStatusPayload } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { Agent as HttpsAgent } from "https"; -import { Monitor } from "@/types/monitor.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; export class HttpProvider implements IStatusProvider { @@ -15,7 +15,7 @@ export class HttpProvider implements IStatusProvider { private advancedMatcher: AdvancedMatcher ) {} - supports(type: string) { + supports(type: MonitorType) { return type === "http"; } diff --git a/server/src/service/infrastructure/network/PageSpeedProvider.ts b/server/src/service/infrastructure/network/PageSpeedProvider.ts new file mode 100644 index 000000000..121fe9bf8 --- /dev/null +++ b/server/src/service/infrastructure/network/PageSpeedProvider.ts @@ -0,0 +1,55 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; +import { ISettingsService } from "@/service/system/settingsService.js"; +import { ILogger } from "@/utils/logger.js"; +import { AppError } from "@/utils/AppError.js"; + +export class PageSpeedProvider implements IStatusProvider { + readonly type = "pagespeed"; + constructor( + private httpProvider: HttpProvider, + private settingsService: ISettingsService, + private logger: ILogger + ) {} + + supports(type: MonitorType) { + return type === "pagespeed"; + } + + async handle(monitor: Monitor): Promise> { + const { url } = monitor; + try { + if (!url) throw new Error("URL is required for PageSpeed monitor"); + const dbSettings = await this.settingsService.getDBSettings(); + const apiKey = dbSettings?.pagespeedApiKey; + let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent( + url + )}&category=seo&category=accessibility&category=best-practices&category=performance`; + + if (apiKey) { + pageSpeedUrl += `&key=${apiKey}`; + } else { + this.logger.warn({ + message: "PageSpeed API key not found, performance may be throttled", + service: "PageSpeedProvider", + method: "handle", + details: { url }, + }); + } + + return await this.httpProvider.handle({ + ...monitor, + url: pageSpeedUrl, + }); + } catch (err: unknown) { + throw new AppError({ + message: err instanceof Error ? err.message : "Error performing PageSpeed request", + service: "PageSpeedProvider", + method: "handle", + details: { url: monitor.url }, + }); + } + } +} diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 2515b6145..539a1f8c2 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -97,6 +97,8 @@ class NetworkService implements INetworkService { // New providers private pingProvider; private httpProvider; + private pageSpeedProvider; + private hardwareProvider; private buildStatusResponse = ({ monitor, @@ -168,7 +170,9 @@ class NetworkService implements INetworkService { // New providers pingProvider: IStatusProvider, - httpProvider: IStatusProvider + httpProvider: IStatusProvider, + pagespeedProvider: IStatusProvider, + hardwareProvider: IStatusProvider ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -196,7 +200,8 @@ class NetworkService implements INetworkService { // New providers this.pingProvider = pingProvider; this.httpProvider = httpProvider; - + this.pageSpeedProvider = pagespeedProvider; + this.hardwareProvider = hardwareProvider; const cacheable = new CacheableLookup(); this.got = got.extend({ @@ -247,7 +252,7 @@ class NetworkService implements INetworkService { case this.TYPE_HTTP: return await this.httpProvider.handle(monitor); case this.TYPE_PAGESPEED: - return await this.requestPageSpeed(monitor); + return await this.pageSpeedProvider.handle(monitor); case this.TYPE_HARDWARE: return await this.requestHardware(monitor); case this.TYPE_DOCKER: From f3b156f2d0c02c19983c65b6eb8b759aa6919a42 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 18:19:03 +0000 Subject: [PATCH 12/53] docker provider --- server/src/config/services.ts | 6 +- .../infrastructure/network/DockerProvider.ts | 160 ++++++++++++++++++ .../service/infrastructure/networkService.ts | 9 +- 3 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 server/src/service/infrastructure/network/DockerProvider.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 11e795feb..eed243817 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -80,6 +80,7 @@ import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; +import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; export type InitializedServices = { settingsService: any; @@ -155,6 +156,8 @@ export const initializeServices = async ({ const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); const hardwareProvider = new HardwareProvider(httpProvider); + const dockerProvider = new DockerProvider(logger, Docker); + const networkService = new NetworkService( axios, got, @@ -173,7 +176,8 @@ export const initializeServices = async ({ pingProvider, httpProvider, pageSpeedProvider, - hardwareProvider + hardwareProvider, + dockerProvider ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/service/infrastructure/network/DockerProvider.ts b/server/src/service/infrastructure/network/DockerProvider.ts new file mode 100644 index 000000000..8e84b5dc6 --- /dev/null +++ b/server/src/service/infrastructure/network/DockerProvider.ts @@ -0,0 +1,160 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { ILogger } from "@/utils/logger.js"; +import { AppError } from "@/utils/AppError.js"; +import Dockerode from "dockerode"; +import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js"; + +type DockerodeType = typeof Dockerode; + +const SERVICE_NAME = "DockerProvider"; + +export interface DockerError extends Error { + statusCode?: number; + reason?: string; + json?: { message?: string }; +} + +export class DockerProvider implements IStatusProvider { + readonly type = "docker"; + private docker: Dockerode; + constructor( + private logger: ILogger, + private DockerLib: DockerodeType + ) { + this.docker = new this.DockerLib(); + } + + supports(type: MonitorType): boolean { + return type === "docker"; + } + + private isDockerError(error: unknown): error is DockerError { + return error instanceof Error && ("statusCode" in error || "reason" in error || "json" in error); + } + + async handle(monitor: Monitor): Promise> { + const { url: containerInput } = monitor; + try { + if (!containerInput) { + throw new Error("Container name or ID is required for Docker monitor"); + } + + const containers = await this.docker.listContainers({ all: true }); + const normalizedInput = containerInput.replace(/^\/+/, "").toLowerCase(); + + // Priority-based matching to avoid ambiguity: + // 1. Exact full ID match (64-char) + const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); + + // 2. Exact container name match (case-insensitive) + const exactNameMatch = containers.find((c) => + c.Names.some((name: string) => { + const cleanName = name.replace(/^\/+/, "").toLowerCase(); + return cleanName === normalizedInput; + }) + ); + + // 3. Partial ID match (fallback for backwards compatibility) + const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); + + // Select container based on priority + const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; + + // Handle no match + if (!targetContainer) { + this.logger.warn({ + message: `No container found for "${monitor.url}".`, + service: SERVICE_NAME, + method: "handle", + details: { url: monitor.url }, + }); + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: 404, + message: "Docker container not found", + responseTime: 0, + payload: null, + }; + } + + // 5. Handle Ambiguity Check + const matchTypes: string[] = []; + if (exactIdMatch) matchTypes.push("exact ID"); + if (exactNameMatch) matchTypes.push("exact name"); + if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID"); + + if (matchTypes.length > 1) { + const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`; + + this.logger.warn({ + message, + service: SERVICE_NAME, + method: "handle", + details: { url: containerInput }, + }); + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: NETWORK_ERROR, + message, + responseTime: 0, + payload: null, + }; + } + + // 6. Inspect Container Status + const container = this.docker.getContainer(targetContainer.Id); + const { response, responseTime, error } = await timeRequest(() => container.inspect()); + + if (error) { + let message = "Failed to fetch Docker container information"; + let code = NETWORK_ERROR; + + if (this.isDockerError(error)) { + code = error.statusCode ?? NETWORK_ERROR; + message = error.json?.message ?? error.reason ?? error.message; + } else if (error instanceof Error) { + message = error.message; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: code, + message: message, + responseTime, + payload: null, + }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response?.State?.Status === "running", + code: 200, + message: "Docker container status fetched successfully", + responseTime, + payload: response as unknown as DockerStatusPayload, + }; + } catch (err: unknown) { + throw new AppError({ + message: err instanceof Error ? err.message : "Error performing Docker request", + service: SERVICE_NAME, + method: "handle", + details: { url: containerInput }, + }); + } + } +} diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 539a1f8c2..c79a2cee6 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -99,6 +99,7 @@ class NetworkService implements INetworkService { private httpProvider; private pageSpeedProvider; private hardwareProvider; + private dockerProvider; private buildStatusResponse = ({ monitor, @@ -172,7 +173,8 @@ class NetworkService implements INetworkService { pingProvider: IStatusProvider, httpProvider: IStatusProvider, pagespeedProvider: IStatusProvider, - hardwareProvider: IStatusProvider + hardwareProvider: IStatusProvider, + dockerProvider: IStatusProvider ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -202,6 +204,7 @@ class NetworkService implements INetworkService { this.httpProvider = httpProvider; this.pageSpeedProvider = pagespeedProvider; this.hardwareProvider = hardwareProvider; + this.dockerProvider = dockerProvider; const cacheable = new CacheableLookup(); this.got = got.extend({ @@ -254,9 +257,9 @@ class NetworkService implements INetworkService { case this.TYPE_PAGESPEED: return await this.pageSpeedProvider.handle(monitor); case this.TYPE_HARDWARE: - return await this.requestHardware(monitor); + return await this.hardwareProvider.handle(monitor); case this.TYPE_DOCKER: - return await this.requestDocker(monitor); + return await this.dockerProvider.handle(monitor); case this.TYPE_PORT: return await this.requestPort(monitor); case this.TYPE_GAME: From 80603c9a4214012c7092cf7869c0e96eaa507da8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 18:30:06 +0000 Subject: [PATCH 13/53] port provider --- .../infrastructure/network/PortProvider.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 server/src/service/infrastructure/network/PortProvider.ts diff --git a/server/src/service/infrastructure/network/PortProvider.ts b/server/src/service/infrastructure/network/PortProvider.ts new file mode 100644 index 000000000..bacbd19ff --- /dev/null +++ b/server/src/service/infrastructure/network/PortProvider.ts @@ -0,0 +1,98 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js"; +import * as net from "net"; +type NetType = typeof net; + +const SERVICE_NAME = "PortProvider"; + +export class PortProvider implements IStatusProvider { + readonly type = "port"; + + constructor(private net: NetType) {} + + supports(type: MonitorType): boolean { + return type === "port"; + } + + async handle(monitor: Monitor): Promise> { + try { + const { url, port } = monitor; + if (!url || !port) { + throw new Error("URL and port are required for port monitoring"); + } + + const { responseTime, error } = await timeRequest(async () => { + return new Promise((resolve, reject) => { + const socket = this.net.createConnection( + { + host: url, + port, + }, + () => { + socket.end(); + socket.destroy(); + resolve({ success: true }); + } + ); + + socket.setTimeout(5000); + socket.on("timeout", () => { + socket.destroy(); + reject(new Error("Connection timeout")); + }); + + socket.on("error", (err: unknown) => { + socket.destroy(); + reject(err); + }); + }); + }); + if (error) { + const errorMessage = error instanceof Error ? error.message : "Port check failed"; + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: NETWORK_ERROR, + message: errorMessage, + responseTime: responseTime, + timings: undefined, + payload: { success: false }, + jsonPath: monitor.jsonPath, + matchMethod: monitor.matchMethod, + expectedValue: monitor.expectedValue, + extracted: undefined, + }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: true, + code: 200, + message: "Port check successful", + responseTime: responseTime, + timings: undefined, + payload: { success: true }, + jsonPath: monitor.jsonPath, + matchMethod: monitor.matchMethod, + expectedValue: monitor.expectedValue, + extracted: undefined, + }; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing port check", + status: 500, + service: SERVICE_NAME, + method: "requestPort", + details: { url: monitor.url, port: monitor.port }, + }); + } + } +} From 49672db563524412ccd49d22ff710f1b8605b61a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 18:35:19 +0000 Subject: [PATCH 14/53] port provider --- server/src/config/services.ts | 5 ++++- .../src/service/infrastructure/network/PortProvider.ts | 10 +--------- server/src/service/infrastructure/networkService.ts | 7 +++++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index eed243817..1d8558f9c 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -81,6 +81,7 @@ import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatche import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; +import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; export type InitializedServices = { settingsService: any; @@ -157,6 +158,7 @@ export const initializeServices = async ({ const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); const hardwareProvider = new HardwareProvider(httpProvider); const dockerProvider = new DockerProvider(logger, Docker); + const portProvider = new PortProvider(net); const networkService = new NetworkService( axios, @@ -177,7 +179,8 @@ export const initializeServices = async ({ httpProvider, pageSpeedProvider, hardwareProvider, - dockerProvider + dockerProvider, + portProvider ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/service/infrastructure/network/PortProvider.ts b/server/src/service/infrastructure/network/PortProvider.ts index bacbd19ff..7c1c96ce4 100644 --- a/server/src/service/infrastructure/network/PortProvider.ts +++ b/server/src/service/infrastructure/network/PortProvider.ts @@ -62,10 +62,6 @@ export class PortProvider implements IStatusProvider { responseTime: responseTime, timings: undefined, payload: { success: false }, - jsonPath: monitor.jsonPath, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - extracted: undefined, }; } @@ -79,10 +75,6 @@ export class PortProvider implements IStatusProvider { responseTime: responseTime, timings: undefined, payload: { success: true }, - jsonPath: monitor.jsonPath, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - extracted: undefined, }; } catch (err: unknown) { const originalMessage = err instanceof Error ? err.message : String(err); @@ -90,7 +82,7 @@ export class PortProvider implements IStatusProvider { message: originalMessage || "Error performing port check", status: 500, service: SERVICE_NAME, - method: "requestPort", + method: "handle", details: { url: monitor.url, port: monitor.port }, }); } diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index c79a2cee6..f535eae6c 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -100,6 +100,7 @@ class NetworkService implements INetworkService { private pageSpeedProvider; private hardwareProvider; private dockerProvider; + private portProvider; private buildStatusResponse = ({ monitor, @@ -174,7 +175,8 @@ class NetworkService implements INetworkService { httpProvider: IStatusProvider, pagespeedProvider: IStatusProvider, hardwareProvider: IStatusProvider, - dockerProvider: IStatusProvider + dockerProvider: IStatusProvider, + portProvider: IStatusProvider ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -205,6 +207,7 @@ class NetworkService implements INetworkService { this.pageSpeedProvider = pagespeedProvider; this.hardwareProvider = hardwareProvider; this.dockerProvider = dockerProvider; + this.portProvider = portProvider; const cacheable = new CacheableLookup(); this.got = got.extend({ @@ -261,7 +264,7 @@ class NetworkService implements INetworkService { case this.TYPE_DOCKER: return await this.dockerProvider.handle(monitor); case this.TYPE_PORT: - return await this.requestPort(monitor); + return await this.portProvider.handle(monitor); case this.TYPE_GAME: return await this.requestGame(monitor); case this.TYPE_GRPC: From 190d9dd2b1ae829db40391e2fd5816c9293cb74b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 18:45:27 +0000 Subject: [PATCH 15/53] gameProvider --- .../infrastructure/network/GameProvider.ts | 78 +++++++++++++++++++ .../infrastructure/network/PortProvider.ts | 1 + server/src/types/network.ts | 4 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 server/src/service/infrastructure/network/GameProvider.ts diff --git a/server/src/service/infrastructure/network/GameProvider.ts b/server/src/service/infrastructure/network/GameProvider.ts new file mode 100644 index 000000000..e2ace7433 --- /dev/null +++ b/server/src/service/infrastructure/network/GameProvider.ts @@ -0,0 +1,78 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { GameDig } from "gamedig"; +import { AppError } from "@/utils/AppError.js"; +import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; +import { ILogger } from "@/utils/logger.js"; + +type GameDigType = typeof GameDig; + +const SERVICE_NAME = "GameProvider"; +export class GameProvider implements IStatusProvider { + readonly type = "game"; + constructor( + private logger: ILogger, + private gameDig: GameDigType + ) {} + + supports(type: MonitorType): boolean { + return type === "game"; + } + + async handle(monitor: Monitor): Promise> { + const { url, port, gameId, id, teamId, type } = monitor; + + // Clean the host in case a full URL was passed + const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0] || ""; + try { + const state = await this.gameDig + .query({ + type: gameId ?? "unknown", + host: host, + port: port ?? 0, + }) + .catch((error: unknown) => { + this.logger.warn({ + message: error instanceof Error ? error.message : String(error), + service: SERVICE_NAME, + method: "handle", + details: { host, port, gameId }, + }); + return undefined; + }); + + if (!state) { + return { + monitorId: id, + teamId: teamId, + type: type, + status: false, + code: NETWORK_ERROR, + message: "No response from game server", + responseTime: 0, + payload: null, + }; + } + + return { + monitorId: id, + teamId: teamId, + type: type, + status: true, + code: 200, + message: "Success", + responseTime: state.ping ?? 0, + payload: state, + }; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing game server check", + service: SERVICE_NAME, + method: "handle", + details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId }, + }); + } + } +} diff --git a/server/src/service/infrastructure/network/PortProvider.ts b/server/src/service/infrastructure/network/PortProvider.ts index 7c1c96ce4..66fefd0fe 100644 --- a/server/src/service/infrastructure/network/PortProvider.ts +++ b/server/src/service/infrastructure/network/PortProvider.ts @@ -51,6 +51,7 @@ export class PortProvider implements IStatusProvider { }); }); if (error) { + console.log(error); const errorMessage = error instanceof Error ? error.message : "Port check failed"; return { monitorId: monitor.id, diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 083193e06..93ddc4ac9 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -14,6 +14,8 @@ import type { MonitorStatus, } from "@/types/index.js"; +import type { QueryResult } from "gamedig"; + export interface MonitorStatusResponse< T = | HttpStatusPayload @@ -100,7 +102,7 @@ export interface PortStatusPayload { success: boolean; } -export type GameStatusPayload = Record; +export type GameStatusPayload = QueryResult; export interface GrpcStatusPayload { grpcStatusCode: number; From 27c44a0fbd69c55a4a7de994ca945972037d34e0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 18:48:19 +0000 Subject: [PATCH 16/53] config --- server/src/config/services.ts | 5 +- .../service/infrastructure/networkService.ts | 54 ++----------------- 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 1d8558f9c..953d89b0f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -82,6 +82,7 @@ import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedPro import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; +import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; export type InitializedServices = { settingsService: any; @@ -159,6 +160,7 @@ export const initializeServices = async ({ const hardwareProvider = new HardwareProvider(httpProvider); const dockerProvider = new DockerProvider(logger, Docker); const portProvider = new PortProvider(net); + const gameProvider = new GameProvider(logger, GameDig); const networkService = new NetworkService( axios, @@ -180,7 +182,8 @@ export const initializeServices = async ({ pageSpeedProvider, hardwareProvider, dockerProvider, - portProvider + portProvider, + gameProvider ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index f535eae6c..d727345da 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -101,6 +101,7 @@ class NetworkService implements INetworkService { private hardwareProvider; private dockerProvider; private portProvider; + private gameProvider; private buildStatusResponse = ({ monitor, @@ -176,7 +177,8 @@ class NetworkService implements INetworkService { pagespeedProvider: IStatusProvider, hardwareProvider: IStatusProvider, dockerProvider: IStatusProvider, - portProvider: IStatusProvider + portProvider: IStatusProvider, + gameProvider: IStatusProvider ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -208,6 +210,7 @@ class NetworkService implements INetworkService { this.hardwareProvider = hardwareProvider; this.dockerProvider = dockerProvider; this.portProvider = portProvider; + this.gameProvider = gameProvider; const cacheable = new CacheableLookup(); this.got = got.extend({ @@ -266,7 +269,7 @@ class NetworkService implements INetworkService { case this.TYPE_PORT: return await this.portProvider.handle(monitor); case this.TYPE_GAME: - return await this.requestGame(monitor); + return await this.gameProvider.handle(monitor); case this.TYPE_GRPC: return await this.requestGrpc(monitor); default: @@ -628,53 +631,6 @@ class NetworkService implements INetworkService { } } - private async requestGame(monitor: Monitor): Promise> { - try { - const { url, port, gameId } = monitor; - - const gameResponse = this.buildStatusResponse({ - monitor, - overrides: { - code: 200, - status: true, - message: "Success", - }, - }); - - const state = await this.GameDig.query({ - type: gameId ?? "unknown", - host: url, - port: port ?? 0, - }).catch((error: unknown): undefined => { - this.logger.warn({ - message: error instanceof Error ? error.message : String(error), - service: this.SERVICE_NAME, - method: "requestGame", - details: { url, port, gameId }, - }); - return undefined; - }); - - if (!state) { - gameResponse.code = this.NETWORK_ERROR; - gameResponse.status = false; - gameResponse.message = "No response"; - return gameResponse; - } - - gameResponse.responseTime = state.ping ?? 0; - gameResponse.payload = state as GameStatusPayload; - return gameResponse; - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing game server check", - service: this.SERVICE_NAME, - method: "requestGame", - details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId }, - }); - } - } private async requestGrpc(monitor: Monitor): Promise> { try { const { url, port, ignoreTlsErrors } = monitor; From c7689cc64a691f71d1fb06d5a5ce4085f7d1c255 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 19:18:29 +0000 Subject: [PATCH 17/53] grpc --- .../infrastructure/network/GrpcProvider.ts | 187 +++++++++ .../service/infrastructure/networkService.ts | 354 ------------------ 2 files changed, 187 insertions(+), 354 deletions(-) create mode 100644 server/src/service/infrastructure/network/GrpcProvider.ts diff --git a/server/src/service/infrastructure/network/GrpcProvider.ts b/server/src/service/infrastructure/network/GrpcProvider.ts new file mode 100644 index 000000000..4bf127785 --- /dev/null +++ b/server/src/service/infrastructure/network/GrpcProvider.ts @@ -0,0 +1,187 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js"; +import { ILogger } from "@/utils/logger.js"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; + +import path from "path"; +import { fileURLToPath } from "url"; +import { Network } from "inspector/promises"; + +type GrpcType = typeof grpc; +type ProtoLoaderType = typeof protoLoader; + +const SERVICE_NAME = "GrpcProvider"; + +export interface GrpcError extends Error { + code: number; + details?: string; +} + +export class GrpcProvider implements IStatusProvider { + readonly type = "grpc"; + constructor( + private logger: ILogger, + private grpc: GrpcType, + private protoLoader: ProtoLoaderType + ) {} + + supports(type: MonitorType): boolean { + return type === "grpc"; + } + + private isGrpcError(error: unknown): error is GrpcError { + return error instanceof Error && "code" in error && typeof (error as Record).code === "number"; + } + + private getGrpcStatusName(code: number): string { + const statusNames: Record = { + 0: "OK", + 1: "CANCELLED", + 2: "UNKNOWN", + 3: "INVALID_ARGUMENT", + 4: "DEADLINE_EXCEEDED", + 5: "NOT_FOUND", + 6: "ALREADY_EXISTS", + 7: "PERMISSION_DENIED", + 8: "RESOURCE_EXHAUSTED", + 9: "FAILED_PRECONDITION", + 10: "ABORTED", + 11: "OUT_OF_RANGE", + 12: "UNIMPLEMENTED", + 13: "INTERNAL", + 14: "UNAVAILABLE", + 15: "DATA_LOSS", + 16: "UNAUTHENTICATED", + }; + return statusNames[code] || "UNKNOWN"; + } + + async handle(monitor: Monitor): Promise> { + const { id, teamId, type, url, port, ignoreTlsErrors } = monitor; + const grpcServiceName = monitor.grpcServiceName || ""; + const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0]; + + try { + if (!host || !port) { + throw new Error("Valid host and port are required"); + } + const target = `${host}:${port}`; + const currentFilePath = fileURLToPath(import.meta.url); + const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto"); + const packageDefinition = this.protoLoader.loadSync(protoPath, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + }); + const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as any; + const HealthClient = grpcObject.grpc.health.v1.Health; + let credentials; + if (ignoreTlsErrors) { + credentials = this.grpc.credentials.createSsl(null, null, null, { + checkServerIdentity: () => undefined, + }); + } else { + credentials = this.grpc.credentials.createInsecure(); + } + const client = new HealthClient(target, credentials); + const deadline = new Date(Date.now() + 10000); // 10s timeout + const { response, responseTime, error } = await timeRequest(() => { + return new Promise((resolve, reject) => { + client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => { + client.close(); + + if (err) { + const grpcErr = err as { code?: number; details?: string; message?: string }; + const payload: GrpcStatusPayload = { + grpcStatusCode: grpcErr.code ?? -1, + grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1), + serviceName: grpcServiceName, + servingStatus: "UNKNOWN", + }; + const grpcError = new AppError({ + message: grpcErr.details || grpcErr.message || "gRPC error", + service: SERVICE_NAME, + method: "handle", + }) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; + grpcError.grpcPayload = payload; + grpcError.grpcCode = grpcErr.code; + reject(grpcError); + return; + } + + const resp = response as { status?: string } | undefined; + const servingStatus = resp?.status ?? "UNKNOWN"; + resolve({ + grpcStatusCode: 0, + grpcStatusName: "OK", + serviceName: grpcServiceName, + servingStatus, + }); + }); + }); + }); + + if (error) { + let grpcCode = -1; + let message = error instanceof Error ? error.message : "gRPC health check failed"; + + if (this.isGrpcError(error)) { + grpcCode = error.code; + message = error.details || error.message; + } + + return { + monitorId: id, + teamId: teamId, + type: type, + status: false, + code: 500, + message: message, + responseTime, + payload: { + grpcStatusCode: grpcCode, + grpcStatusName: this.getGrpcStatusName(grpcCode), + serviceName: grpcServiceName, + servingStatus: "UNKNOWN", + }, + }; + } + + if (!response) { + throw new Error("No response received from gRPC health check"); + } + + const isServing = response.servingStatus === "SERVING"; + + return { + monitorId: id, + teamId: teamId, + type: type, + status: isServing, + code: isServing ? 200 : NETWORK_ERROR, + message: isServing ? "gRPC health check successful" : "gRPC health check failed", + responseTime, + payload: { + grpcStatusCode: isServing ? 200 : NETWORK_ERROR, + grpcStatusName: response.grpcStatusName, + serviceName: response.serviceName, + servingStatus: response.servingStatus || "UNKNOWN", + }, + }; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing gRPC health check", + service: SERVICE_NAME, + method: "handle", + details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName }, + }); + } + } +} diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index d727345da..1a0d71553 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -277,360 +277,6 @@ class NetworkService implements INetworkService { } } - private async requestHttp(monitor: Monitor): Promise> { - const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse = this.buildStatusResponse({ - monitor, - overrides: { - status: false, - code: this.NETWORK_ERROR, - message: "Request not executed", - }, - }); - - try { - if (!url) { - throw new Error("Monitor URL is required"); - } - const config: Record = { - headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, - }; - - if (ignoreTlsErrors) { - config.agent = { - https: new this.https.Agent({ - rejectUnauthorized: false, - }), - }; - } - - const response = await this.got(url, config); - - let payload: T | string | undefined; - const contentType = response.headers["content-type"]; - - if (contentType && contentType.includes("application/json")) { - try { - payload = JSON.parse(response.body); - } catch { - payload = response.body; - } - } else { - payload = response.body; - } - - Object.assign(httpResponse, { - code: response.statusCode, - status: response.ok, - message: response.statusMessage ?? "", - responseTime: response.timings.phases.total || 0, - payload, - timings: response.timings, - }); - - if (!useAdvancedMatching) { - return httpResponse; - } - - if (expectedValue && !jsonPath) { - let ok = false; - if (matchMethod === "equal") ok = payload === expectedValue; - if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue); - if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload); - - if (ok === true) { - return httpResponse; - } else { - httpResponse.code = 500; - httpResponse.status = false; - httpResponse.message = "Expected value did not match"; - return httpResponse; - } - } - - if (jsonPath) { - const contentType = response.headers["content-type"]; - const isJson = contentType?.includes("application/json"); - if (!isJson) { - httpResponse.status = false; - httpResponse.message = "Response is not JSON"; - return httpResponse; - } - try { - const extracted = this.jmespath.search(payload, jsonPath); - if (expectedValue) { - let ok = false; - if (matchMethod === "equal") ok = extracted === expectedValue; - if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue); - if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted); - - if (ok) { - httpResponse.extracted = extracted; - return httpResponse; - } else { - httpResponse.status = false; - httpResponse.code = 500; - httpResponse.message = "Expected value did not match"; - httpResponse.extracted = extracted; - return httpResponse; - } - } else { - const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null; - if (!isFalsey) { - httpResponse.extracted = extracted; - return httpResponse; - } else { - httpResponse.status = false; - httpResponse.code = 500; - httpResponse.message = "Expected value did not match"; - httpResponse.extracted = extracted; - return httpResponse; - } - } - } catch { - httpResponse.status = false; - httpResponse.message = "Error evaluating JSON path"; - return httpResponse; - } - } - return httpResponse; - } catch (err: unknown) { - if (err instanceof HTTPError || err instanceof RequestError) { - httpResponse.code = err.response?.statusCode || this.NETWORK_ERROR; - httpResponse.status = false; - httpResponse.message = String(err.response?.statusCode) || err.message; - httpResponse.responseTime = err.timings?.phases?.total || 0; - httpResponse.payload = null; - httpResponse.timings = err.timings || undefined; - return httpResponse; - } - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing HTTP request", - service: this.SERVICE_NAME, - method: "requestHttp", - details: { url: monitor.url }, - }); - } - } - - private async requestPageSpeed(monitor: Monitor): Promise> { - try { - const url = monitor.url; - if (!url) { - throw new Error("Monitor URL is required"); - } - let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; - const dbSettings = await this.settingsService.getDBSettings(); - if (dbSettings?.pagespeedApiKey) { - pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`; - } else { - this.logger.warn({ - message: "PageSpeed API key not found, job not executed", - service: this.SERVICE_NAME, - method: "requestPagespeed", - details: { url }, - }); - } - return await this.requestHttp({ - ...monitor, - url: pageSpeedUrl, - }); - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing PageSpeed request", - service: this.SERVICE_NAME, - method: "requestPageSpeed", - details: { url: monitor.url }, - }); - } - } - - private async requestHardware(monitor: Monitor): Promise> { - try { - return await this.requestHttp(monitor); - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing hardware request", - service: this.SERVICE_NAME, - method: "requestHardware", - details: { url: monitor.url }, - }); - } - } - - private async requestDocker(monitor: Monitor): Promise> { - try { - if (!monitor.url) { - throw new Error("Monitor URL is required"); - } - - const docker = new this.Docker({ - socketPath: "/var/run/docker.sock", - }); - - const dockerResponse = this.buildStatusResponse({ - monitor, - overrides: { - status: false, - code: this.NETWORK_ERROR, - message: "No response", - }, - }); - - const containers = await docker.listContainers({ all: true }); - - // Normalize input: strip leading slashes and convert to lowercase for comparison - const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase(); - - // Priority-based matching to avoid ambiguity: - // 1. Exact full ID match (64-char) - const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); - - // 2. Exact container name match (case-insensitive) - const exactNameMatch = containers.find((c) => - c.Names.some((name: string) => { - const cleanName = name.replace(/^\/+/, "").toLowerCase(); - return cleanName === normalizedInput; - }) - ); - - // 3. Partial ID match (fallback for backwards compatibility) - const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); - - // Select container based on priority - const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; - - // Return negative response if no container - if (!targetContainer) { - this.logger.warn({ - message: `No container found for "${monitor.url}".`, - service: this.SERVICE_NAME, - method: "requestDocker", - details: { url: monitor.url }, - }); - - dockerResponse.code = 404; - dockerResponse.status = false; - dockerResponse.message = "Docker container not found"; - return dockerResponse; - } - - // Return negative response if ambiguous matches exist - const matchTypes = []; - if (exactIdMatch) matchTypes.push("exact ID"); - if (exactNameMatch) matchTypes.push("exact name"); - if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID"); - - if (matchTypes.length > 1) { - this.logger.warn({ - message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`, - service: this.SERVICE_NAME, - method: "requestDocker", - details: { url: monitor.url }, - }); - dockerResponse.status = false; - dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`; - return dockerResponse; - } - - const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } = - await this.timeRequest(() => container.inspect()); - - dockerResponse.responseTime = responseTime; - dockerResponse.status = response?.State?.Status === "running" ? true : false; - dockerResponse.code = 200; - dockerResponse.message = "Docker container status fetched successfully"; - - if (error) { - dockerResponse.status = false; - dockerResponse.code = - (error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) || - this.NETWORK_ERROR; - dockerResponse.message = - (error && typeof error === "object" && "reason" in error ? (error as { reason?: string }).reason : undefined) || - "Failed to fetch Docker container information"; - return dockerResponse; - } - - return dockerResponse; - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing Docker request", - service: this.SERVICE_NAME, - method: "requestDocker", - details: { url: monitor.url }, - }); - } - } - - private async requestPort(monitor: Monitor): Promise> { - try { - const { url, port } = monitor; - if (!port) { - throw new Error("Port is required for port monitor"); - } - - const { response, responseTime, error } = await this.timeRequest(async () => { - return new Promise((resolve, reject) => { - const socket = this.net.createConnection( - { - host: url, - port, - }, - () => { - socket.end(); - socket.destroy(); - resolve({ success: true }); - } - ); - - socket.setTimeout(5000); - socket.on("timeout", () => { - socket.destroy(); - reject(new Error("Connection timeout")); - }); - - socket.on("error", (err: unknown) => { - socket.destroy(); - reject(err); - }); - }); - }); - - const portResponse = this.buildStatusResponse({ - monitor, - overrides: { - code: 200, - status: (response as { success?: boolean })?.success ?? false, - message: "Port check successful", - responseTime, - }, - }); - - if (error) { - portResponse.code = this.NETWORK_ERROR; - portResponse.status = false; - portResponse.message = "Port check failed"; - return portResponse; - } - - return portResponse; - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing port check", - service: this.SERVICE_NAME, - method: "requestPort", - details: { url: monitor.url, port: monitor.port }, - }); - } - } - private async requestGrpc(monitor: Monitor): Promise> { try { const { url, port, ignoreTlsErrors } = monitor; From ad7d4e54c397ee2c9cfe147e31434c3af00f1c99 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 19:34:57 +0000 Subject: [PATCH 18/53] cleanup --- server/src/config/services.ts | 18 +- .../infrastructure/network/GrpcProvider.ts | 97 +++--- .../infrastructure/network/HttpProvider.ts | 12 +- .../service/infrastructure/networkService.ts | 293 +----------------- 4 files changed, 66 insertions(+), 354 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 953d89b0f..bd13b7dfc 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -29,7 +29,6 @@ import { NotificationMessageBuilder, INotificationMessageBuilder } from "../serv import axios from "axios"; import got from "got"; import ping from "ping"; -import https from "https"; import Docker from "dockerode"; import net from "net"; import fs from "fs"; @@ -83,6 +82,7 @@ import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvi import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; +import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; export type InitializedServices = { settingsService: any; @@ -161,29 +161,19 @@ export const initializeServices = async ({ const dockerProvider = new DockerProvider(logger, Docker); const portProvider = new PortProvider(net); const gameProvider = new GameProvider(logger, GameDig); + const grpcProvider = new GrpcProvider(grpc, protoLoader); const networkService = new NetworkService( axios, - got, - https, - jmespath, - GameDig as unknown as { - query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>; - }, - ping, logger, - Docker, - net, - settingsService, - grpc, - protoLoader, pingProvider, httpProvider, pageSpeedProvider, hardwareProvider, dockerProvider, portProvider, - gameProvider + gameProvider, + grpcProvider ); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/service/infrastructure/network/GrpcProvider.ts b/server/src/service/infrastructure/network/GrpcProvider.ts index 4bf127785..66684ba0c 100644 --- a/server/src/service/infrastructure/network/GrpcProvider.ts +++ b/server/src/service/infrastructure/network/GrpcProvider.ts @@ -2,29 +2,21 @@ import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvide import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { Monitor, MonitorType } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js"; -import { ILogger } from "@/utils/logger.js"; +import { timeRequest } from "@/service/infrastructure/network/utils.js"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import path from "path"; import { fileURLToPath } from "url"; -import { Network } from "inspector/promises"; type GrpcType = typeof grpc; type ProtoLoaderType = typeof protoLoader; const SERVICE_NAME = "GrpcProvider"; -export interface GrpcError extends Error { - code: number; - details?: string; -} - export class GrpcProvider implements IStatusProvider { readonly type = "grpc"; constructor( - private logger: ILogger, private grpc: GrpcType, private protoLoader: ProtoLoaderType ) {} @@ -33,10 +25,6 @@ export class GrpcProvider implements IStatusProvider { return type === "grpc"; } - private isGrpcError(error: unknown): error is GrpcError { - return error instanceof Error && "code" in error && typeof (error as Record).code === "number"; - } - private getGrpcStatusName(code: number): string { const statusNames: Record = { 0: "OK", @@ -61,15 +49,20 @@ export class GrpcProvider implements IStatusProvider { } async handle(monitor: Monitor): Promise> { - const { id, teamId, type, url, port, ignoreTlsErrors } = monitor; - const grpcServiceName = monitor.grpcServiceName || ""; - const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0]; - try { - if (!host || !port) { - throw new Error("Valid host and port are required"); + const { id, teamId, type, url, port, ignoreTlsErrors } = monitor; + const grpcServiceName = monitor.grpcServiceName || ""; + + if (!url) { + throw new AppError({ message: "Monitor host is required", service: SERVICE_NAME, method: "handle" }); } + if (!port) { + throw new AppError({ message: "Monitor port is required", service: SERVICE_NAME, method: "handle" }); + } + + const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0]; const target = `${host}:${port}`; + const currentFilePath = fileURLToPath(import.meta.url); const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto"); const packageDefinition = this.protoLoader.loadSync(protoPath, { @@ -79,8 +72,23 @@ export class GrpcProvider implements IStatusProvider { defaults: true, oneofs: true, }); - const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as any; - const HealthClient = grpcObject.grpc.health.v1.Health; + const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as { + grpc: { + health: { + v1: { + Health: new ( + target: string, + credentials: unknown + ) => { + Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void; + close: () => void; + }; + }; + }; + }; + }; + const healthService = grpcObject.grpc.health.v1.Health; + let credentials; if (ignoreTlsErrors) { credentials = this.grpc.credentials.createSsl(null, null, null, { @@ -89,8 +97,12 @@ export class GrpcProvider implements IStatusProvider { } else { credentials = this.grpc.credentials.createInsecure(); } - const client = new HealthClient(target, credentials); - const deadline = new Date(Date.now() + 10000); // 10s timeout + + const client = new healthService(target, credentials); + + const TIMEOUT_MS = 10000; + const deadline = new Date(Date.now() + TIMEOUT_MS); + const { response, responseTime, error } = await timeRequest(() => { return new Promise((resolve, reject) => { client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => { @@ -128,51 +140,32 @@ export class GrpcProvider implements IStatusProvider { }); if (error) { - let grpcCode = -1; - let message = error instanceof Error ? error.message : "gRPC health check failed"; - - if (this.isGrpcError(error)) { - grpcCode = error.code; - message = error.details || error.message; - } - + const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; + const payload = grpcError.grpcPayload; return { monitorId: id, teamId: teamId, type: type, status: false, - code: 500, - message: message, + code: grpcError.grpcCode ?? 5000, + message: grpcError.message ?? "gRPC health check failed", responseTime, - payload: { - grpcStatusCode: grpcCode, - grpcStatusName: this.getGrpcStatusName(grpcCode), - serviceName: grpcServiceName, - servingStatus: "UNKNOWN", - }, + payload: payload ?? null, }; } - if (!response) { - throw new Error("No response received from gRPC health check"); - } - - const isServing = response.servingStatus === "SERVING"; + const grpcPayload = response as GrpcStatusPayload; + const isServing = grpcPayload.servingStatus === "SERVING"; return { monitorId: id, teamId: teamId, type: type, status: isServing, - code: isServing ? 200 : NETWORK_ERROR, - message: isServing ? "gRPC health check successful" : "gRPC health check failed", + code: isServing ? 200 : 5000, + message: isServing ? `gRPC service healthy (${grpcPayload.servingStatus})` : `gRPC service unhealthy (${grpcPayload.servingStatus})`, responseTime, - payload: { - grpcStatusCode: isServing ? 200 : NETWORK_ERROR, - grpcStatusName: response.grpcStatusName, - serviceName: response.serviceName, - servingStatus: response.servingStatus || "UNKNOWN", - }, + payload: grpcPayload, }; } catch (err: unknown) { const originalMessage = err instanceof Error ? err.message : String(err); diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index e373e2cff..42fa39a40 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -6,6 +6,7 @@ import { MonitorStatusResponse } from "@/types/network.js"; import { Agent as HttpsAgent } from "https"; import { Monitor, MonitorType } from "@/types/monitor.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; +import CacheableLookup from "cacheable-lookup"; export class HttpProvider implements IStatusProvider { readonly type = "http"; @@ -13,7 +14,16 @@ export class HttpProvider implements IStatusProvider { constructor( private got: Got, private advancedMatcher: AdvancedMatcher - ) {} + ) { + const cacheable = new CacheableLookup(); + this.got = got.extend({ + dnsCache: cacheable, + timeout: { + request: 30000, + }, + retry: { limit: 1 }, + }); + } supports(type: MonitorType) { return type === "http"; diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 1a0d71553..7b7c739d8 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,5 +1,4 @@ -import { HTTPError, RequestError } from "got"; -import type { Got, Response } from "got"; +import type { Got } from "got"; import type { Monitor, MonitorStatusResponse, @@ -14,28 +13,12 @@ import type { } from "@/types/index.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; -import path from "path"; -import { fileURLToPath } from "url"; -import { MonitorStatusResponseOverrides } from "@/types/index.js"; -import CacheableLookup from "cacheable-lookup"; import { ISettingsService } from "../system/settingsService.js"; import { ILogger } from "@/utils/logger.js"; import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; -interface BuildStatusResponseArgs { - monitor: Monitor; - response?: Response | null; - error?: Error | RequestError | HTTPError | null; - payload?: T | null; - jsonPath?: string; - matchMethod?: MonitorStatusResponse["matchMethod"]; - expectedValue?: string; - extracted?: unknown; - overrides?: MonitorStatusResponseOverrides; -} - export interface INetworkService { readonly serviceName: string; requestStatus( @@ -82,19 +65,7 @@ class NetworkService implements INetworkService { private PING_ERROR: number; private axios: AxiosStatic; - private got: Got; - private https: typeof import("https"); - private jmespath: { search: (data: unknown, expression: string) => unknown }; - private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> }; - private ping: typeof import("ping"); private logger: ILogger; - private Docker: typeof import("dockerode"); - private net: typeof import("net"); - private settingsService: ISettingsService; - private grpc: typeof import("@grpc/grpc-js"); - private protoLoader: typeof import("@grpc/proto-loader"); - - // New providers private pingProvider; private httpProvider; private pageSpeedProvider; @@ -102,83 +73,19 @@ class NetworkService implements INetworkService { private dockerProvider; private portProvider; private gameProvider; - - private buildStatusResponse = ({ - monitor, - response, - error, - payload, - jsonPath, - matchMethod, - expectedValue, - extracted, - overrides, - }: BuildStatusResponseArgs): MonitorStatusResponse => { - if (error) { - const statusResponse: MonitorStatusResponse = { - monitorId: monitor.id, - teamId: monitor.teamId, - type: monitor.type, - status: false, - code: this.NETWORK_ERROR, - message: error.message ?? "Network error", - responseTime: 0, - timings: undefined, - jsonPath, - matchMethod, - expectedValue, - extracted, - payload, - }; - if (error instanceof HTTPError || error instanceof RequestError) { - statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR; - statusResponse.message = error.message; - statusResponse.responseTime = error.timings?.phases?.total ?? 0; - statusResponse.timings = error.timings; - } - return { ...statusResponse, ...(overrides ?? {}) } as MonitorStatusResponse; - } - - return { - monitorId: monitor.id, - teamId: monitor.teamId, - type: monitor.type, - status: response?.ok ?? false, - code: response?.statusCode ?? this.NETWORK_ERROR, - message: response?.statusMessage ?? "", - responseTime: response?.timings?.phases?.total ?? 0, - timings: response?.timings, - payload: payload ?? response?.body, - jsonPath, - matchMethod, - expectedValue, - extracted, - ...(overrides ?? {}), - } as MonitorStatusResponse; - }; + private grpcProvider; constructor( axios: AxiosStatic, - got: Got, - https: typeof import("https"), - jmespath: { search: (data: unknown, expression: string) => unknown }, - GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> }, - ping: typeof import("ping"), logger: ILogger, - Docker: typeof import("dockerode"), - net: typeof import("net"), - settingsService: ISettingsService, - grpc: typeof import("@grpc/grpc-js"), - protoLoader: typeof import("@grpc/proto-loader"), - - // New providers pingProvider: IStatusProvider, httpProvider: IStatusProvider, pagespeedProvider: IStatusProvider, hardwareProvider: IStatusProvider, dockerProvider: IStatusProvider, portProvider: IStatusProvider, - gameProvider: IStatusProvider + gameProvider: IStatusProvider, + grpcProvider: IStatusProvider ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -192,18 +99,8 @@ class NetworkService implements INetworkService { this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; this.axios = axios; - this.https = https; - this.jmespath = jmespath; - this.GameDig = GameDig; - this.ping = ping; this.logger = logger; - this.Docker = Docker; - this.net = net; - this.settingsService = settingsService; - this.grpc = grpc; - this.protoLoader = protoLoader; - // New providers this.pingProvider = pingProvider; this.httpProvider = httpProvider; this.pageSpeedProvider = pagespeedProvider; @@ -211,34 +108,13 @@ class NetworkService implements INetworkService { this.dockerProvider = dockerProvider; this.portProvider = portProvider; this.gameProvider = gameProvider; - const cacheable = new CacheableLookup(); - - this.got = got.extend({ - dnsCache: cacheable, - timeout: { - request: 30000, - }, - retry: { limit: 1 }, - }); + this.grpcProvider = grpcProvider; } get serviceName(): string { return NetworkService.SERVICE_NAME; } - // Helper functions - private async timeRequest(operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> { - const start = process.hrtime.bigint(); - try { - const response = await operation(); - const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response, responseTime: elapsedMs, error: null }; - } catch (error) { - const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response: null, responseTime: elapsedMs, error }; - } - } - // Main entry point async requestStatus( monitor: Monitor @@ -271,169 +147,12 @@ class NetworkService implements INetworkService { case this.TYPE_GAME: return await this.gameProvider.handle(monitor); case this.TYPE_GRPC: - return await this.requestGrpc(monitor); + return await this.grpcProvider.handle(monitor); default: return this.handleUnsupportedType(type); } } - private async requestGrpc(monitor: Monitor): Promise> { - try { - const { url, port, ignoreTlsErrors } = monitor; - const grpcServiceName = monitor.grpcServiceName || ""; - - if (!url) { - throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" }); - } - if (!port) { - throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" }); - } - - const target = `${url}:${port}`; - - const currentFilePath = fileURLToPath(import.meta.url); - const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto"); - - const packageDefinition = this.protoLoader.loadSync(protoPath, { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - }); - const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as { - grpc: { - health: { - v1: { - Health: new ( - target: string, - credentials: unknown - ) => { - Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void; - close: () => void; - }; - }; - }; - }; - }; - const healthService = grpcObject.grpc.health.v1.Health; - - let credentials; - if (ignoreTlsErrors) { - credentials = this.grpc.credentials.createSsl(null, null, null, { - checkServerIdentity: () => undefined, - }); - } else { - credentials = this.grpc.credentials.createInsecure(); - } - - const client = new healthService(target, credentials); - - const TIMEOUT_MS = 10000; - const deadline = new Date(Date.now() + TIMEOUT_MS); - - const grpcResponse = this.buildStatusResponse({ - monitor, - overrides: { - status: false, - code: this.NETWORK_ERROR, - message: "gRPC health check not executed", - }, - }); - - const { response, responseTime, error } = await this.timeRequest(() => { - return new Promise((resolve, reject) => { - client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => { - client.close(); - - if (err) { - const grpcErr = err as { code?: number; details?: string; message?: string }; - const payload: GrpcStatusPayload = { - grpcStatusCode: grpcErr.code ?? -1, - grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1), - serviceName: grpcServiceName, - servingStatus: "UNKNOWN", - }; - const grpcError = new AppError({ - message: grpcErr.details || grpcErr.message || "gRPC error", - service: this.SERVICE_NAME, - method: "requestGrpc", - }) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; - grpcError.grpcPayload = payload; - grpcError.grpcCode = grpcErr.code; - reject(grpcError); - return; - } - - const resp = response as { status?: string } | undefined; - const servingStatus = resp?.status ?? "UNKNOWN"; - resolve({ - grpcStatusCode: 0, - grpcStatusName: "OK", - serviceName: grpcServiceName, - servingStatus, - }); - }); - }); - }); - - if (error) { - const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; - const payload = grpcError.grpcPayload; - grpcResponse.status = false; - grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR; - grpcResponse.message = grpcError.message ?? "gRPC health check failed"; - grpcResponse.responseTime = responseTime; - grpcResponse.payload = payload ?? null; - return grpcResponse; - } - - const grpcPayload = response as GrpcStatusPayload; - const isServing = grpcPayload.servingStatus === "SERVING"; - - grpcResponse.status = isServing; - grpcResponse.code = isServing ? 200 : this.NETWORK_ERROR; - grpcResponse.message = isServing - ? `gRPC service healthy (${grpcPayload.servingStatus})` - : `gRPC service unhealthy (${grpcPayload.servingStatus})`; - grpcResponse.responseTime = responseTime; - grpcResponse.payload = grpcPayload; - - return grpcResponse; - } catch (err: unknown) { - const originalMessage = err instanceof Error ? err.message : String(err); - throw new AppError({ - message: originalMessage || "Error performing gRPC health check", - service: this.SERVICE_NAME, - method: "requestGrpc", - details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName }, - }); - } - } - - private getGrpcStatusName(code: number): string { - const statusNames: Record = { - 0: "OK", - 1: "CANCELLED", - 2: "UNKNOWN", - 3: "INVALID_ARGUMENT", - 4: "DEADLINE_EXCEEDED", - 5: "NOT_FOUND", - 6: "ALREADY_EXISTS", - 7: "PERMISSION_DENIED", - 8: "RESOURCE_EXHAUSTED", - 9: "FAILED_PRECONDITION", - 10: "ABORTED", - 11: "OUT_OF_RANGE", - 12: "UNIMPLEMENTED", - 13: "INTERNAL", - 14: "UNAVAILABLE", - 15: "DATA_LOSS", - 16: "UNAUTHENTICATED", - }; - return statusNames[code] || "UNKNOWN"; - } - private async handleUnsupportedType(type: string): Promise { return { monitorId: "unknown", From f8c0b57d212882ed36c9d5317f43d05343aa82d5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 19:37:32 +0000 Subject: [PATCH 19/53] remove unused imports --- server/src/service/infrastructure/networkService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 7b7c739d8..2d6c3fe08 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,4 +1,3 @@ -import type { Got } from "got"; import type { Monitor, MonitorStatusResponse, @@ -14,7 +13,6 @@ import type { import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; -import { ISettingsService } from "../system/settingsService.js"; import { ILogger } from "@/utils/logger.js"; import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; From c5c9a1a88b4f7681986de0effe330b374e1ada76 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 13:57:39 -0800 Subject: [PATCH 20/53] fix missing await --- server/src/service/infrastructure/emailService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index 0840d5a4c..5246351c0 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -94,7 +94,7 @@ class EmailService implements IEmailService { if (!mjml) { throw new Error(`Template ${template} not found`); } - const html = this.mjml2html(mjml); + const html = await this.mjml2html(mjml); return html.html; } catch (error: unknown) { this.logger.error({ From 69e07975e35872c47cafe097fbb811a3b40811bb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 13:57:45 -0800 Subject: [PATCH 21/53] bump dependencies --- client/package-lock.json | 308 ++++----- client/package.json | 2 +- server/package-lock.json | 1387 +++++++++++++++++++------------------- server/package.json | 4 +- 4 files changed, 841 insertions(+), 860 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 338de1e26..e1dfadad5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -58,7 +58,7 @@ "lint-staged": "^16.2.7", "prettier": "^3.3.3", "typescript": "5.9.3", - "vite": "6.3.6" + "vite": "6.4.1" } }, "node_modules/@babel/code-frame": { @@ -83,6 +83,7 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -353,6 +354,7 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -390,6 +392,7 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -434,7 +437,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -451,7 +453,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -468,7 +469,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -485,7 +485,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -502,7 +501,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -519,7 +517,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -536,7 +533,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -553,7 +549,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -570,7 +565,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -587,7 +581,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -604,7 +597,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -621,7 +613,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,7 +629,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -655,7 +645,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -672,7 +661,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -689,7 +677,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -704,7 +691,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -721,7 +707,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -738,7 +723,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -755,7 +739,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -772,7 +755,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -789,7 +771,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -806,7 +787,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -823,7 +803,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -840,7 +819,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -857,7 +835,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1185,6 +1162,7 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1289,6 +1267,7 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -1660,13 +1639,12 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1674,13 +1652,12 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1688,13 +1665,12 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1702,13 +1678,12 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1716,13 +1691,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1730,13 +1704,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1744,13 +1717,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1758,13 +1730,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1772,13 +1743,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1786,13 +1756,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1800,13 +1769,12 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1814,13 +1782,12 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1828,13 +1795,12 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1842,13 +1808,12 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1856,13 +1821,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1870,13 +1834,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1884,13 +1847,12 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1898,11 +1860,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1910,13 +1873,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1924,13 +1886,12 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1938,13 +1899,12 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1952,13 +1912,12 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1966,13 +1925,12 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1980,13 +1938,12 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1994,13 +1951,12 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2350,8 +2306,9 @@ }, "node_modules/@types/node": { "version": "24.5.2", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2366,8 +2323,8 @@ }, "node_modules/@types/react": { "version": "18.3.27", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2489,6 +2446,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2505,7 +2463,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2811,6 +2771,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3242,7 +3203,8 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -3566,7 +3528,6 @@ }, "node_modules/esbuild": { "version": "0.25.12", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -3625,6 +3586,7 @@ "version": "8.57.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3884,7 +3846,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4018,7 +3979,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4371,6 +4331,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5154,6 +5115,7 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5251,7 +5213,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5317,7 +5281,6 @@ }, "node_modules/nanoid": { "version": "3.3.11", - "dev": true, "funding": [ { "type": "github", @@ -5650,7 +5613,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "dev": true, "funding": [ { "type": "opencollective", @@ -5801,6 +5763,7 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5812,6 +5775,7 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6011,7 +5975,8 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6157,9 +6122,11 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.57.1", - "dev": true, + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6171,31 +6138,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -6530,7 +6497,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6818,7 +6784,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -6948,8 +6913,9 @@ }, "node_modules/typescript": { "version": "5.9.3", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6992,7 +6958,7 @@ }, "node_modules/undici-types": { "version": "7.12.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/union-value": { @@ -7081,9 +7047,11 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "dev": true, + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7366,7 +7334,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/client/package.json b/client/package.json index dd585636b..ba2d59275 100644 --- a/client/package.json +++ b/client/package.json @@ -66,6 +66,6 @@ "lint-staged": "^16.2.7", "prettier": "^3.3.3", "typescript": "5.9.3", - "vite": "6.3.6" + "vite": "6.4.1" } } diff --git a/server/package-lock.json b/server/package-lock.json index ced3a5d0e..889c08f15 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -23,7 +23,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "8.0.1", - "gamedig": "^5.3.1", + "gamedig": "5.2.0", "got": "14.4.7", "handlebars": "^4.7.8", "helmet": "^8.0.0", @@ -36,7 +36,7 @@ "mjml": "^5.0.0-alpha.4", "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.14", + "nodemailer": "8.0.1", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", @@ -262,558 +262,530 @@ } }, "node_modules/@aws-sdk/client-sesv2": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.917.0.tgz", - "integrity": "sha512-Jwz+3rO9fc2H8JyF4ainL5x06tiQ0n4ewKIpdUeGIauKdndMItYP1aeM0Y8wJQPTF1XEpWFC36cTSDgk/eypxA==", + "version": "3.1003.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.1003.0.tgz", + "integrity": "sha512-NR1rfYBcT4QWMG+VN2CTgbtWpf8I6aYsrU81hslucQd+z9O0qHMzZ3KHEdIcT9+PGYtqDwqhxUArboF7G09Oiw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/credential-provider-node": "3.917.0", - "@aws-sdk/middleware-host-header": "3.914.0", - "@aws-sdk/middleware-logger": "3.914.0", - "@aws-sdk/middleware-recursion-detection": "3.914.0", - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/region-config-resolver": "3.914.0", - "@aws-sdk/signature-v4-multi-region": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@aws-sdk/util-user-agent-browser": "3.914.0", - "@aws-sdk/util-user-agent-node": "3.916.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/core": "^3.17.1", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/hash-node": "^4.2.3", - "@smithy/invalid-dependency": "^4.2.3", - "@smithy/middleware-content-length": "^4.2.3", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-retry": "^4.4.5", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.4", - "@smithy/util-defaults-mode-node": "^4.2.6", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.17", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.18", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/signature-v4-multi-region": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.3", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", - "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/middleware-host-header": "3.914.0", - "@aws-sdk/middleware-logger": "3.914.0", - "@aws-sdk/middleware-recursion-detection": "3.914.0", - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/region-config-resolver": "3.914.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@aws-sdk/util-user-agent-browser": "3.914.0", - "@aws-sdk/util-user-agent-node": "3.916.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/core": "^3.17.1", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/hash-node": "^4.2.3", - "@smithy/invalid-dependency": "^4.2.3", - "@smithy/middleware-content-length": "^4.2.3", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-retry": "^4.4.5", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.4", - "@smithy/util-defaults-mode-node": "^4.2.6", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/core": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", - "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.18.tgz", + "integrity": "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@aws-sdk/xml-builder": "3.914.0", - "@smithy/core": "^3.17.1", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/signature-v4": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/xml-builder": "^3.972.10", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", - "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.16.tgz", + "integrity": "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", - "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.18.tgz", + "integrity": "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-stream": "^4.5.4", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.917.0.tgz", - "integrity": "sha512-rvQ0QamLySRq+Okc0ZqFHZ3Fbvj3tYuWNIlzyEKklNmw5X5PM1idYKlOJflY2dvUGkIqY3lUC9SC2WL+1s7KIw==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.16.tgz", + "integrity": "sha512-hzAnzNXKV0A4knFRWGu2NCt72P4WWxpEGnOc6H3DptUjC4oX3hGw846oN76M1rTHAOwDdbhjU0GAOWR4OUfTZg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/credential-provider-env": "3.916.0", - "@aws-sdk/credential-provider-http": "3.916.0", - "@aws-sdk/credential-provider-process": "3.916.0", - "@aws-sdk/credential-provider-sso": "3.916.0", - "@aws-sdk/credential-provider-web-identity": "3.917.0", - "@aws-sdk/nested-clients": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/credential-provider-imds": "^4.2.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-login": "^3.972.16", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.16", + "@aws-sdk/nested-clients": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.16.tgz", + "integrity": "sha512-VI0kXTlr0o1FTay+Jvx6AKqx5ECBgp7X4VevGBEbuXdCXnNp7SPU0KvjsOLVhIz3OoPK4/lTXphk43t0IVk65w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.917.0.tgz", - "integrity": "sha512-n7HUJ+TgU9wV/Z46yR1rqD9hUjfG50AKi+b5UXTlaDlVD8bckg40i77ROCllp53h32xQj/7H0yBIYyphwzLtmg==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.17.tgz", + "integrity": "sha512-98MAcQ2Dk7zkvgwZ5f6fLX2lTyptC3gTSDx4EpvTdJWET8qs9lBPYggoYx7GmKp/5uk0OwVl0hxIDZsDNS/Y9g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.916.0", - "@aws-sdk/credential-provider-http": "3.916.0", - "@aws-sdk/credential-provider-ini": "3.917.0", - "@aws-sdk/credential-provider-process": "3.916.0", - "@aws-sdk/credential-provider-sso": "3.916.0", - "@aws-sdk/credential-provider-web-identity": "3.917.0", - "@aws-sdk/types": "3.914.0", - "@smithy/credential-provider-imds": "^4.2.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-ini": "^3.972.16", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.16", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", - "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.16.tgz", + "integrity": "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", - "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.16.tgz", + "integrity": "sha512-b9of7tQgERxgcEcwAFWvRe84ivw+Kw6b3jVuz/6LQzonkomiY5UoWfprkbjc8FSCQ2VjDqKTvIRA9F0KSQ025w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.916.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/token-providers": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.6", + "@aws-sdk/token-providers": "3.1003.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.917.0.tgz", - "integrity": "sha512-pZncQhFbwW04pB0jcD5OFv3x2gAddDYCVxyJVixgyhSw7bKCYxqu6ramfq1NxyVpmm+qsw+ijwi/3cCmhUHF/A==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.16.tgz", + "integrity": "sha512-PaOH5jFoPQX4WkqpKzKh9cM7rieKtbgEGqrZ+ybGmotJhcvhI/xl69yCwMbHGnpQJJmHZIX9q2zaPB7HTBn/4w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/nested-clients": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", - "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", + "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", - "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", + "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/types": "^4.8.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", - "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", + "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@aws/lambda-invoke-store": "^0.0.1", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/types": "^3.973.5", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.916.0.tgz", - "integrity": "sha512-pjmzzjkEkpJObzmTthqJPq/P13KoNFuEi/x5PISlzJtHofCNcyXeVAQ90yvY2dQ6UXHf511Rh1/ytiKy2A8M0g==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.18.tgz", + "integrity": "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.17.1", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/signature-v4": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-stream": "^4.5.4", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", - "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.18.tgz", + "integrity": "sha512-KcqQDs/7WtoEnp52+879f8/i1XAJkgka5i4arOtOCPR10o4wWo3VRecDI9Gxoh6oghmLCnIiOSKyRcXI/50E+w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@smithy/core": "^3.17.1", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@smithy/core": "^3.23.8", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", - "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.6.tgz", + "integrity": "sha512-blNJ3ugn4gCQ9ZSZi/firzKCvVl5LvPFVxv24LprENeWI4R8UApG006UQkF4SkmLygKq2BQXRad2/anQ13Te4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/middleware-host-header": "3.914.0", - "@aws-sdk/middleware-logger": "3.914.0", - "@aws-sdk/middleware-recursion-detection": "3.914.0", - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/region-config-resolver": "3.914.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@aws-sdk/util-user-agent-browser": "3.914.0", - "@aws-sdk/util-user-agent-node": "3.916.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/core": "^3.17.1", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/hash-node": "^4.2.3", - "@smithy/invalid-dependency": "^4.2.3", - "@smithy/middleware-content-length": "^4.2.3", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-retry": "^4.4.5", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.4", - "@smithy/util-defaults-mode-node": "^4.2.6", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.18", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.3", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", - "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", + "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/types": "^4.8.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/config-resolver": "^4.4.10", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.916.0.tgz", - "integrity": "sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==", + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.6.tgz", + "integrity": "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/protocol-http": "^5.3.3", - "@smithy/signature-v4": "^5.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", - "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", + "version": "3.1003.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1003.0.tgz", + "integrity": "sha512-SOyyWNdT7njKRwtZ1JhwHlH1csv6Pkgf305X96/OIfnhq1pU/EjmT6W6por57rVrjrKuHBuEIXgpWv8OgoMHpg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/nested-clients": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", - "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "version": "3.973.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", + "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", - "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", - "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", + "version": "3.996.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", + "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-endpoints": "^3.2.3", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", - "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", - "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", + "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/types": "^4.8.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", - "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", + "version": "3.973.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.3.tgz", + "integrity": "sha512-8s2cQmTUOwcBlIJyI9PAZNnnnF+cGtdhHc1yzMMsSD/GR/Hxj7m0IGUE92CslXXb8/p5Q76iqOCjN1GFwyf+1A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/types": "^4.8.0", + "@aws-sdk/middleware-user-agent": "^3.972.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" @@ -825,24 +797,44 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.914.0.tgz", - "integrity": "sha512-k75evsBD5TcIjedycYS7QXQ98AmOtbnxRJOPtCo0IwYRmy7UvqgS/gBL5SmrIqeV6FDSYRQMgdBxSMp6MLmdew==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", + "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", - "fast-xml-parser": "5.2.5", + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", - "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3487,13 +3479,13 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", - "integrity": "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", + "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3501,17 +3493,17 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.0.tgz", - "integrity": "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", + "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -3519,21 +3511,21 @@ } }, "node_modules/@smithy/core": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.1.tgz", - "integrity": "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.8.tgz", + "integrity": "sha512-f7uPeBi7ehmLT4YF2u9j3qx6lSnurG1DLXOsTtJrIRNDF7VXio4BGHQ+SQteN/BrUVudbkuL4v7oOsRCzq4BqA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-stream": "^4.5.4", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -3541,16 +3533,16 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.3.tgz", - "integrity": "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", + "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -3558,16 +3550,16 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.4.tgz", - "integrity": "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw==", + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", + "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.3", - "@smithy/querystring-builder": "^4.2.3", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -3575,15 +3567,15 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.3.tgz", - "integrity": "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", + "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3591,13 +3583,13 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.3.tgz", - "integrity": "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", + "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3605,9 +3597,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3618,14 +3610,14 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.3.tgz", - "integrity": "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", + "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3633,19 +3625,19 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.5.tgz", - "integrity": "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw==", + "version": "4.4.22", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.22.tgz", + "integrity": "sha512-sc81w1o4Jy+/MAQlY3sQ8C7CmSpcvIi3TAzXblUv2hjG11BBSJi/Cw8vDx5BxMxapuH2I+Gc+45vWsgU07WZRQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.17.1", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-middleware": "^4.2.3", + "@smithy/core": "^3.23.8", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -3653,20 +3645,20 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.5.tgz", - "integrity": "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A==", + "version": "4.4.39", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.39.tgz", + "integrity": "sha512-MCVCxaCzuZgiHtHGV2Ke44nh6t4+8/tO+rTYOzrr2+G4nMLU/qbzNCWKBX54lyEaVcGQrfOJiG2f8imtiw+nIQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/service-error-classification": "^4.2.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/uuid": "^1.1.0", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -3674,14 +3666,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.3.tgz", - "integrity": "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", + "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3689,13 +3681,13 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.3.tgz", - "integrity": "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", + "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3703,15 +3695,15 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.3.tgz", - "integrity": "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", + "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3719,16 +3711,16 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.3.tgz", - "integrity": "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ==", + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", + "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/querystring-builder": "^4.2.3", - "@smithy/types": "^4.8.0", + "@smithy/abort-controller": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3736,13 +3728,13 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.3.tgz", - "integrity": "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", + "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3750,13 +3742,13 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.3.tgz", - "integrity": "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", + "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3764,14 +3756,14 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.3.tgz", - "integrity": "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", + "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", - "@smithy/util-uri-escape": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3779,13 +3771,13 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.3.tgz", - "integrity": "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", + "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3793,26 +3785,26 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.3.tgz", - "integrity": "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", + "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0" + "@smithy/types": "^4.13.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.3.tgz", - "integrity": "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", + "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3820,19 +3812,19 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.3.tgz", - "integrity": "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", + "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3840,18 +3832,18 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.1.tgz", - "integrity": "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.2.tgz", + "integrity": "sha512-HezY3UuG0k4T+4xhFKctLXCA5N2oN+Rtv+mmL8Gt7YmsUY2yhmcLyW75qrSzldfj75IsCW/4UhY3s20KcFnZqA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.17.1", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-stream": "^4.5.4", + "@smithy/core": "^3.23.8", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" }, "engines": { @@ -3859,9 +3851,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.0.tgz", - "integrity": "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3872,14 +3864,14 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.3.tgz", - "integrity": "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", + "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.3", - "@smithy/types": "^4.8.0", + "@smithy/querystring-parser": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3887,14 +3879,14 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3902,9 +3894,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3915,9 +3907,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3928,13 +3920,13 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -3942,9 +3934,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3955,15 +3947,15 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.4.tgz", - "integrity": "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw==", + "version": "4.3.38", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.38.tgz", + "integrity": "sha512-c8P1mFLNxcsdAMabB8/VUQUbWzFmgujWi4bAXSggcqLYPc8V4U5abqFqOyn+dK4YT+q8UyCVkTO8807t4t2syA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3971,18 +3963,18 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.6.tgz", - "integrity": "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ==", + "version": "4.2.41", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.41.tgz", + "integrity": "sha512-/UG+9MT3UZAR0fLzOtMJMfWGcjjHvgggq924x/CRy8vRbL+yFf3Z6vETlvq8vDH92+31P/1gSOFoo7303wN8WQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.0", - "@smithy/credential-provider-imds": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", + "@smithy/config-resolver": "^4.4.10", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -3990,14 +3982,14 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.3.tgz", - "integrity": "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", + "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/types": "^4.8.0", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4005,9 +3997,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4018,13 +4010,13 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.3.tgz", - "integrity": "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", + "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4032,14 +4024,14 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.3.tgz", - "integrity": "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", + "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.3", - "@smithy/types": "^4.8.0", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4047,19 +4039,19 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.4.tgz", - "integrity": "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw==", + "version": "4.5.17", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", + "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4067,9 +4059,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4080,13 +4072,13 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4094,9 +4086,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4735,6 +4727,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5389,9 +5382,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -5546,13 +5539,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", - "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -5879,9 +5872,9 @@ "license": "ISC" }, "node_modules/bowser": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", - "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "dev": true, "license": "MIT" }, @@ -7188,9 +7181,9 @@ "license": "MIT" }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7316,10 +7309,13 @@ } }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -7391,14 +7387,6 @@ "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", "license": "ISC" }, - "node_modules/emitter-component": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", - "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -8137,23 +8125,18 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } + "license": "MIT" }, "node_modules/fastq": { "version": "1.20.1", @@ -8343,9 +8326,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -8434,19 +8417,18 @@ } }, "node_modules/gamedig": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.2.tgz", - "integrity": "sha512-R2b1LwjW783PZsHRl9M8R06UkvJwXmJ6PDKk48UukJQ9ktiwrCeAk90MAZx6nF3oA444uf7r5eHjfaYbNoSV+Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.2.0.tgz", + "integrity": "sha512-YAk9KmQnbiThASSXYiL2LPeppgfthMjG8PIQK4UK8DveF/X/zhwYaUKqi+op05d0iLLoCzvx5J2Q2vUr+OVzQg==", "license": "MIT", "dependencies": { - "fast-xml-parser": "5.2.5", + "cheerio": "1.0.0-rc.12", "gbxremote": "0.2.1", "got": "13.0.0", - "iconv-lite": "0.7.0", - "long": "5.3.2", + "iconv-lite": "0.6.3", + "long": "5.2.3", "minimist": "1.2.8", "seek-bzip": "2.0.0", - "telnet-client": "2.2.6", "varint": "6.0.0" }, "bin": { @@ -8533,21 +8515,23 @@ } }, "node_modules/gamedig/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, + "node_modules/gamedig/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, "node_modules/gamedig/node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -8751,12 +8735,12 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10683,9 +10667,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -11116,9 +11100,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -11236,12 +11220,12 @@ } }, "node_modules/mjml-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -11859,12 +11843,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/net": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", - "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", - "license": "MIT" - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -11962,9 +11940,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", - "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -12960,9 +12938,9 @@ } }, "node_modules/postcss-svgo/node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", "license": "MIT", "dependencies": { "commander": "^11.1.0", @@ -12971,7 +12949,7 @@ "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", - "sax": "^1.4.1" + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo.js" @@ -13271,9 +13249,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -13640,9 +13618,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -14127,15 +14105,6 @@ "node": ">= 0.8" } }, - "node_modules/stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", - "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", - "license": "MIT", - "dependencies": { - "emitter-component": "^1.1.1" - } - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -14376,9 +14345,10 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "dev": true, "funding": [ { "type": "github", @@ -14473,6 +14443,63 @@ "node": ">=8" } }, + "node_modules/svgo": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgo/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/svgo/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0", + "optional": true + }, "node_modules/swagger-ui-dist": { "version": "5.30.0", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", @@ -14547,20 +14574,6 @@ "node": ">=6" } }, - "node_modules/telnet-client": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.6.tgz", - "integrity": "sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==", - "license": "MIT", - "dependencies": { - "net": "^1.0.2", - "stream": "^0.0.2" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/kozjak" - } - }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -14587,13 +14600,13 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -15035,9 +15048,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/server/package.json b/server/package.json index b94a1e852..ce2c0822e 100755 --- a/server/package.json +++ b/server/package.json @@ -38,7 +38,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "8.0.1", - "gamedig": "^5.3.1", + "gamedig": "5.2.0", "got": "14.4.7", "handlebars": "^4.7.8", "helmet": "^8.0.0", @@ -51,7 +51,7 @@ "mjml": "^5.0.0-alpha.4", "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.14", + "nodemailer": "8.0.1", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", From 0718e63e2f9d1107e8b67b8692fbcc2d26c1bfce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 14:11:11 -0800 Subject: [PATCH 22/53] cleanup network service --- server/src/config/services.ts | 8 +- .../infrastructure/network/PortProvider.ts | 1 - .../service/infrastructure/networkService.ts | 136 +++--------------- 3 files changed, 21 insertions(+), 124 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index bd13b7dfc..f7746ef47 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -163,9 +163,7 @@ export const initializeServices = async ({ const gameProvider = new GameProvider(logger, GameDig); const grpcProvider = new GrpcProvider(grpc, protoLoader); - const networkService = new NetworkService( - axios, - logger, + const networkService = new NetworkService(axios, logger, [ pingProvider, httpProvider, pageSpeedProvider, @@ -173,8 +171,8 @@ export const initializeServices = async ({ dockerProvider, portProvider, gameProvider, - grpcProvider - ); + grpcProvider, + ]); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const notificationMessageBuilder = new NotificationMessageBuilder(); diff --git a/server/src/service/infrastructure/network/PortProvider.ts b/server/src/service/infrastructure/network/PortProvider.ts index 66fefd0fe..7c1c96ce4 100644 --- a/server/src/service/infrastructure/network/PortProvider.ts +++ b/server/src/service/infrastructure/network/PortProvider.ts @@ -51,7 +51,6 @@ export class PortProvider implements IStatusProvider { }); }); if (error) { - console.log(error); const errorMessage = error instanceof Error ? error.message : "Port check failed"; return { monitorId: monitor.id, diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 2d6c3fe08..f9c79ac29 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,38 +1,14 @@ -import type { - Monitor, - MonitorStatusResponse, - GrpcStatusPayload, - PageSpeedStatusPayload, - HttpStatusPayload, - HardwareStatusPayload, - PingStatusPayload, - DockerStatusPayload, - PortStatusPayload, - GameStatusPayload, -} from "@/types/index.js"; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; - +import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; import { ILogger } from "@/utils/logger.js"; import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; export interface INetworkService { readonly serviceName: string; - requestStatus( - monitor: Monitor - ): Promise< - MonitorStatusResponse< - | PingStatusPayload - | HttpStatusPayload - | PageSpeedStatusPayload - | HardwareStatusPayload - | DockerStatusPayload - | PortStatusPayload - | GameStatusPayload - | GrpcStatusPayload - > - >; + requestStatus(monitor: Monitor): Promise>; requestWebhook( type: string, url: string, @@ -50,63 +26,16 @@ export interface INetworkService { class NetworkService implements INetworkService { static SERVICE_NAME = SERVICE_NAME; - private TYPE_PING: string; - private TYPE_HTTP: string; - private TYPE_PAGESPEED: string; - private TYPE_HARDWARE: string; - private TYPE_DOCKER: string; - private TYPE_PORT: string; - private TYPE_GAME: string; - private TYPE_GRPC: string; - private SERVICE_NAME: string; - private NETWORK_ERROR: number; - private PING_ERROR: number; - private axios: AxiosStatic; private logger: ILogger; - private pingProvider; - private httpProvider; - private pageSpeedProvider; - private hardwareProvider; - private dockerProvider; - private portProvider; - private gameProvider; - private grpcProvider; constructor( axios: AxiosStatic, logger: ILogger, - pingProvider: IStatusProvider, - httpProvider: IStatusProvider, - pagespeedProvider: IStatusProvider, - hardwareProvider: IStatusProvider, - dockerProvider: IStatusProvider, - portProvider: IStatusProvider, - gameProvider: IStatusProvider, - grpcProvider: IStatusProvider + private providers: IStatusProvider[] ) { - this.TYPE_PING = "ping"; - this.TYPE_HTTP = "http"; - this.TYPE_PAGESPEED = "pagespeed"; - this.TYPE_HARDWARE = "hardware"; - this.TYPE_DOCKER = "docker"; - this.TYPE_PORT = "port"; - this.TYPE_GAME = "game"; - this.TYPE_GRPC = "grpc"; - this.SERVICE_NAME = SERVICE_NAME; - this.NETWORK_ERROR = 5000; - this.PING_ERROR = 5001; this.axios = axios; this.logger = logger; - - this.pingProvider = pingProvider; - this.httpProvider = httpProvider; - this.pageSpeedProvider = pagespeedProvider; - this.hardwareProvider = hardwareProvider; - this.dockerProvider = dockerProvider; - this.portProvider = portProvider; - this.gameProvider = gameProvider; - this.grpcProvider = grpcProvider; } get serviceName(): string { @@ -114,41 +43,12 @@ class NetworkService implements INetworkService { } // Main entry point - async requestStatus( - monitor: Monitor - ): Promise< - MonitorStatusResponse< - | PingStatusPayload - | HttpStatusPayload - | PageSpeedStatusPayload - | HardwareStatusPayload - | DockerStatusPayload - | PortStatusPayload - | GameStatusPayload - | GrpcStatusPayload - > - > { - const type = monitor?.type || "unknown"; - switch (type) { - case this.TYPE_PING: - return await this.pingProvider.handle(monitor); - case this.TYPE_HTTP: - return await this.httpProvider.handle(monitor); - case this.TYPE_PAGESPEED: - return await this.pageSpeedProvider.handle(monitor); - case this.TYPE_HARDWARE: - return await this.hardwareProvider.handle(monitor); - case this.TYPE_DOCKER: - return await this.dockerProvider.handle(monitor); - case this.TYPE_PORT: - return await this.portProvider.handle(monitor); - case this.TYPE_GAME: - return await this.gameProvider.handle(monitor); - case this.TYPE_GRPC: - return await this.grpcProvider.handle(monitor); - default: - return this.handleUnsupportedType(type); + async requestStatus(monitor: Monitor) { + const provider = this.providers.find((p) => p.supports(monitor.type)); + if (!provider) { + return this.handleUnsupportedType(monitor.type); } + return provider.handle(monitor); } private async handleUnsupportedType(type: string): Promise { @@ -157,7 +57,7 @@ class NetworkService implements INetworkService { teamId: "unknown", type: "unknown", status: false, - code: this.NETWORK_ERROR, + code: NETWORK_ERROR, message: `Unsupported type: ${type}`, }; } @@ -181,7 +81,7 @@ class NetworkService implements INetworkService { } catch (err: unknown) { this.logger.warn({ message: err instanceof Error ? err.message : String(err), - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "requestWebhook", }); @@ -190,7 +90,7 @@ class NetworkService implements INetworkService { return { type: "webhook", status: false, - code: axiosError.response?.status ?? this.NETWORK_ERROR, + code: axiosError.response?.status ?? NETWORK_ERROR, message: `Failed to send ${type} notification`, payload: axiosError.response?.data, }; @@ -199,7 +99,7 @@ class NetworkService implements INetworkService { return { type: "webhook", status: false, - code: this.NETWORK_ERROR, + code: NETWORK_ERROR, message: `Failed to send ${type} notification`, }; } @@ -225,7 +125,7 @@ class NetworkService implements INetworkService { throw new AppError({ message: originalMessage || "Error sending PagerDuty notification", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "requestPagerDuty", details: { responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined, @@ -268,7 +168,7 @@ class NetworkService implements INetworkService { if (err instanceof Error) { this.logger.warn({ message: err.message, - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "requestMatrix", }); @@ -276,7 +176,7 @@ class NetworkService implements INetworkService { const axiosError = err as { response?: { status?: number; data?: unknown } }; return { status: false, - code: axiosError.response?.status || this.NETWORK_ERROR, + code: axiosError.response?.status || NETWORK_ERROR, message: "Failed to send Matrix notification", payload: axiosError.response?.data, }; @@ -285,13 +185,13 @@ class NetworkService implements INetworkService { this.logger.warn({ message: String(err), - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "requestMatrix", }); return { status: false, - code: this.NETWORK_ERROR, + code: NETWORK_ERROR, message: "Failed to send Matrix notification", }; } From bd32854c7a6c58784c5bbb864bdb29621f88197b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Mar 2026 14:46:28 -0800 Subject: [PATCH 23/53] cleanup services --- server/src/config/services.ts | 115 +++++++++++------- server/src/db/IDb.ts | 4 + server/src/db/MongoDB.ts | 20 +-- server/src/index.ts | 2 +- server/src/service/business/checkService.ts | 3 +- .../src/service/business/diagnosticService.ts | 19 +-- .../src/service/business/geoChecksService.ts | 3 +- .../src/service/business/incidentService.ts | 27 +++- server/src/service/business/inviteService.ts | 9 +- .../business/maintenanceWindowService.ts | 16 ++- server/src/service/business/userService.ts | 65 ++++++---- server/src/service/index.ts | 31 ++++- .../SuperSimpleQueue/SuperSimpleQueue.ts | 3 +- .../SuperSimpleQueueHelper.ts | 9 +- .../service/infrastructure/bufferService.ts | 3 +- .../service/infrastructure/emailService.ts | 3 +- .../infrastructure/globalPingService.ts | 3 +- .../service/infrastructure/networkService.ts | 3 +- server/src/service/system/settingsService.ts | 3 +- 19 files changed, 221 insertions(+), 120 deletions(-) create mode 100644 server/src/db/IDb.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index f7746ef47..b55bd0f87 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -1,31 +1,65 @@ import MongoDB from "../db/MongoDB.js"; -import NetworkService from "../service/infrastructure/networkService.js"; -import EmailService from "../service/infrastructure/emailService.js"; -import BufferService from "../service/infrastructure/bufferService.js"; +import { IDb } from "@/db/IDb.js"; import { + // Service classes + NetworkService, + EmailService, + BufferService, + GlobalPingService, + SuperSimpleQueue, + SuperSimpleQueueHelper, NotificationsService, StatusService, + NotificationMessageBuilder, + MonitorService, + StatusPageService, + UserService, + CheckService, + GeoChecksService, + DiagnosticService, + InviteService, + MaintenanceWindowService, + IncidentService, + // Notification providers WebhookProvider, SlackProvider, EmailProvider, DiscordProvider, PagerDutyProvider, MatrixProvider, + // Interfaces + INetworkService, + IEmailService, + IBufferService, + ISuperSimpleQueue, INotificationsService, + IStatusService, + IMonitorService, + IUserService, + ICheckService, + IGeoChecksService, + IDiagnosticService, + IInviteService, + IMaintenanceWindowService, + IStatusPageService, + IIncidentService, + INotificationMessageBuilder, + ISettingsService, + EnvConfig, } from "@/service/index.js"; -import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/business/userService.js"; -import CheckService from "../service/business/checkService.js"; -import GeoChecksService from "../service/business/geoChecksService.js"; -import GlobalPingService from "../service/infrastructure/globalPingService.js"; -import DiagnosticService from "../service/business/diagnosticService.js"; -import InviteService from "../service/business/inviteService.js"; -import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; -import { MonitorService } from "@/service/index.js"; -import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js"; -import IncidentService from "../service/business/incidentService.js"; -import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js"; + +// Network providers +import { PingProvider } from "@/service/infrastructure/network/PingProvider.js"; +import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; +import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; +import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; +import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; +import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; +import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; +import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; +import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; + +// Third-party import axios from "axios"; import got from "got"; import ping from "ping"; @@ -44,7 +78,7 @@ import jmespath from "jmespath"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; -// repositories +// Repositories import { MongoMonitorsRepository, MongoChecksRepository, @@ -73,33 +107,23 @@ import { IMaintenanceWindowsRepository, } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; -import { EnvConfig } from "@/service/system/settingsService.js"; -import { PingProvider } from "@/service/infrastructure/network/PingProvider.js"; -import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; -import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; -import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; -import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; -import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; -import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; -import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; -import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; export type InitializedServices = { - settingsService: any; - db: any; - networkService: any; - emailService: any; - bufferService: any; - statusService: any; - jobQueue: any; - userService: any; - checkService: any; - geoChecksService: any; - diagnosticService: any; - inviteService: any; - maintenanceWindowService: any; - monitorService: any; - incidentService: any; + settingsService: ISettingsService; + db: IDb; + networkService: INetworkService; + emailService: IEmailService; + bufferService: IBufferService; + statusService: IStatusService; + jobQueue: ISuperSimpleQueue; + userService: IUserService; + checkService: ICheckService; + geoChecksService: IGeoChecksService; + diagnosticService: IDiagnosticService; + inviteService: IInviteService; + maintenanceWindowService: IMaintenanceWindowService; + monitorService: IMonitorService; + incidentService: IIncidentService; logger: ILogger; notificationsService: INotificationsService; statusPageService: IStatusPageService; @@ -129,7 +153,7 @@ export const initializeServices = async ({ }: { logger: ILogger; envSettings: EnvConfig; - settingsService: any; + settingsService: ISettingsService; settingsRepository: ISettingsRepository; }): Promise => { // Create DB @@ -152,8 +176,7 @@ export const initializeServices = async ({ const teamsRepository = new MongoTeamsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); - // Providers - + // Network providers const pingProvider = new PingProvider(ping); const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); @@ -194,6 +217,7 @@ export const initializeServices = async ({ const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository); + // Notification providers const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); const emailProvider = new EmailProvider(emailService, logger); @@ -277,7 +301,6 @@ export const initializeServices = async ({ const statusPageService = new StatusPageService(statusPagesRepository); const services = { - //v1 settingsService, db, networkService, diff --git a/server/src/db/IDb.ts b/server/src/db/IDb.ts new file mode 100644 index 000000000..4c719999f --- /dev/null +++ b/server/src/db/IDb.ts @@ -0,0 +1,4 @@ +export interface IDb { + connect(): Promise; + disconnect(): Promise; +} diff --git a/server/src/db/MongoDB.ts b/server/src/db/MongoDB.ts index d62f4bbad..82d05c3a2 100755 --- a/server/src/db/MongoDB.ts +++ b/server/src/db/MongoDB.ts @@ -1,10 +1,12 @@ import mongoose from "mongoose"; -import AppSettings from "./models/AppSettings.js"; -import { runMigrations } from "./migration/index.js"; +import AppSettings from "@/db/models/AppSettings.js"; +import { runMigrations } from "@/db/migration/index.js"; import { ILogger } from "@/utils/logger.js"; import { EnvConfig } from "@/service/system/settingsService.js"; const SERVICE_NAME = "MongoDB"; -class MongoDB { +import { IDb } from "@/db/IDb.js"; + +class MongoDB implements IDb { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -46,12 +48,12 @@ class MongoDB { }); await runMigrations(); - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "connect", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -63,12 +65,12 @@ class MongoDB { await mongoose.disconnect(); this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" }); return; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "disconnect", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); } }; diff --git a/server/src/index.ts b/server/src/index.ts index e5b679beb..1a6195d80 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,7 +9,7 @@ import fs from "fs"; import { runMigrations } from "./db/migration/index.js"; import Logger, { ILogger } from "@/utils/logger.js"; -import SettingsService from "@/service/system/settingsService.js"; +import { SettingsService } from "@/service/index.js"; import { MongoSettingsRepository } from "./repositories/index.js"; const SERVICE_NAME = "Server"; diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index abeb577ed..8779b0a00 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -19,7 +19,7 @@ export interface ICheckService { updateChecksTTL(params: { teamId: string; ttl: string }): Promise; } -class CheckService implements ICheckService { +export class CheckService implements ICheckService { static SERVICE_NAME = SERVICE_NAME; private monitorsRepository: IMonitorsRepository; @@ -195,4 +195,3 @@ class CheckService implements ICheckService { }; } -export default CheckService; diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index 67729ce98..f7a8b8ff9 100644 --- a/server/src/service/business/diagnosticService.ts +++ b/server/src/service/business/diagnosticService.ts @@ -3,15 +3,19 @@ import os from "os"; const SERVICE_NAME = "diagnosticService"; -interface MemoryUsageMetrics { - rss: number; - heapTotal: number; - heapUsed: number; - external: number; - arrayBuffers: number; +export interface IDiagnosticService { + getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>; + getSystemStats(): Promise<{ + osStats: { freeMemoryBytes: number; totalMemoryBytes: number }; + memoryUsage: Record; + cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number }; + v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number }; + eventLoopDelayMs: number; + uptimeMs: number; + }>; } -class DiagnosticService { +export class DiagnosticService implements IDiagnosticService { static SERVICE_NAME = SERVICE_NAME; constructor() { @@ -104,4 +108,3 @@ class DiagnosticService { }; } -export default DiagnosticService; diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 1e5720656..5c5869d94 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -16,7 +16,7 @@ export interface IGeoChecksService { getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise; } -class GeoChecksService implements IGeoChecksService { +export class GeoChecksService implements IGeoChecksService { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -189,4 +189,3 @@ class GeoChecksService implements IGeoChecksService { }; } -export default GeoChecksService; diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index db44bdbb2..81a98893a 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -5,12 +5,34 @@ import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; import { getDateForRange } from "@/utils/dataUtils.js"; import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; -import type { Incident } from "@/types/index.js"; +import type { Incident, IncidentSummary, User } from "@/types/index.js"; import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js"; import type { ILogger } from "@/utils/logger.js"; -class IncidentService { +export interface IIncidentService { + handleIncident( + monitor: Monitor, + code: number, + decision: MonitorActionDecision, + monitorStatusResponse?: MonitorStatusResponse + ): Promise; + resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise; + getIncidentsByTeam( + teamId: string, + sortOrder: string, + dateRange: string, + page: string, + rowsPerPage: string, + status: string, + monitorId: string, + resolutionType: string + ): Promise<{ incidents: Incident[]; count: number }>; + getIncidentSummary(teamId: string, limit?: string): Promise; + getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>; +} + +export class IncidentService implements IIncidentService { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -245,4 +267,3 @@ class IncidentService { }; } -export default IncidentService; diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index e4069d485..26b3b9dbe 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -5,7 +5,13 @@ import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "inviteService"; -class InviteService { +export interface IInviteService { + getInviteToken(params: { invite: Partial; teamId: string; userRoles: UserRole[] }): Promise; + sendInviteEmail(params: { invite: Partial; firstName: string; userRoles: UserRole[] }): Promise; + verifyInviteToken(params: { inviteToken: string }): Promise; +} + +export class InviteService implements IInviteService { static SERVICE_NAME = SERVICE_NAME; private settingsService: any; @@ -89,4 +95,3 @@ class InviteService { }; } -export default InviteService; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 816bdca59..2257718f3 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,10 +1,23 @@ import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; +import type { MaintenanceWindow } from "@/types/index.js"; import { ParseBoolean } from "@/utils/utils.js"; import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "maintenanceWindowService"; -class MaintenanceWindowService { +export interface IMaintenanceWindowService { + createMaintenanceWindow(params: { teamId: string; body: Partial & { monitors: string[] } }): Promise; + getMaintenanceWindowById(params: { id: string; teamId: string }): Promise; + getMaintenanceWindowsByTeamId(params: { + teamId: string; + query: Record; + }): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>; + getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise; + deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise; + editMaintenanceWindow(params: { id: string; teamId: string; body: Partial }): Promise; +} + +export class MaintenanceWindowService implements IMaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; private monitorsRepository: IMonitorsRepository; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; @@ -84,4 +97,3 @@ class MaintenanceWindowService { }; } -export default MaintenanceWindowService; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index d05f027b1..fde9d6292 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -11,10 +11,31 @@ import { canManageRole, type UserRole } from "@/types/user.js"; import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; - +import { IEmailService } from "@/service/infrastructure/emailService.js"; +import { ISettingsService } from "@/service/system/settingsService.js"; +import { ILogger } from "@/utils/logger.js"; +import jwt from "jsonwebtoken"; +type JwtType = typeof jwt; const SERVICE_NAME = "userService"; -class UserService { +export interface IUserService { + issueToken(payload: Partial, appSettings: { jwtTTL?: string; jwtSecret?: string }): string; + registerUser(user: Partial, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>; + createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: unknown): Promise; + loginUser(email: string, password: string): Promise<{ user: User; token: string }>; + editUser(updates: Partial, file: unknown, currentUser: User): Promise; + checkSuperadminExists(): Promise; + requestRecovery(email: string): Promise; + validateRecovery(recoveryToken: string): Promise; + resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>; + deleteUser(user: User): Promise; + getAllUsers(): Promise; + getUserById(roles: UserRole[], userId: string): Promise; + editUserById(userId: string, patch: Partial): Promise; + setPasswordByUserId(userId: string, password: string): Promise; +} + +export class UserService implements IUserService { static SERVICE_NAME = SERVICE_NAME; private hashPassword = (password: string): string => { @@ -22,10 +43,10 @@ class UserService { return bcrypt.hashSync(password, salt); }; - private emailService: any; - private settingsService: any; - private logger: any; - private jwt: any; + private emailService: IEmailService; + private settingsService: ISettingsService; + private logger: ILogger; + private jwt: JwtType; private jobQueue: ISuperSimpleQueue; private crypto: any; private monitorsRepository: IMonitorsRepository; @@ -50,10 +71,10 @@ class UserService { teamsRepository, }: { crypto: any; - emailService: any; - settingsService: any; - logger: any; - jwt: any; + emailService: IEmailService; + settingsService: ISettingsService; + logger: ILogger; + jwt: JwtType; jobQueue: ISuperSimpleQueue; monitorsRepository: IMonitorsRepository; usersRepository: IUsersRepository; @@ -134,20 +155,23 @@ class UserService { const html = await this.emailService.buildEmail("welcomeEmailTemplate", { name: newUser.firstName, }); - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => { + if (!html) { + throw new Error("Failed to build welcome email HTML"); + } + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => { this.logger.warn({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "registerUser", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); }); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "registerUser", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); } @@ -313,13 +337,13 @@ class UserService { if (roles.includes("superadmin")) { // 2. Remove all jobs, delete checks and alerts - res && - res?.length > 0 && - (await Promise.all( + if (res && res.length > 0) { + await Promise.all( res.map(async (monitor) => { await this.jobQueue.deleteJob(monitor.id); }) - )); + ); + } } // 6. Delete the user by id await this.usersRepository.deleteById(userId); @@ -348,4 +372,3 @@ class UserService { return updatedUser; }; } -export default UserService; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 7ec49288f..464866e09 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,14 +1,33 @@ +// Business services +export * from "@/service/business/checkService.js"; +export * from "@/service/business/diagnosticService.js"; +export * from "@/service/business/geoChecksService.js"; +export * from "@/service/business/incidentService.js"; +export * from "@/service/business/inviteService.js"; +export * from "@/service/business/maintenanceWindowService.js"; export * from "@/service/business/monitorService.js"; +export * from "@/service/business/statusPageService.js"; +export * from "@/service/business/userService.js"; + +// Infrastructure services +export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +export * from "@/service/infrastructure/notificationMessageBuilder.js"; +export * from "@/service/infrastructure/bufferService.js"; +export * from "@/service/infrastructure/emailService.js"; +export * from "@/service/infrastructure/globalPingService.js"; export * from "@/service/infrastructure/networkService.js"; +export * from "@/service/infrastructure/notificationsService.js"; +export * from "@/service/infrastructure/statusService.js"; + +// Notification providers export * from "@/service/infrastructure/notificationProviders/discord.js"; export * from "@/service/infrastructure/notificationProviders/email.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; -export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; export * from "@/service/infrastructure/notificationProviders/matrix.js"; +export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; export * from "@/service/infrastructure/notificationProviders/slack.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; -export * from "@/service/infrastructure/notificationsService.js"; -export * from "@/service/infrastructure/statusService.js"; -export * from "@/service/infrastructure/notificationProviders/webhook.js"; -export * from "@/service/infrastructure/notificationProviders/matrix.js"; -export * from "@/service/business/statusPageService.js"; + +// System services +export * from "@/service/system/settingsService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 0eb645cb7..216a821b1 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -53,7 +53,7 @@ export interface ISuperSimpleQueue { obliterate(): Promise; } -class SuperSimpleQueue implements ISuperSimpleQueue { +export class SuperSimpleQueue implements ISuperSimpleQueue { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -290,4 +290,3 @@ class SuperSimpleQueue implements ISuperSimpleQueue { }; } -export default SuperSimpleQueue; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 3946f620a..11a44d1bd 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,10 +1,8 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; +import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js"; import type { StatusChangeResult } from "@/types/index.js"; -import IncidentService from "@/service/business/incidentService.js"; -import type { IGeoChecksService } from "@/service/business/geoChecksService.js"; import { IMaintenanceWindowsRepository, IMonitorsRepository, @@ -15,7 +13,7 @@ import { IGeoChecksRepository, } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; -import { IBufferService } from "../bufferService.js"; +import { IBufferService } from "@/service/index.js"; export interface ISuperSimpleQueueHelper { readonly serviceName: string; @@ -39,7 +37,7 @@ export interface MonitorActionDecision { }; } -class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { +export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private logger: any; @@ -415,4 +413,3 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { } } -export default SuperSimpleQueueHelper; diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 318fd35cd..ae89a1c09 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -15,7 +15,7 @@ export interface IBufferService { flushGeoBuffer(): Promise; } -class BufferService implements IBufferService { +export class BufferService implements IBufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; private logger: ILogger; @@ -178,4 +178,3 @@ class BufferService implements IBufferService { } } -export default BufferService; diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index 5246351c0..180a728f3 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -23,7 +23,7 @@ export interface IEmailService { sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise; } -class EmailService implements IEmailService { +export class EmailService implements IEmailService { static SERVICE_NAME = SERVICE_NAME; private settingsService: ISettingsService; @@ -179,4 +179,3 @@ class EmailService implements IEmailService { }; } -export default EmailService; diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index 883b9f76b..0a65412f4 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -54,7 +54,7 @@ export interface IGlobalPingService { pollForResults(measurementId: string, timeoutMs?: number): Promise; } -class GlobalPingService implements IGlobalPingService { +export class GlobalPingService implements IGlobalPingService { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -207,4 +207,3 @@ class GlobalPingService implements IGlobalPingService { } } -export default GlobalPingService; diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index f9c79ac29..a459c6480 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -23,7 +23,7 @@ export interface INetworkService { }>; } -class NetworkService implements INetworkService { +export class NetworkService implements INetworkService { static SERVICE_NAME = SERVICE_NAME; private axios: AxiosStatic; @@ -198,4 +198,3 @@ class NetworkService implements INetworkService { } } -export default NetworkService; diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 58c896dc6..46959c554 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -21,7 +21,7 @@ export interface ISettingsService { getDBSettings(): Promise; } -class SettingsService implements ISettingsService { +export class SettingsService implements ISettingsService { static SERVICE_NAME = SERVICE_NAME; private settings: EnvConfig; private settingsRepository: ISettingsRepository; @@ -75,4 +75,3 @@ class SettingsService implements ISettingsService { }; } -export default SettingsService; From 69e98a51ec03bde22dcb763f9d71b3c6a49df477 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:14:12 -0800 Subject: [PATCH 24/53] resolve ts errors --- server/src/service/business/userService.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index fde9d6292..7cd1491c3 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -141,7 +141,7 @@ export class UserService implements IUserService { message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser.id, + details: { userId: newUser.id }, }); delete newUser.profileImage; @@ -205,7 +205,7 @@ export class UserService implements IUserService { message: "New user created by superadmin", service: SERVICE_NAME, method: "createUser", - details: newUser.id, + details: { userId: newUser.id }, }); newUser.profileImage = undefined; @@ -280,6 +280,14 @@ export class UserService implements IUserService { email, url, }); + if (!html) { + throw new AppError({ + message: "Failed to build password reset email HTML", + service: SERVICE_NAME, + method: "requestRecovery", + status: 500, + }); + } const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); return msgId; }; From 990dc7875ab3ec5f9fad7bd36ca5ef2054286de4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:16:22 -0800 Subject: [PATCH 25/53] format --- server/src/service/business/checkService.ts | 1 - server/src/service/business/diagnosticService.ts | 1 - server/src/service/business/geoChecksService.ts | 1 - server/src/service/business/incidentService.ts | 1 - server/src/service/business/inviteService.ts | 1 - server/src/service/business/maintenanceWindowService.ts | 1 - .../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts | 1 - .../infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts | 1 - server/src/service/infrastructure/bufferService.ts | 1 - server/src/service/infrastructure/emailService.ts | 1 - server/src/service/infrastructure/globalPingService.ts | 1 - server/src/service/infrastructure/networkService.ts | 1 - server/src/service/system/settingsService.ts | 1 - 13 files changed, 13 deletions(-) diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8779b0a00..c83a3d411 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -194,4 +194,3 @@ export class CheckService implements ICheckService { throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); }; } - diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index f7a8b8ff9..f9c53f633 100644 --- a/server/src/service/business/diagnosticService.ts +++ b/server/src/service/business/diagnosticService.ts @@ -107,4 +107,3 @@ export class DiagnosticService implements IDiagnosticService { return diagnostics; }; } - diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 5c5869d94..4a31348b0 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -188,4 +188,3 @@ export class GeoChecksService implements IGeoChecksService { return result; }; } - diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 81a98893a..710336016 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -266,4 +266,3 @@ export class IncidentService implements IIncidentService { } }; } - diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index 26b3b9dbe..9d1cbf59d 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -94,4 +94,3 @@ export class InviteService implements IInviteService { return await this.invitesRepository.findByToken(inviteToken); }; } - diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 2257718f3..abf513eb5 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -96,4 +96,3 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; } - diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 216a821b1..2857f5b82 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -289,4 +289,3 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; } - diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 11a44d1bd..011008b32 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -412,4 +412,3 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return decision; } } - diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index ae89a1c09..229c5e7d4 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -177,4 +177,3 @@ export class BufferService implements IBufferService { } } } - diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index 180a728f3..f536f3f18 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -178,4 +178,3 @@ export class EmailService implements IEmailService { } }; } - diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index 0a65412f4..b58486ad3 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -206,4 +206,3 @@ export class GlobalPingService implements IGlobalPingService { return new Promise((resolve) => setTimeout(resolve, ms)); } } - diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index a459c6480..e475bc24b 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -197,4 +197,3 @@ export class NetworkService implements INetworkService { } } } - diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 46959c554..1d75c0a36 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -74,4 +74,3 @@ export class SettingsService implements ISettingsService { return settings; }; } - From 24b9b649791c45f085db82aa8e316183eb7c9a69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:35:07 -0800 Subject: [PATCH 26/53] manually build response --- .../infrastructure/network/PingProvider.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/server/src/service/infrastructure/network/PingProvider.ts b/server/src/service/infrastructure/network/PingProvider.ts index 2749584f5..03181b078 100644 --- a/server/src/service/infrastructure/network/PingProvider.ts +++ b/server/src/service/infrastructure/network/PingProvider.ts @@ -4,7 +4,7 @@ import { MonitorType, Monitor } from "@/types/monitor.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import ping from "ping"; -import { buildStatusResponse, timeRequest } from "@/service/infrastructure/network/utils.js"; +import { timeRequest } from "@/service/infrastructure/network/utils.js"; const SERVICE_NAME = "PingProvider"; type Ping = typeof ping; @@ -33,7 +33,7 @@ export class PingProvider implements IStatusProvider { const sanitizedHost = this.sanitizeHost(monitor.url); const { response, error } = await timeRequest(() => this.ping.promise.probe(sanitizedHost)); - const safeTime = typeof response?.time === "number" ? response.time : parseFloat(String(response?.time)) || 0; + if (error) { throw error; } @@ -42,16 +42,19 @@ export class PingProvider implements IStatusProvider { throw new Error(`No response from ping for host: ${sanitizedHost}`); } - return buildStatusResponse({ - monitor, + const responseTime = + typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0; + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response.alive ?? false, + code: response.alive ? 200 : 5000, + message: response.alive ? "Success" : "Ping failed", + responseTime, payload: response, - overrides: { - status: response.alive ?? false, - code: 200, - message: "Success", - responseTime: safeTime, - }, - }); + }; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); throw new AppError({ From 6ecb27d522cb1eec1ce6be7b6c395e9824a31a4e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:35:52 -0800 Subject: [PATCH 27/53] remove buildStatusResponse --- .../service/infrastructure/network/utils.ts | 71 ------------------- 1 file changed, 71 deletions(-) diff --git a/server/src/service/infrastructure/network/utils.ts b/server/src/service/infrastructure/network/utils.ts index c70f06bf8..ddb962f47 100644 --- a/server/src/service/infrastructure/network/utils.ts +++ b/server/src/service/infrastructure/network/utils.ts @@ -1,8 +1,3 @@ -import { HTTPError, RequestError } from "got"; -import type { Response } from "got"; -import type { Monitor } from "@/types/monitor.js"; -import { MonitorStatusResponse, MonitorStatusResponseOverrides } from "@/types/network.js"; - export const timeRequest = async (operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> => { const start = process.hrtime.bigint(); try { @@ -17,69 +12,3 @@ export const timeRequest = async (operation: () => Promise): Promise<{ res export const NETWORK_ERROR = 5000; export const PING_ERROR = 5001; - -interface BuildStatusResponseArgs { - monitor: Monitor; - response?: Response | null; - error?: Error | RequestError | HTTPError | null; - payload?: T | null; - jsonPath?: string; - matchMethod?: MonitorStatusResponse["matchMethod"]; - expectedValue?: string; - extracted?: unknown; - overrides?: MonitorStatusResponseOverrides; -} - -export const buildStatusResponse = ({ - monitor, - response, - error, - payload, - jsonPath, - matchMethod, - expectedValue, - extracted, - overrides, -}: BuildStatusResponseArgs): MonitorStatusResponse => { - if (error) { - const statusResponse: MonitorStatusResponse = { - monitorId: monitor.id, - teamId: monitor.teamId, - type: monitor.type, - status: false, - code: NETWORK_ERROR, - message: error.message ?? "Network error", - responseTime: 0, - timings: undefined, - jsonPath, - matchMethod, - expectedValue, - extracted, - payload, - }; - - if (error instanceof HTTPError || error instanceof RequestError) { - statusResponse.code = error?.response?.statusCode ?? NETWORK_ERROR; - statusResponse.message = error.message; - statusResponse.responseTime = error.timings?.phases?.total ?? 0; - statusResponse.timings = error.timings; - } - return { ...statusResponse, ...(overrides ?? {}) }; - } - - return { - monitorId: monitor.id, - teamId: monitor.teamId, - type: monitor.type, - status: response?.ok ?? false, - code: response?.statusCode ?? NETWORK_ERROR, - message: response?.statusMessage ?? "", - responseTime: response?.timings?.phases?.total ?? 0, - timings: response?.timings, - payload: payload ?? response?.body, - jsonPath, - matchMethod, - expectedValue, - extracted, - }; -}; From 9a8a175f7cdff86638ad0412c23505d6cf651fd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:37:45 -0800 Subject: [PATCH 28/53] format --- server/src/service/infrastructure/network/PingProvider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/service/infrastructure/network/PingProvider.ts b/server/src/service/infrastructure/network/PingProvider.ts index 03181b078..0b9691da9 100644 --- a/server/src/service/infrastructure/network/PingProvider.ts +++ b/server/src/service/infrastructure/network/PingProvider.ts @@ -42,8 +42,7 @@ export class PingProvider implements IStatusProvider { throw new Error(`No response from ping for host: ${sanitizedHost}`); } - const responseTime = - typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0; + const responseTime = typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0; return { monitorId: monitor.id, From 35f018623f22b630f39befc389e7b2efe34180de Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 09:45:33 -0800 Subject: [PATCH 29/53] fix prettier hook --- client/package.json | 2 +- server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/package.json b/client/package.json index ba2d59275..b6c48e1c1 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "format-check": "prettier --check ." }, "lint-staged": { - "**/*": "npm run format" + "**/*": "prettier --write" }, "dependencies": { "@emotion/react": "^11.13.3", diff --git a/server/package.json b/server/package.json index ce2c0822e..cb3ae3131 100755 --- a/server/package.json +++ b/server/package.json @@ -15,7 +15,7 @@ "format-check": "prettier --check ." }, "lint-staged": { - "**/*": "npm run format" + "**/*": "prettier --write" }, "keywords": [], "author": "", From 124a8c0b07ba21711e3844818fd8f84588006d00 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 10:37:29 -0800 Subject: [PATCH 30/53] ping type globalping checks --- client/src/Pages/CreateMonitor/index.tsx | 2 +- client/src/Pages/Uptime/Details/index.tsx | 12 ++- .../geo-checks/MongoGeoChecksRepository.ts | 7 +- .../src/service/business/geoChecksService.ts | 2 +- server/src/service/business/monitorService.ts | 2 +- .../SuperSimpleQueue/SuperSimpleQueue.ts | 9 ++- .../SuperSimpleQueueHelper.ts | 11 ++- .../infrastructure/globalPingService.ts | 74 +++++++++++++------ server/src/types/geoCheck.ts | 1 + 9 files changed, 80 insertions(+), 40 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 34e141795..dac5189ba 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -885,7 +885,7 @@ const CreateMonitorPage = () => { /> )} - {watchedType === "http" && ( + {(watchedType === "http" || watchedType === "ping") && ( { ); const geoChecksUrl = useMemo(() => { - if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) { + if ( + !monitorId || + (monitor?.type !== "http" && monitor?.type !== "ping") || + !monitor?.geoCheckEnabled + ) { return null; } const params = new URLSearchParams(); @@ -141,7 +145,11 @@ const UptimeDetailsPage = () => { // Fetch paginated geo checks for the table const geoChecksTableUrl = useMemo(() => { - if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) { + if ( + !monitorId || + (monitor?.type !== "http" && monitor?.type !== "ping") || + !monitor?.geoCheckEnabled + ) { return null; } const params = new URLSearchParams(); diff --git a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts index 7633fcf7c..e1db5ab1d 100644 --- a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts @@ -1,17 +1,18 @@ import { IGeoChecksRepository } from "./IGeoChecksRepository.js"; import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js"; -import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js"; +import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js"; import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "GeoChecksRepository"; class MongoGeoChecksRepository implements IGeoChecksRepository { static SERVICE_NAME = SERVICE_NAME; - private logger: any; - constructor(logger: any) { + private logger: ILogger; + constructor(logger: ILogger) { this.logger = logger; } diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 4a31348b0..f7c5b33ea 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -68,7 +68,7 @@ export class GeoChecksService implements IGeoChecksService { } // Step 1: Create measurement request - const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations); + const measurementId = await this.globalPingService.createMeasurement(monitor.type, monitor.url, monitor.geoCheckLocations); if (!measurementId) { // GlobalPing API is down, skip this check diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b94d99c90..fe211c598 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -340,7 +340,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); } - if (monitor.type !== "http" || !monitor.geoCheckEnabled) { + if ((monitor.type !== "http" && monitor.type !== "ping") || !monitor.geoCheckEnabled) { return { groupedGeoChecks: [] }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 2857f5b82..3fb67bcfc 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -125,8 +125,13 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { data: monitor, }); + // Return early if we don't need geo checks + if (monitor.type !== "http" && monitor.type !== "ping") { + return; + } + // Add geo check job if enabled for HTTP monitors - if (monitor.geoCheckEnabled && monitor.type === "http") { + if (monitor.geoCheckEnabled) { this.scheduler.addJob({ id: `${monitorId}-geo`, template: "geo-check-job", @@ -175,7 +180,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { // Handle geo check job lifecycle const geoJobId = `${monitor.id}-geo`; - if (monitor.geoCheckEnabled && monitor.type === "http") { + if (monitor.geoCheckEnabled && (monitor.type === "http" || monitor.type === "ping")) { // Check if geo job exists const existingGeoJob = await this.scheduler.getJob(geoJobId); if (existingGeoJob) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 011008b32..766f2d907 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -280,19 +280,18 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" }); } - if (monitor.type !== "http") { + if (!monitor.geoCheckEnabled) { + return; + } + if (monitor.type !== "http" && monitor.type !== "ping") { this.logger.debug({ - message: `Monitor ${monitorId} is not HTTP type, skipping geo check`, + message: `Monitor ${monitorId} is not HTTP or Ping type, skipping geo check`, service: SERVICE_NAME, method: "getGeoCheckJob", }); return; } - if (!monitor.geoCheckEnabled) { - return; - } - if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) { this.logger.warn({ message: `No geo check locations configured for monitor ${monitorId}`, diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index b58486ad3..ff67dfd99 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -1,4 +1,5 @@ import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js"; +import { MonitorType } from "@/types/index.js"; import type { ILogger } from "@/utils/logger.js"; import got from "got"; @@ -8,7 +9,7 @@ const POLL_INTERVAL_MS = 2000; const MAX_POLL_TIMEOUT_MS = 30000; interface GlobalPingMeasurementRequest { - type: "http"; + type: "http" | "ping"; target: string; locations: Array<{ continent: GeoContinent }>; limit: number; @@ -44,13 +45,22 @@ interface GlobalPingProbeResult { firstByte: number; download: number; }; + stats?: { + min: number; + max: number; + avg: number; + total: number; + loss: number; + rcv: number; + drop: number; + }; rawOutput?: string; }; } export interface IGlobalPingService { readonly serviceName: string; - createMeasurement(url: string, locations: GeoContinent[]): Promise; + createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise; pollForResults(measurementId: string, timeoutMs?: number): Promise; } @@ -67,13 +77,16 @@ export class GlobalPingService implements IGlobalPingService { return GlobalPingService.SERVICE_NAME; } - async createMeasurement(url: string, locations: GeoContinent[]): Promise { + async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise { try { + if (monitorType !== "http" && monitorType !== "ping") { + throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`); + } // GlobalPing API expects target without protocol (http:// or https://) const cleanTarget = url.replace(/^https?:\/\//, ""); const requestBody: GlobalPingMeasurementRequest = { - type: "http", + type: monitorType, target: cleanTarget, locations: locations.map((continent) => ({ continent })), limit: locations.length, @@ -88,10 +101,9 @@ export class GlobalPingService implements IGlobalPingService { const measurementId = response.body.id; this.logger.debug({ - message: `Created GlobalPing measurement: ${measurementId}`, + message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`, service: SERVICE_NAME, method: "createMeasurement", - details: { measurementId, url, locations }, }); return measurementId; @@ -165,8 +177,7 @@ export class GlobalPingService implements IGlobalPingService { const successfulResults: GeoCheckResult[] = []; for (const probeResult of probeResults) { - // Skip failed or timeout results - if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) { + if (probeResult.result.status !== "finished") { continue; } @@ -180,23 +191,38 @@ export class GlobalPingService implements IGlobalPingService { latitude: probeResult.probe.latitude, }; - const timings: GeoCheckTimings = { - total: probeResult.result.timings.total, - dns: probeResult.result.timings.dns, - tcp: probeResult.result.timings.tcp, - tls: probeResult.result.timings.tls, - firstByte: probeResult.result.timings.firstByte, - download: probeResult.result.timings.download, - }; + // HTTP results have statusCode and timings, ping results have stats + if (probeResult.result.statusCode && probeResult.result.timings) { + const timings: GeoCheckTimings = { + total: probeResult.result.timings.total, + dns: probeResult.result.timings.dns, + tcp: probeResult.result.timings.tcp, + tls: probeResult.result.timings.tls, + firstByte: probeResult.result.timings.firstByte, + download: probeResult.result.timings.download, + }; - const result: GeoCheckResult = { - location, - status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300, - statusCode: probeResult.result.statusCode, - timings, - }; - - successfulResults.push(result); + successfulResults.push({ + location, + status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300, + statusCode: probeResult.result.statusCode, + timings, + }); + } else if (probeResult.result.stats) { + successfulResults.push({ + location, + status: probeResult.result.stats.loss === 0, + statusCode: probeResult.result.stats.loss === 0 ? 200 : 5000, + timings: { + total: probeResult.result.stats.avg, + dns: 0, + tcp: 0, + tls: 0, + firstByte: 0, + download: 0, + }, + }); + } } return successfulResults; diff --git a/server/src/types/geoCheck.ts b/server/src/types/geoCheck.ts index 548c4c65f..6a75592a9 100644 --- a/server/src/types/geoCheck.ts +++ b/server/src/types/geoCheck.ts @@ -33,6 +33,7 @@ export interface GeoCheckResult { status: boolean; statusCode: number; timings: GeoCheckTimings; + } export interface GeoCheck { From 2f1a164c26d3d139e5e31e54c57e08e9cdfe9626 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 10:46:00 -0800 Subject: [PATCH 31/53] remove rtt --- client/src/Pages/Uptime/Details/index.tsx | 14 +++----------- client/src/Types/Monitor.ts | 9 +++++++++ .../src/service/business/geoChecksService.ts | 19 +++---------------- server/src/service/business/monitorService.ts | 3 ++- .../SuperSimpleQueue/SuperSimpleQueue.ts | 5 +++-- .../SuperSimpleQueueHelper.ts | 5 +++-- .../infrastructure/globalPingService.ts | 5 +++-- server/src/types/monitor.ts | 3 +++ 8 files changed, 29 insertions(+), 34 deletions(-) diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index bea41705f..8828aa29a 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -20,7 +20,7 @@ import { useState, useMemo } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; import { useGet } from "@/Hooks/UseApi"; -import type { MonitorDetailsResponse } from "@/Types/Monitor"; +import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor"; import type { ChecksResponse } from "@/Types/Check"; import type { GeoChecksResult, @@ -122,11 +122,7 @@ const UptimeDetailsPage = () => { ); const geoChecksUrl = useMemo(() => { - if ( - !monitorId || - (monitor?.type !== "http" && monitor?.type !== "ping") || - !monitor?.geoCheckEnabled - ) { + if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { return null; } const params = new URLSearchParams(); @@ -145,11 +141,7 @@ const UptimeDetailsPage = () => { // Fetch paginated geo checks for the table const geoChecksTableUrl = useMemo(() => { - if ( - !monitorId || - (monitor?.type !== "http" && monitor?.type !== "ping") || - !monitor?.geoCheckEnabled - ) { + if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { return null; } const params = new URLSearchParams(); diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 01cae62e9..b87e65bc6 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -16,6 +16,15 @@ export const MonitorTypes = [ ] as const; export type MonitorType = (typeof MonitorTypes)[number]; +export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; + +export const supportsGeoCheck = (type: MonitorType | undefined): boolean => { + if (!type) { + return false; + } + return GeoCheckSupportedTypes.includes(type); +}; + export const MonitorStatuses = [ "up", "down", diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index f7c5b33ea..f35826a8f 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -76,7 +76,6 @@ export class GeoChecksService implements IGeoChecksService { message: "Skipping geo check due to API unavailability", service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id }, }); return null; } @@ -90,7 +89,6 @@ export class GeoChecksService implements IGeoChecksService { message: "No successful geo check results", service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id, measurementId }, }); return null; } @@ -102,17 +100,16 @@ export class GeoChecksService implements IGeoChecksService { message: `Geo check completed for monitor ${monitor.id}`, service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id, resultsCount: results.length }, }); return geoCheck; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ message: "Error executing geo check", service: SERVICE_NAME, method: "buildGeoCheck", - details: { monitorId: monitor.id, error: error.message }, - stack: error.stack, + details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" }, + stack: error instanceof Error ? error.stack : undefined, }); return null; } @@ -172,19 +169,9 @@ export class GeoChecksService implements IGeoChecksService { const { sortOrder, dateRange, page, rowsPerPage, continent } = query; const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined; - - this.logger.debug({ - message: "getGeoChecksByMonitor query params", - service: SERVICE_NAME, - method: "getGeoChecksByMonitor", - details: { continent, continents, query }, - }); - const parsedPage = page ? parseInt(page) : page; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; - const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents); - return result; }; } diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index fe211c598..b197c04bd 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -8,6 +8,7 @@ import type { PageSpeedDetailsResult, GamesMap, } from "@/types/monitor.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; import type { GeoContinent } from "@/types/geoCheck.js"; import type { IChecksRepository, @@ -340,7 +341,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); } - if ((monitor.type !== "http" && monitor.type !== "ping") || !monitor.geoCheckEnabled) { + if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) { return { groupedGeoChecks: [] }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 3fb67bcfc..a96e7855b 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -2,6 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import Scheduler from "super-simple-scheduler"; import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; const SERVICE_NAME = "JobQueue"; type QueueJobFailure = { @@ -126,7 +127,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); // Return early if we don't need geo checks - if (monitor.type !== "http" && monitor.type !== "ping") { + if (!supportsGeoCheck(monitor.type)) { return; } @@ -180,7 +181,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { // Handle geo check job lifecycle const geoJobId = `${monitor.id}-geo`; - if (monitor.geoCheckEnabled && (monitor.type === "http" || monitor.type === "ping")) { + if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) { // Check if geo job exists const existingGeoJob = await this.scheduler.getJob(geoJobId); if (existingGeoJob) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 766f2d907..eef0a75be 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,5 +1,6 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js"; import type { StatusChangeResult } from "@/types/index.js"; @@ -283,9 +284,9 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { if (!monitor.geoCheckEnabled) { return; } - if (monitor.type !== "http" && monitor.type !== "ping") { + if (!supportsGeoCheck(monitor.type)) { this.logger.debug({ - message: `Monitor ${monitorId} is not HTTP or Ping type, skipping geo check`, + message: `Monitor ${monitorId} type does not support geo checks, skipping`, service: SERVICE_NAME, method: "getGeoCheckJob", }); diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index ff67dfd99..41f96b286 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -1,4 +1,5 @@ import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js"; +import { supportsGeoCheck } from "@/types/monitor.js"; import { MonitorType } from "@/types/index.js"; import type { ILogger } from "@/utils/logger.js"; import got from "got"; @@ -9,7 +10,7 @@ const POLL_INTERVAL_MS = 2000; const MAX_POLL_TIMEOUT_MS = 30000; interface GlobalPingMeasurementRequest { - type: "http" | "ping"; + type: MonitorType; target: string; locations: Array<{ continent: GeoContinent }>; limit: number; @@ -79,7 +80,7 @@ export class GlobalPingService implements IGlobalPingService { async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise { try { - if (monitorType !== "http" && monitorType !== "ping") { + if (!supportsGeoCheck(monitorType)) { throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`); } // GlobalPing API expects target without protocol (http:// or https://) diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index a6ecc4ef4..efbaa9632 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -6,6 +6,9 @@ export type { GeoContinent } from "@/types/geoCheck.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; +export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; +export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type); + export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; From 206e28d1b999b456f23a77fa4b5e1d71d0190f3e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 11:01:38 -0800 Subject: [PATCH 32/53] user helper --- client/src/Pages/CreateMonitor/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index dac5189ba..9a310f3b1 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -32,7 +32,7 @@ import { import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; -import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor"; +import { type Monitor, type MonitorType, type GamesMap, supportsGeoCheck } from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { MonitorFormData } from "@/Validation/monitor"; @@ -885,7 +885,7 @@ const CreateMonitorPage = () => { /> )} - {(watchedType === "http" || watchedType === "ping") && ( + {supportsGeoCheck(watchedType) && ( Date: Fri, 6 Mar 2026 11:02:20 -0800 Subject: [PATCH 33/53] format --- client/src/Pages/CreateMonitor/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 9a310f3b1..22719986e 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -32,7 +32,12 @@ import { import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; -import { type Monitor, type MonitorType, type GamesMap, supportsGeoCheck } from "@/Types/Monitor"; +import { + type Monitor, + type MonitorType, + type GamesMap, + supportsGeoCheck, +} from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { MonitorFormData } from "@/Validation/monitor"; From 116eb80fc9ab485dc1593fc5a1f74a1105e0d35a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 11:07:48 -0800 Subject: [PATCH 34/53] format --- server/src/types/geoCheck.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/types/geoCheck.ts b/server/src/types/geoCheck.ts index 6a75592a9..548c4c65f 100644 --- a/server/src/types/geoCheck.ts +++ b/server/src/types/geoCheck.ts @@ -33,7 +33,6 @@ export interface GeoCheckResult { status: boolean; statusCode: number; timings: GeoCheckTimings; - } export interface GeoCheck { From a424092fbd144a4d4450719d810cfcea79aee83d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 11:12:52 -0800 Subject: [PATCH 35/53] remove unused imports --- client/src/Components/layout/AppLayout.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/src/Components/layout/AppLayout.tsx b/client/src/Components/layout/AppLayout.tsx index 369bd0726..34822c5ae 100644 --- a/client/src/Components/layout/AppLayout.tsx +++ b/client/src/Components/layout/AppLayout.tsx @@ -1,9 +1,6 @@ import Box from "@mui/material/Box"; import { useState, useEffect, useRef } from "react"; import { useTheme } from "@mui/material/styles"; -import { useSelector } from "react-redux"; -import BackgroundSVG from "@/assets/Images/background.svg"; -import type { RootState } from "@/Types/state"; import { OfflineBanner } from "@/Components/design-elements"; import { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; @@ -13,7 +10,6 @@ interface AppLayoutProps { const AppLayout = ({ children }: AppLayoutProps) => { const theme = useTheme(); - const mode = useSelector((state: RootState) => state.ui.mode); const [serverUnreachable, setServerUnreachable] = useState(false); const retryIntervalRef = useRef(null); From 82998a7795b15f4ecf0aba2355f440edd0f5a3e3 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 6 Mar 2026 14:18:27 -0500 Subject: [PATCH 36/53] feat: add ability to remove team members ## Changes - Add DELETE /api/v1/auth/users/:userId endpoint (admin + superadmin) - Add deleteUserById service method with role hierarchy enforcement - Superadmins can remove admins and regular users - Admins can only remove regular users - No one can remove superadmins, demo users, or themselves via this endpoint - Add "Remove user" button with confirmation dialog on edit user page - Allow admins to view user details and access the edit user page - Add i18n strings for remove user button and confirmation dialog ## Access control - superadmin -> can remove admin, user - admin -> can remove user only - admin cannot remove superadmin or other admins (enforced on both backend and frontend) --- client/src/Pages/Account/EditUser/index.tsx | 74 ++++++++++++++++--- .../Pages/Account/components/TeamTable.tsx | 8 +- client/src/locales/en.json | 9 +++ server/src/controllers/authController.ts | 14 ++++ server/src/routes/authRoute.ts | 3 +- server/src/service/business/userService.ts | 45 ++++++++++- 6 files changed, 136 insertions(+), 17 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index a3ddebaba..482a2f7bb 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -14,9 +14,14 @@ import type { UserRole, User } from "@/Types/User"; import { useParams } from "react-router-dom"; import { useTheme } from "@mui/material"; import { useEditUserForm } from "@/Hooks/useEditUserForm"; -import { useGet, usePatch } from "@/Hooks/UseApi"; +import { useGet, usePatch, useDelete } from "@/Hooks/UseApi"; import type { EditUserFormData } from "@/Validation/editUser"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { DialogInput } from "@/Components/inputs/Dialog"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; interface RoleOption { id: UserRole; @@ -29,6 +34,12 @@ const EditUserPage = () => { const { userId } = useParams<{ userId: string }>(); const { resolver, defaults } = useEditUserForm(); const { patch, loading: isSaving } = usePatch(); + const { deleteFn, loading: isDeleting } = useDelete(); + const navigate = useNavigate(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const isAdmin = useIsAdmin(); + const isSuperAdmin = useIsSuperAdmin(); + const currentUser = useSelector((state: RootState) => state.auth.user); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -60,6 +71,21 @@ const EditUserPage = () => { setValue("role", newRoles, { shouldValidate: true }); }; + const canDeleteUser = + isAdmin && + userId !== currentUser?.id && + !user?.role?.includes("superadmin") && + !user?.role?.includes("demo") && + (isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin")); + + const handleDeleteUser = async () => { + const result = await deleteFn(`/auth/users/${userId}`); + if (result) { + setShowDeleteDialog(false); + navigate("/account", { state: { tab: "team" } }); + } + }; + const onSubmit = async (data: EditUserFormData) => { await patch(`/auth/users/${userId}`, data); }; @@ -165,17 +191,45 @@ const EditUserPage = () => { } /> - + {canDeleteUser && ( + + )} + + + setShowDeleteDialog(false)} + onConfirm={handleDeleteUser} + confirmColor="error" + confirmText={t("pages.editUser.buttons.removeUser")} + loading={isDeleting} + /> ); }; diff --git a/client/src/Pages/Account/components/TeamTable.tsx b/client/src/Pages/Account/components/TeamTable.tsx index ab8dcc3b4..289bfb331 100644 --- a/client/src/Pages/Account/components/TeamTable.tsx +++ b/client/src/Pages/Account/components/TeamTable.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Table } from "@/Components/design-elements"; import type { Header } from "@/Components/design-elements/Table"; -import { useIsSuperAdmin } from "@/Hooks/useIsAdmin"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; import type { User } from "@/Types/User"; interface TeamTableProps { @@ -13,7 +13,7 @@ interface TeamTableProps { export const TeamTable = ({ users }: TeamTableProps) => { const { t } = useTranslation(); const navigate = useNavigate(); - const isSuperAdmin = useIsSuperAdmin(); + const isAdmin = useIsAdmin(); const headers: Header[] = [ { @@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => { ]; const handleRowClick = (row: User) => { - if (isSuperAdmin) { + if (isAdmin) { navigate(`/account/team/${row.id}`); } }; @@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => { ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6bf41478..a6d5a8d41 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -619,6 +619,15 @@ "title": "Roles", "description": "Assign roles to the user. Multiple roles can be selected." } + }, + "buttons": { + "removeUser": "Remove user" + }, + "dialog": { + "removeUser": { + "title": "Remove user", + "content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone." + } } }, "incidents": { diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 8add39087..f196e4dd5 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -180,6 +180,20 @@ class AuthController { } }; + deleteUserById = async (req: Request, res: Response, next: NextFunction) => { + try { + getUserByIdParamValidation.parse(req.params); + const targetUserId = req.params.userId; + await this.userService.deleteUserById(req.user, targetUserId); + return res.status(200).json({ + success: true, + msg: "User removed successfully", + }); + } catch (error) { + next(error); + } + }; + getAllUsers = async (req: Request, res: Response, next: NextFunction) => { try { const allUsers = await this.userService.getAllUsers(); diff --git a/server/src/routes/authRoute.ts b/server/src/routes/authRoute.ts index cc1ec3fd6..2e5fa1069 100755 --- a/server/src/routes/authRoute.ts +++ b/server/src/routes/authRoute.ts @@ -26,9 +26,10 @@ class AuthRoutes { this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), upload.single("profileImage"), this.authController.createUser); - this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById); + this.router.get("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getUserById); this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById); this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); + this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById); this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser); this.router.delete("/user", verifyJWT, this.authController.deleteUser); diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 7cd1491c3..48f3a51cf 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -29,6 +29,7 @@ export interface IUserService { validateRecovery(recoveryToken: string): Promise; resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>; deleteUser(user: User): Promise; + deleteUserById(actor: User, targetUserId: string): Promise; getAllUsers(): Promise; getUserById(roles: UserRole[], userId: string): Promise; editUserById(userId: string, patch: Partial): Promise; @@ -357,13 +358,53 @@ export class UserService implements IUserService { await this.usersRepository.deleteById(userId); }; + deleteUserById = async (actor: User, targetUserId: string) => { + if (actor.id === targetUserId) { + throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); + } + + const targetUser = await this.usersRepository.findById(targetUserId); + + if (targetUser.teamId !== actor.teamId) { + throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 }); + } + + if (targetUser.role.includes("demo")) { + throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); + } + + const actorRoles = actor.role; + const targetRoles = targetUser.role; + + // Check actor can manage all of target's roles + for (const targetRole of targetRoles) { + const canManage = actorRoles.some((actorRole) => canManageRole(actorRole, targetRole)); + if (!canManage) { + throw new AppError({ + message: "You do not have permission to remove this user", + service: SERVICE_NAME, + method: "deleteUserById", + status: 403, + }); + } + } + + await this.usersRepository.deleteById(targetUserId); + + this.logger.info({ + message: `User ${targetUserId} deleted by ${actor.id}`, + service: SERVICE_NAME, + method: "deleteUserById", + }); + }; + getAllUsers = async () => { return await this.usersRepository.findAll(); }; getUserById = async (roles: any, userId: any) => { - if (!roles.includes("superadmin")) { - throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 }); + if (!roles.includes("superadmin") && !roles.includes("admin")) { + throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 }); } const user = await this.usersRepository.findById(userId); From 55bdae77f965a2bf025834f8591e560fc6c01d85 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:56:58 +0530 Subject: [PATCH 37/53] Feat: Backend types - Register websocket type and payload --- client/src/Types/Monitor.ts | 1 + server/package-lock.json | 90 ++++++------------------------------- server/package.json | 2 + server/src/types/monitor.ts | 2 +- server/src/types/network.ts | 8 +++- 5 files changed, 25 insertions(+), 78 deletions(-) diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index b87e65bc6..053b517d1 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -12,6 +12,7 @@ export const MonitorTypes = [ "port", "game", "grpc", + "websocket", "unknown", ] as const; export type MonitorType = (typeof MonitorTypes)[number]; diff --git a/server/package-lock.json b/server/package-lock.json index 889c08f15..4b751858f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,6 +12,7 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", + "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -43,6 +44,7 @@ "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0", + "ws": "^8.19.0", "zod": "^4.3.6" }, "devDependencies": { @@ -871,7 +873,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1501,7 +1502,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1546,7 +1546,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -4338,7 +4337,6 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4499,7 +4497,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4665,6 +4662,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4727,7 +4733,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5341,7 +5346,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5920,7 +5924,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6863,7 +6866,6 @@ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", "license": "MIT", - "peer": true, "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" @@ -7631,7 +7633,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7993,7 +7994,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9638,7 +9638,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -12509,7 +12508,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14443,63 +14441,6 @@ "node": ">=8" } }, - "node_modules/svgo": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", - "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", - "license": "MIT", - "optional": true, - "dependencies": { - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0", - "sax": "^1.5.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/svgo/node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/svgo/node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0", - "optional": true - }, "node_modules/swagger-ui-dist": { "version": "5.30.0", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", @@ -14662,7 +14603,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14831,7 +14771,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14994,7 +14933,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15628,9 +15566,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/server/package.json b/server/package.json index cb3ae3131..17ed45295 100755 --- a/server/package.json +++ b/server/package.json @@ -27,6 +27,7 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", + "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -58,6 +59,7 @@ "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0", + "ws": "^8.19.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index efbaa9632..89740162b 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -3,7 +3,7 @@ export type { CheckSnapshot } from "@/types/check.js"; import type { GeoContinent } from "@/types/geoCheck.js"; export type { GeoContinent } from "@/types/geoCheck.js"; -export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const; +export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 93ddc4ac9..07e7c3736 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -24,7 +24,8 @@ export interface MonitorStatusResponse< | HardwareStatusPayload | DockerStatusPayload | GameStatusPayload - | GrpcStatusPayload, + | GrpcStatusPayload + | WebSocketStatusPayload, > { monitorId: string; teamId: string; @@ -111,6 +112,10 @@ export interface GrpcStatusPayload { servingStatus: string; } +export interface WebSocketStatusPayload { + connected: boolean; +} + export interface MonitorPayloadMap { ping: PingStatusPayload; http: HttpStatusPayload; @@ -120,6 +125,7 @@ export interface MonitorPayloadMap { port: PortStatusPayload; game: GameStatusPayload; grpc: GrpcStatusPayload; + websocket: WebSocketStatusPayload; default: unknown; } From 62f1e62a26f29a0b8c9f05a1512b488f457f5452 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:57:52 +0530 Subject: [PATCH 38/53] Feat: Backend service - Implement WebSocket check in NetworkService --- .../network/WebSocketProvider.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 server/src/service/infrastructure/network/WebSocketProvider.ts diff --git a/server/src/service/infrastructure/network/WebSocketProvider.ts b/server/src/service/infrastructure/network/WebSocketProvider.ts new file mode 100644 index 000000000..0af436067 --- /dev/null +++ b/server/src/service/infrastructure/network/WebSocketProvider.ts @@ -0,0 +1,94 @@ +import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; +import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import { Monitor, MonitorType } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js"; +import type WebSocket from "ws"; + +type WebSocketConstructor = typeof WebSocket; + +const SERVICE_NAME = "WebSocketProvider"; +const TIMEOUT_MS = 10000; + +export class WebSocketProvider implements IStatusProvider { + readonly type = "websocket"; + + constructor(private WS: WebSocketConstructor) {} + + supports(type: MonitorType): boolean { + return type === "websocket"; + } + + async handle(monitor: Monitor): Promise> { + try { + const { url } = monitor; + if (!url) { + throw new Error("URL is required for WebSocket monitoring"); + } + + const { responseTime, error } = await timeRequest(async () => { + return new Promise<{ connected: boolean }>((resolve, reject) => { + const options: WebSocket.ClientOptions = {}; + if (monitor.ignoreTlsErrors) { + options.rejectUnauthorized = false; + } + + const ws = new this.WS(url, options); + + const timeout = setTimeout(() => { + ws.close(); + reject(new Error("WebSocket connection timeout")); + }, TIMEOUT_MS); + + ws.on("open", () => { + clearTimeout(timeout); + ws.close(); + resolve({ connected: true }); + }); + + ws.on("error", (err: unknown) => { + clearTimeout(timeout); + ws.close(); + reject(err); + }); + }); + }); + + if (error) { + const errorMessage = error instanceof Error ? error.message : "WebSocket check failed"; + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: NETWORK_ERROR, + message: errorMessage, + responseTime: responseTime, + timings: undefined, + payload: { connected: false }, + }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: true, + code: 200, + message: "WebSocket check successful", + responseTime: responseTime, + timings: undefined, + payload: { connected: true }, + }; + } catch (err: unknown) { + const originalMessage = err instanceof Error ? err.message : String(err); + throw new AppError({ + message: originalMessage || "Error performing WebSocket check", + status: 500, + service: SERVICE_NAME, + method: "handle", + details: { url: monitor.url }, + }); + } + } +} From 216d9b2996dedf91708ae9cf61044f51b92b4295 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:58:55 +0530 Subject: [PATCH 39/53] Feat: Backend - Config --- server/src/config/services.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index b55bd0f87..65377eeb4 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -58,6 +58,7 @@ import { DockerProvider } from "@/service/infrastructure/network/DockerProvider. import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; +import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js"; // Third-party import axios from "axios"; @@ -77,6 +78,7 @@ import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; +import WebSocket from "ws"; // Repositories import { @@ -185,6 +187,7 @@ export const initializeServices = async ({ const portProvider = new PortProvider(net); const gameProvider = new GameProvider(logger, GameDig); const grpcProvider = new GrpcProvider(grpc, protoLoader); + const webSocketProvider = new WebSocketProvider(WebSocket); const networkService = new NetworkService(axios, logger, [ pingProvider, @@ -195,6 +198,7 @@ export const initializeServices = async ({ portProvider, gameProvider, grpcProvider, + webSocketProvider, ]); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); From 23247d43194f06af6cce5d6c988e08a47de484dd Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 00:59:39 +0530 Subject: [PATCH 40/53] Feat: Update server validation --- server/src/validation/checkValidation.ts | 2 +- server/src/validation/monitorValidation.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/validation/checkValidation.ts b/server/src/validation/checkValidation.ts index 547281d68..d4f75f3ac 100644 --- a/server/src/validation/checkValidation.ts +++ b/server/src/validation/checkValidation.ts @@ -11,7 +11,7 @@ export const getChecksParamValidation = z.object({ }); export const getChecksQueryValidation = z.object({ - type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"]).optional(), + type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(), sortOrder: z.enum(["asc", "desc"]).optional(), limit: z.coerce.number().optional(), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 4694143ac..75ffccd19 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -21,8 +21,8 @@ export const getMonitorsByTeamIdParamValidation = z.object({}); export const getMonitorsByTeamIdQueryValidation = z.object({ type: z .union([ - z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]), - z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])), + z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]), + z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])), ]) .optional(), filter: z.union([z.string(), z.literal(""), z.null()]).optional(), @@ -37,8 +37,8 @@ export const getMonitorsWithChecksQueryValidation = z.object({ order: z.enum(["asc", "desc"]).optional(), type: z .union([ - z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]), - z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])), + z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]), + z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])), ]) .optional(), explain: booleanCoercion.optional(), From 1d529e3be492d23bca138e6ba795fa60c74decc9 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:00:38 +0530 Subject: [PATCH 41/53] Feat: Frontend - Add websocket to client types and validations --- client/src/Utils/MonitorUtils.ts | 1 + client/src/Validation/monitor.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index ad744ba6d..d0ede67e1 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -9,6 +9,7 @@ export const getMonitorPath = (type: MonitorType): string => { ping: "uptime", game: "uptime", grpc: "uptime", + websocket: "uptime", unknown: "uptime", docker: "uptime", hardware: "infrastructure", diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 439af977f..9acffe6fe 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -115,6 +115,13 @@ const hardwareSchema = baseSchema.extend({ selectedDisks: z.array(z.string()), }); +// WebSocket monitor schema +const websocketSchema = baseSchema.extend({ + type: z.literal("websocket"), + url: z.string().min(1, "WebSocket URL is required"), + ignoreTlsErrors: z.boolean(), +}); + // Discriminated union of all monitor types export const monitorSchema = z.discriminatedUnion("type", [ httpSchema, @@ -125,6 +132,7 @@ export const monitorSchema = z.discriminatedUnion("type", [ grpcSchema, pagespeedSchema, hardwareSchema, + websocketSchema, ]); export type MonitorFormData = z.infer; @@ -139,4 +147,5 @@ export { grpcSchema, pagespeedSchema, hardwareSchema, + websocketSchema, }; From db5441d0e21c133709a779e1fb8cbc3a313c7d63 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:01:49 +0530 Subject: [PATCH 42/53] Feat: Frontend - update form hook and create/edit page --- client/src/Hooks/useMonitorForm.ts | 8 ++++++++ client/src/Pages/CreateMonitor/index.tsx | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 8b5252879..963409fc8 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -103,6 +103,14 @@ export const useMonitorForm = ({ selectedDisks: data?.selectedDisks || [], }; break; + case "websocket": + defaults = { + ...base, + type: "websocket", + url: data?.url || "", + ignoreTlsErrors: data?.ignoreTlsErrors || false, + }; + break; default: defaults = { ...base, diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 22719986e..dd68fee7d 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -146,6 +146,17 @@ const getGeneralSettingsConfig = ( showGrpcServiceName: false, showIgnoreTls: false, }, + websocket: { + urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + showGrpcServiceName: false, + showIgnoreTls: true, + }, }; return configs[type] || configs.http; }; @@ -331,6 +342,13 @@ const CreateMonitorPage = () => { "pages.createMonitor.form.type.optionGrpcDescription" )} /> + )} @@ -747,7 +765,7 @@ const CreateMonitorPage = () => { } /> - {(watchedType === "http" || watchedType === "grpc") && ( + {(watchedType === "http" || watchedType === "grpc" || watchedType === "websocket") && ( Date: Fri, 6 Mar 2026 14:31:59 -0500 Subject: [PATCH 43/53] refactor: simplify EditUser imports and canDeleteUser logic - Merge duplicate react-router-dom imports into one - Replace useIsAdmin + useIsSuperAdmin + useSelector with single useSelector - Remove redundant !includes("superadmin") check from canDeleteUser --- client/src/Pages/Account/EditUser/index.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index 482a2f7bb..d31722dcf 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -11,15 +11,13 @@ import { ConfigBox, BasePage } from "@/Components/design-elements"; import { UserRoles } from "@/Types/User"; import { useTranslation } from "react-i18next"; import type { UserRole, User } from "@/Types/User"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material"; import { useEditUserForm } from "@/Hooks/useEditUserForm"; import { useGet, usePatch, useDelete } from "@/Hooks/UseApi"; import type { EditUserFormData } from "@/Validation/editUser"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; import { DialogInput } from "@/Components/inputs/Dialog"; -import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; @@ -37,9 +35,9 @@ const EditUserPage = () => { const { deleteFn, loading: isDeleting } = useDelete(); const navigate = useNavigate(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const isAdmin = useIsAdmin(); - const isSuperAdmin = useIsSuperAdmin(); const currentUser = useSelector((state: RootState) => state.auth.user); + const isSuperAdmin = currentUser?.role?.includes("superadmin") ?? false; + const isAdmin = isSuperAdmin || (currentUser?.role?.includes("admin") ?? false); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -74,7 +72,6 @@ const EditUserPage = () => { const canDeleteUser = isAdmin && userId !== currentUser?.id && - !user?.role?.includes("superadmin") && !user?.role?.includes("demo") && (isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin")); From 19174ab3e5771216da9e31cf3c0b1228d571ef1b Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:02:30 +0530 Subject: [PATCH 44/53] Feat: update filter controls and default types --- client/src/Components/monitors/ControlsFilter.tsx | 3 ++- client/src/Pages/Uptime/Monitors/index.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/Components/monitors/ControlsFilter.tsx b/client/src/Components/monitors/ControlsFilter.tsx index 0f26e04a7..a617d454c 100644 --- a/client/src/Components/monitors/ControlsFilter.tsx +++ b/client/src/Components/monitors/ControlsFilter.tsx @@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor"; import { Typography, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; -const types = ["http", "ping", "port", "docker", "game", "grpc"]; +const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"]; const typeDisplayNames: Record = { http: "HTTP", ping: "Ping", @@ -15,6 +15,7 @@ const typeDisplayNames: Record = { docker: "Docker", game: "Game", grpc: "gRPC", + websocket: "WebSocket", }; const statuses = ["up", "down"]; const states = ["active", "paused"]; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 80ee8e2d9..792e0fd78 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => { const effectiveTypes = selectedTypes.length > 0 ? selectedTypes - : ["http", "ping", "docker", "port", "game", "grpc"]; + : ["http", "ping", "docker", "port", "game", "grpc", "websocket"]; // Build URL for monitors with checks const monitorsWithChecksUrl = useMemo(() => { From 5ebe46a81ce3e85f1a12b40b73d128dcb917dd9c Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:11:20 +0530 Subject: [PATCH 45/53] Feat: Websocket Implementation --- client/package-lock.json | 23 ++----------------- client/src/locales/en.json | 7 ++++++ server/src/service/business/monitorService.ts | 3 ++- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index e1dfadad5..b82e9e6a6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -83,7 +83,6 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -354,7 +353,6 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -392,7 +390,6 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1162,7 +1159,6 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1267,7 +1263,6 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2308,7 +2303,6 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2324,7 +2318,6 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2446,7 +2439,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2771,7 +2763,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3203,8 +3194,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3586,7 +3576,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4331,7 +4320,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5115,7 +5103,6 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5763,7 +5750,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5775,7 +5761,6 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5975,8 +5960,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6126,7 +6110,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6915,7 +6898,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7051,7 +7033,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6bf41478..441a02b80 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -487,6 +487,10 @@ "grpcServiceName": { "label": "Service name", "placeholder": "e.g. my.service.v1 (leave empty for overall health)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" } }, "title": "General settings", @@ -498,6 +502,7 @@ "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", "pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.", "grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.", + "websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.", "hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure." } }, @@ -534,6 +539,8 @@ "optionGameDescription": "Monitor if a specific game server is online.", "optionGrpc": "gRPC", "optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.", + "optionWebSocket": "WebSocket", + "optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.", "optionHttp": "HTTP(S)", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", "optionPing": "Ping", diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b197c04bd..a7a30111c 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -227,7 +227,8 @@ export class MonitorService implements IMonitorService { checksData.monitorType !== "docker" && checksData.monitorType !== "port" && checksData.monitorType !== "game" && - checksData.monitorType !== "grpc" + checksData.monitorType !== "grpc" && + checksData.monitorType !== "websocket" ) { throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 }); } From 125d70c4b1ba121066f02bc7c956e11539ef40d9 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:13:00 +0530 Subject: [PATCH 46/53] Format Checks --- client/src/Pages/CreateMonitor/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index dd68fee7d..b80cddaaa 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -765,7 +765,9 @@ const CreateMonitorPage = () => { } /> - {(watchedType === "http" || watchedType === "grpc" || watchedType === "websocket") && ( + {(watchedType === "http" || + watchedType === "grpc" || + watchedType === "websocket") && ( Date: Fri, 6 Mar 2026 12:07:18 -0800 Subject: [PATCH 47/53] restore PORT env var --- server/src/index.ts | 5 ++--- server/src/validation/envValidation.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 1a6195d80..2286d3887 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -50,9 +50,8 @@ const startApp = async () => { openApiSpec, }); - const port = 52345; - const server = app.listen(port, () => { - logger.info({ message: `Server started on port:${port}` }); + const server = app.listen(env.PORT, () => { + logger.info({ message: `Server started on port:${env.PORT}` }); }); initShutdownListener(server, services); diff --git a/server/src/validation/envValidation.ts b/server/src/validation/envValidation.ts index 6dffdd0cd..2b0e36fcf 100644 --- a/server/src/validation/envValidation.ts +++ b/server/src/validation/envValidation.ts @@ -2,6 +2,7 @@ import { z } from "zod"; const envSchema = z.object({ // Server Configuration + PORT: z.string().default("52345"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"), From 7eeff7a39619b6400b44a4462b35d7c2497bcd57 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:37:37 +0530 Subject: [PATCH 48/53] Fix: Dev Dependencies --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 29 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0ca809f88..cebf0bf46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,30 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@types/ws": "^8.18.1", "husky": "^9.1.7" } }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -27,6 +48,13 @@ "funding": { "url": "https://github.com/sponsors/typicode" } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index f5368f28c..37297e8e1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/bluewave-labs/Checkmate#readme", "devDependencies": { + "@types/ws": "^8.18.1", "husky": "^9.1.7" } } From 9f600f87af3b44e51bbb12b0d5b0bb0fbd2e0cd5 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Sat, 7 Mar 2026 01:40:07 +0530 Subject: [PATCH 49/53] Fix: Dev Dependencies in Server --- package-lock.json | 28 ---------------------------- package.json | 1 - server/package-lock.json | 3 ++- server/package.json | 2 +- 4 files changed, 3 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index cebf0bf46..0ca809f88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,30 +9,9 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@types/ws": "^8.18.1", "husky": "^9.1.7" } }, - "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -48,13 +27,6 @@ "funding": { "url": "https://github.com/sponsors/typicode" } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" } } } diff --git a/package.json b/package.json index 37297e8e1..f5368f28c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ }, "homepage": "https://github.com/bluewave-labs/Checkmate#readme", "devDependencies": { - "@types/ws": "^8.18.1", "husky": "^9.1.7" } } diff --git a/server/package-lock.json b/server/package-lock.json index 4b751858f..5d9d920ee 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,7 +12,6 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", - "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -65,6 +64,7 @@ "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", "@types/swagger-ui-express": "4.1.8", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "c8": "10.1.3", @@ -4666,6 +4666,7 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" diff --git a/server/package.json b/server/package.json index 17ed45295..7bceee766 100755 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,6 @@ "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", "@pulsecron/pulse": "1.6.8", - "@types/ws": "^8.18.1", "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", @@ -80,6 +79,7 @@ "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", "@types/swagger-ui-express": "4.1.8", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "c8": "10.1.3", From 172467c0a6a1e100b0346cc8adfc61695373bcdd Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 6 Mar 2026 15:19:01 -0500 Subject: [PATCH 50/53] fix: address PR review feedback - Use useIsAdmin and useIsSuperAdmin hooks instead of manual role checks - Close delete dialog on failure, not just on success --- client/src/Pages/Account/EditUser/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index d31722dcf..728f9f0e6 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -20,6 +20,7 @@ import { useEffect, useState } from "react"; import { DialogInput } from "@/Components/inputs/Dialog"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; interface RoleOption { id: UserRole; @@ -36,8 +37,8 @@ const EditUserPage = () => { const navigate = useNavigate(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const currentUser = useSelector((state: RootState) => state.auth.user); - const isSuperAdmin = currentUser?.role?.includes("superadmin") ?? false; - const isAdmin = isSuperAdmin || (currentUser?.role?.includes("admin") ?? false); + const isSuperAdmin = useIsSuperAdmin(); + const isAdmin = useIsAdmin(); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -77,8 +78,8 @@ const EditUserPage = () => { const handleDeleteUser = async () => { const result = await deleteFn(`/auth/users/${userId}`); + setShowDeleteDialog(false); if (result) { - setShowDeleteDialog(false); navigate("/account", { state: { tab: "team" } }); } }; From 38e2a64255bf899306e7103c8e119db3e50c2ff8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 12:25:32 -0800 Subject: [PATCH 51/53] explicitly bump zod --- client/package-lock.json | 27 ++++++++++++++++++++++++--- client/package.json | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index b82e9e6a6..9cf2824ae 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,7 +44,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "^4.1.11" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "24.5.2", @@ -83,6 +83,7 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -353,6 +354,7 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -390,6 +392,7 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1159,6 +1162,7 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1263,6 +1267,7 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2303,6 +2308,7 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2318,6 +2324,7 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2439,6 +2446,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2763,6 +2771,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3194,7 +3203,8 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -3576,6 +3586,7 @@ "version": "8.57.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4320,6 +4331,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5103,6 +5115,7 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5750,6 +5763,7 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5761,6 +5775,7 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5960,7 +5975,8 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6110,6 +6126,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6898,6 +6915,7 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7033,6 +7051,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7340,6 +7359,8 @@ }, "node_modules/zod": { "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/client/package.json b/client/package.json index b6c48e1c1..2e6b5cfcb 100644 --- a/client/package.json +++ b/client/package.json @@ -52,7 +52,7 @@ "redux-persist": "6.0.0", "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", - "zod": "^4.1.11" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "24.5.2", From d7d237d37b64af677a1683919b8481777522c60f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Mar 2026 12:35:38 -0800 Subject: [PATCH 52/53] move button strings to correct location, flex-end --- client/src/Pages/Account/EditUser/index.tsx | 9 +++++---- client/src/locales/en.json | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/client/src/Pages/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index d31722dcf..79068fac2 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -20,6 +20,7 @@ import { useEffect, useState } from "react"; import { DialogInput } from "@/Components/inputs/Dialog"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; +import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; interface RoleOption { id: UserRole; @@ -189,8 +190,9 @@ const EditUserPage = () => { } /> {canDeleteUser && ( @@ -200,7 +202,7 @@ const EditUserPage = () => { onClick={() => setShowDeleteDialog(true)} sx={{ minWidth: 100 }} > - {t("pages.editUser.buttons.removeUser")} + {t("common.buttons.removeUser")} )}