diff --git a/client/package-lock.json b/client/package-lock.json index 338de1e26..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", @@ -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" @@ -7391,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 dd585636b..2e6b5cfcb 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", @@ -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", @@ -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/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) { diff --git a/client/src/Components/layout/AppLayout.tsx b/client/src/Components/layout/AppLayout.tsx index baadd85ca..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); @@ -47,10 +43,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/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/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/Account/EditUser/index.tsx b/client/src/Pages/Account/EditUser/index.tsx index a3ddebaba..86a52bbbb 100644 --- a/client/src/Pages/Account/EditUser/index.tsx +++ b/client/src/Pages/Account/EditUser/index.tsx @@ -11,12 +11,17 @@ 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 } 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 { DialogInput } from "@/Components/inputs/Dialog"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import { LAYOUT } from "@/Utils/Theme/constants"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; 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 currentUser = useSelector((state: RootState) => state.auth.user); + const isSuperAdmin = useIsSuperAdmin(); + const isAdmin = useIsAdmin(); const { data: user, isLoading } = useGet(`/auth/users/${userId}`); @@ -60,6 +71,20 @@ const EditUserPage = () => { setValue("role", newRoles, { shouldValidate: true }); }; + const canDeleteUser = + isAdmin && + userId !== currentUser?.id && + !user?.role?.includes("demo") && + (isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin")); + + const handleDeleteUser = async () => { + const result = await deleteFn(`/auth/users/${userId}`); + setShowDeleteDialog(false); + if (result) { + navigate("/account", { state: { tab: "team" } }); + } + }; + const onSubmit = async (data: EditUserFormData) => { await patch(`/auth/users/${userId}`, data); }; @@ -165,17 +190,45 @@ const EditUserPage = () => { } /> - + {canDeleteUser && ( + + )} + + + setShowDeleteDialog(false)} + onConfirm={handleDeleteUser} + confirmText={t("common.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/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 34e141795..b80cddaaa 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, 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"; @@ -141,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; }; @@ -326,6 +342,13 @@ const CreateMonitorPage = () => { "pages.createMonitor.form.type.optionGrpcDescription" )} /> + )} @@ -742,7 +765,9 @@ const CreateMonitorPage = () => { } /> - {(watchedType === "http" || watchedType === "grpc") && ( + {(watchedType === "http" || + watchedType === "grpc" || + watchedType === "websocket") && ( { /> )} - {watchedType === "http" && ( + {supportsGeoCheck(watchedType) && ( { ); const geoChecksUrl = useMemo(() => { - if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) { + if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { return null; } const params = new URLSearchParams(); @@ -141,7 +141,7 @@ const UptimeDetailsPage = () => { // Fetch paginated geo checks for the table const geoChecksTableUrl = useMemo(() => { - if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) { + if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { return null; } const params = new URLSearchParams(); 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(() => { diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 01cae62e9..053b517d1 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -12,10 +12,20 @@ export const MonitorTypes = [ "port", "game", "grpc", + "websocket", "unknown", ] 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/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/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index d314c8e27..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", }; @@ -31,4 +33,8 @@ export const darkPalette = { secondary: { main: colors.gray700, }, + background: { + default: colors.gray900, + paper: colors.gray850, + }, }; 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, }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6bf41478..5f582cac5 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -46,7 +46,8 @@ "sendTestEmail": "Send test email", "exportToJSON": "Export to JSON", "importFromJSON": "Import from JSON", - "clearFilters": "Clear filters" + "clearFilters": "Clear filters", + "removeUser": "Remove user" }, "charts": { "labels": { @@ -487,6 +488,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 +503,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 +540,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", @@ -619,6 +627,12 @@ "title": "Roles", "description": "Assign roles to the user. Multiple roles can be selected." } + }, + "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/client/vite.config.ts b/client/vite.config.ts index 7408636dd..373a50763 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export default defineConfig(({}) => { - let version = "3.5.0"; + let version = "3.5.1"; return { base: "/", diff --git a/server/package-lock.json b/server/package-lock.json index ced3a5d0e..5d9d920ee 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,13 +36,14 @@ "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", "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": { @@ -63,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", @@ -262,558 +264,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 +799,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": { @@ -879,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", @@ -1509,7 +1502,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1554,7 +1546,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3487,13 +3478,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 +3492,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 +3510,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 +3532,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 +3549,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 +3566,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 +3582,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 +3596,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 +3609,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 +3624,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 +3644,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 +3665,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 +3680,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 +3694,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 +3710,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 +3727,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 +3741,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 +3755,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 +3770,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 +3784,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 +3811,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 +3831,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 +3850,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 +3863,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 +3878,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 +3893,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 +3906,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 +3919,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 +3933,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 +3946,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 +3962,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 +3981,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 +3996,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 +4009,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 +4023,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 +4038,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 +4058,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 +4071,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 +4085,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": { @@ -4346,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", @@ -4507,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" } @@ -4673,6 +4662,16 @@ "@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==", + "dev": true, + "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", @@ -5348,7 +5347,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5389,9 +5387,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 +5544,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 +5877,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" }, @@ -5927,7 +5925,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6870,7 +6867,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" @@ -7188,9 +7184,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 +7312,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 +7390,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", @@ -7643,7 +7634,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8005,7 +7995,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", @@ -8137,23 +8126,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 +8327,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 +8418,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 +8516,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 +8736,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" @@ -9654,7 +9639,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -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" @@ -12531,7 +12509,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12960,9 +12937,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 +12948,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 +13248,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 +13617,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 +14104,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 +14344,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", @@ -14547,20 +14516,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 +14542,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" @@ -14649,7 +14604,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14818,7 +14772,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", @@ -14981,7 +14934,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15035,9 +14987,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" @@ -15615,9 +15567,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 b94a1e852..7bceee766 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": "", @@ -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,13 +51,14 @@ "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", "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": { @@ -78,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", diff --git a/server/src/app.ts b/server/src/app.ts index fea81ba07..58684532c 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: express.Request, res: express.Response, next: express.NextFunction) => { + 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({ diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 56aef2f19..65377eeb4 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -1,35 +1,69 @@ 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"; +import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js"; + +// Third-party 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"; @@ -44,8 +78,9 @@ 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 +// Repositories import { MongoMonitorsRepository, MongoChecksRepository, @@ -74,24 +109,23 @@ import { IMaintenanceWindowsRepository, } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; -import { EnvConfig } from "@/service/system/settingsService.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; @@ -121,7 +155,7 @@ export const initializeServices = async ({ }: { logger: ILogger; envSettings: EnvConfig; - settingsService: any; + settingsService: ISettingsService; settingsRepository: ISettingsRepository; }): Promise => { // Create DB @@ -144,22 +178,28 @@ export const initializeServices = async ({ const teamsRepository = new MongoTeamsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); - 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 - ); + // Network providers + 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 dockerProvider = new DockerProvider(logger, Docker); + 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, + httpProvider, + pageSpeedProvider, + hardwareProvider, + dockerProvider, + portProvider, + gameProvider, + grpcProvider, + webSocketProvider, + ]); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const notificationMessageBuilder = new NotificationMessageBuilder(); @@ -181,6 +221,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); @@ -264,7 +305,6 @@ export const initializeServices = async ({ const statusPageService = new StatusPageService(statusPagesRepository); const services = { - //v1 settingsService, db, networkService, 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/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/db/migration/0004_fixInfrastructureThresholds.ts b/server/src/db/migration/0004_fixInfrastructureThresholds.ts new file mode 100644 index 000000000..6d369f311 --- /dev/null +++ b/server/src/db/migration/0004_fixInfrastructureThresholds.ts @@ -0,0 +1,60 @@ +import { MonitorModel } from "../models/Monitor.js"; +import { logger } from "@/utils/logger.js"; + +/** + * 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 { + 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 0 or 5 + const filter = { + type: "hardware", + $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 0 or 5 found" }); + return; + } + + logger.info({ + service: SERVICE_NAME, + message: `Found ${monitors.length} monitors with threshold value of 0 or 5`, + }); + + let updatedCount = 0; + + for (const monitor of monitors) { + const update: Record = {}; + + for (const field of thresholdFields) { + if ((monitor as any)[field] === 0 || (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 0/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 }) => { diff --git a/server/src/index.ts b/server/src/index.ts index e5b679beb..2286d3887 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"; @@ -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/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/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/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/checkService.ts b/server/src/service/business/checkService.ts index 2d2bf0e01..c83a3d411 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; @@ -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 }); } @@ -189,5 +194,3 @@ class CheckService implements ICheckService { throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); }; } - -export default CheckService; diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index 67729ce98..f9c53f633 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() { @@ -103,5 +107,3 @@ class DiagnosticService { return diagnostics; }; } - -export default DiagnosticService; diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 1e5720656..f35826a8f 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; @@ -68,7 +68,7 @@ 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 @@ -76,7 +76,6 @@ 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 @@ 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 @@ 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,21 +169,9 @@ 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; }; } - -export default GeoChecksService; diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index db44bdbb2..710336016 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; @@ -244,5 +266,3 @@ class IncidentService { } }; } - -export default IncidentService; diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index e4069d485..9d1cbf59d 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; @@ -88,5 +94,3 @@ class InviteService { return await this.invitesRepository.findByToken(inviteToken); }; } - -export default InviteService; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 816bdca59..abf513eb5 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; @@ -83,5 +96,3 @@ class MaintenanceWindowService { return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; } - -export default MaintenanceWindowService; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b94d99c90..a7a30111c 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, @@ -226,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 }); } @@ -340,7 +342,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 (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) { return { groupedGeoChecks: [] }; } diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index d05f027b1..48f3a51cf 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -11,10 +11,32 @@ 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; + deleteUserById(actor: User, targetUserId: string): 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 +44,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 +72,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; @@ -120,7 +142,7 @@ class UserService { message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser.id, + details: { userId: newUser.id }, }); delete newUser.profileImage; @@ -134,20 +156,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, }); } @@ -181,7 +206,7 @@ class UserService { message: "New user created by superadmin", service: SERVICE_NAME, method: "createUser", - details: newUser.id, + details: { userId: newUser.id }, }); newUser.profileImage = undefined; @@ -256,6 +281,14 @@ class UserService { 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; }; @@ -313,25 +346,65 @@ 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); }; + 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); @@ -348,4 +421,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..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 = { @@ -53,7 +54,7 @@ export interface ISuperSimpleQueue { obliterate(): Promise; } -class SuperSimpleQueue implements ISuperSimpleQueue { +export class SuperSimpleQueue implements ISuperSimpleQueue { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -125,8 +126,13 @@ class SuperSimpleQueue implements ISuperSimpleQueue { data: monitor, }); + // Return early if we don't need geo checks + if (!supportsGeoCheck(monitor.type)) { + 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 +181,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue { // Handle geo check job lifecycle const geoJobId = `${monitor.id}-geo`; - if (monitor.geoCheckEnabled && monitor.type === "http") { + if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) { // Check if geo job exists const existingGeoJob = await this.scheduler.getJob(geoJobId); if (existingGeoJob) { @@ -289,5 +295,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..eef0a75be 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,10 +1,9 @@ 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 } 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 +14,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 +38,7 @@ export interface MonitorActionDecision { }; } -class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { +export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private logger: any; @@ -282,19 +281,18 @@ 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 (!supportsGeoCheck(monitor.type)) { this.logger.debug({ - message: `Monitor ${monitorId} is not HTTP type, skipping geo check`, + message: `Monitor ${monitorId} type does not support geo checks, skipping`, 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}`, @@ -414,5 +412,3 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return decision; } } - -export default SuperSimpleQueueHelper; diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 318fd35cd..229c5e7d4 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; @@ -177,5 +177,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 0840d5a4c..f536f3f18 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; @@ -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({ @@ -178,5 +178,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..41f96b286 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -1,4 +1,6 @@ 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"; @@ -8,7 +10,7 @@ const POLL_INTERVAL_MS = 2000; const MAX_POLL_TIMEOUT_MS = 30000; interface GlobalPingMeasurementRequest { - type: "http"; + type: MonitorType; target: string; locations: Array<{ continent: GeoContinent }>; limit: number; @@ -44,17 +46,26 @@ 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; } -class GlobalPingService implements IGlobalPingService { +export class GlobalPingService implements IGlobalPingService { static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; @@ -67,13 +78,16 @@ 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 (!supportsGeoCheck(monitorType)) { + 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 +102,9 @@ 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 +178,7 @@ 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 +192,38 @@ 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; @@ -206,5 +233,3 @@ class GlobalPingService implements IGlobalPingService { return new Promise((resolve) => setTimeout(resolve, ms)); } } - -export default GlobalPingService; 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/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/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/GrpcProvider.ts b/server/src/service/infrastructure/network/GrpcProvider.ts new file mode 100644 index 000000000..66684ba0c --- /dev/null +++ b/server/src/service/infrastructure/network/GrpcProvider.ts @@ -0,0 +1,180 @@ +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 { 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"; + +type GrpcType = typeof grpc; +type ProtoLoaderType = typeof protoLoader; + +const SERVICE_NAME = "GrpcProvider"; + +export class GrpcProvider implements IStatusProvider { + readonly type = "grpc"; + constructor( + private grpc: GrpcType, + private protoLoader: ProtoLoaderType + ) {} + + supports(type: MonitorType): boolean { + return type === "grpc"; + } + + 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> { + try { + 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, { + 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 { 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) { + const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number }; + const payload = grpcError.grpcPayload; + return { + monitorId: id, + teamId: teamId, + type: type, + status: false, + code: grpcError.grpcCode ?? 5000, + message: grpcError.message ?? "gRPC health check failed", + responseTime, + payload: payload ?? null, + }; + } + + const grpcPayload = response as GrpcStatusPayload; + const isServing = grpcPayload.servingStatus === "SERVING"; + + return { + monitorId: id, + teamId: teamId, + type: type, + status: isServing, + code: isServing ? 200 : 5000, + message: isServing ? `gRPC service healthy (${grpcPayload.servingStatus})` : `gRPC service unhealthy (${grpcPayload.servingStatus})`, + responseTime, + payload: grpcPayload, + }; + } 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/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 new file mode 100644 index 000000000..42fa39a40 --- /dev/null +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -0,0 +1,121 @@ +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, 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"; + + 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"; + } + + 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, jsonPath, ignoreTlsErrors } = monitor; + + if (!url) { + throw new Error("URL is required for HTTP 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); + 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; + } 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, + }; + } 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/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/network/PingProvider.ts b/server/src/service/infrastructure/network/PingProvider.ts new file mode 100644 index 000000000..0b9691da9 --- /dev/null +++ b/server/src/service/infrastructure/network/PingProvider.ts @@ -0,0 +1,66 @@ +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 { 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)); + + if (error) { + throw error; + } + + if (!response) { + throw new Error(`No response from ping for host: ${sanitizedHost}`); + } + + 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, + }; + } 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/PortProvider.ts b/server/src/service/infrastructure/network/PortProvider.ts new file mode 100644 index 000000000..7c1c96ce4 --- /dev/null +++ b/server/src/service/infrastructure/network/PortProvider.ts @@ -0,0 +1,90 @@ +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 }, + }; + } + + 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 }, + }; + } 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: "handle", + details: { url: monitor.url, port: monitor.port }, + }); + } + } +} 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 }, + }); + } + } +} diff --git a/server/src/service/infrastructure/network/utils.ts b/server/src/service/infrastructure/network/utils.ts new file mode 100644 index 000000000..ddb962f47 --- /dev/null +++ b/server/src/service/infrastructure/network/utils.ts @@ -0,0 +1,14 @@ +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; diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 702affed2..e475bc24b 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,57 +1,14 @@ -import { HTTPError, RequestError } from "got"; -import type { Got, Response } from "got"; -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 path from "path"; -import { fileURLToPath } from "url"; - -import CacheableLookup from "cacheable-lookup"; -import { ISettingsService } from "../system/settingsService.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"; -type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; - -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( - monitor: Monitor - ): Promise< - MonitorStatusResponse< - | PingStatusPayload - | HttpStatusPayload - | PageSpeedStatusPayload - | HardwareStatusPayload - | DockerStatusPayload - | PortStatusPayload - | GameStatusPayload - | GrpcStatusPayload - > - >; + requestStatus(monitor: Monitor): Promise>; requestWebhook( type: string, url: string, @@ -66,791 +23,32 @@ export interface INetworkService { }>; } -class NetworkService implements INetworkService { +export 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 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"); - - 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; - }; 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") + 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.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; - - const cacheable = new CacheableLookup(); - - this.got = got.extend({ - dnsCache: cacheable, - timeout: { - request: 30000, - }, - retry: { limit: 1 }, - }); } 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 - ): Promise< - MonitorStatusResponse< - | PingStatusPayload - | HttpStatusPayload - | PageSpeedStatusPayload - | HardwareStatusPayload - | DockerStatusPayload - | PortStatusPayload - | GameStatusPayload - | GrpcStatusPayload - > - > { - const type = monitor?.type || "unknown"; - switch (type) { - case this.TYPE_PING: - return await this.requestPing(monitor); - case this.TYPE_HTTP: - return await this.requestHttp(monitor); - case this.TYPE_PAGESPEED: - return await this.requestPageSpeed(monitor); - case this.TYPE_HARDWARE: - return await this.requestHardware(monitor); - case this.TYPE_DOCKER: - return await this.requestDocker(monitor); - case this.TYPE_PORT: - return await this.requestPort(monitor); - case this.TYPE_GAME: - return await this.requestGame(monitor); - case this.TYPE_GRPC: - return await this.requestGrpc(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); } - } - - 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({ - 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 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; - 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"; + return provider.handle(monitor); } private async handleUnsupportedType(type: string): Promise { @@ -859,7 +57,7 @@ class NetworkService implements INetworkService { teamId: "unknown", type: "unknown", status: false, - code: this.NETWORK_ERROR, + code: NETWORK_ERROR, message: `Unsupported type: ${type}`, }; } @@ -883,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", }); @@ -892,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, }; @@ -901,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`, }; } @@ -927,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, @@ -970,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", }); @@ -978,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, }; @@ -987,17 +185,15 @@ 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", }; } } } - -export default NetworkService; diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 58c896dc6..1d75c0a36 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; @@ -74,5 +74,3 @@ class SettingsService implements ISettingsService { return settings; }; } - -export default SettingsService; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index a6ecc4ef4..89740162b 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -3,9 +3,12 @@ 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; +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]; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 2018523cd..07e7c3736 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 @@ -22,7 +24,8 @@ export interface MonitorStatusResponse< | HardwareStatusPayload | DockerStatusPayload | GameStatusPayload - | GrpcStatusPayload, + | GrpcStatusPayload + | WebSocketStatusPayload, > { monitorId: string; teamId: string; @@ -49,7 +52,7 @@ export interface PingStatusPayload { host: string; numeric_host?: string; alive: boolean; - time: number; + time: number | unknown; times?: number[]; output?: string; min?: string; @@ -100,7 +103,7 @@ export interface PortStatusPayload { success: boolean; } -export type GameStatusPayload = Record; +export type GameStatusPayload = QueryResult; export interface GrpcStatusPayload { grpcStatusCode: number; @@ -109,6 +112,10 @@ export interface GrpcStatusPayload { servingStatus: string; } +export interface WebSocketStatusPayload { + connected: boolean; +} + export interface MonitorPayloadMap { ping: PingStatusPayload; http: HttpStatusPayload; @@ -118,6 +125,7 @@ export interface MonitorPayloadMap { port: PortStatusPayload; game: GameStatusPayload; grpc: GrpcStatusPayload; + websocket: WebSocketStatusPayload; default: unknown; } @@ -134,3 +142,5 @@ export type StatusChangeResult = { temp: boolean; }; }; + +export type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; 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/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"), 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(),