Merge pull request #3761 from bluewave-labs/develop

develop -> master
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-07-07 10:19:52 -07:00
commit d368ec5bd7
29 files changed
+1863 -1019

No files matched your search

+299 -189
View File
@@ -1,12 +1,12 @@
{
"name": "client",
"version": "3.9.1",
"version": "3.9.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "client",
"version": "3.9.1",
"version": "3.9.2",
"dependencies": {
"@csstools/css-tokenizer": "^3.0.4",
"@emotion/react": "^11.13.3",
@@ -24,7 +24,7 @@
"html2canvas": "^1.4.1",
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"joi": "17.13.4",
"lucide-react": "^0.562.0",
"maplibre-gl": "^5.19.0",
"mui-color-input": "^7.0.0",
@@ -65,10 +65,12 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -77,26 +79,30 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -117,11 +123,13 @@
"license": "MIT"
},
"node_modules/@babel/generator": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -131,11 +139,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -145,30 +155,36 @@
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.28.6",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.6",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
"@babel/helper-module-imports": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -186,42 +202,52 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
"version": "7.28.6",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -266,27 +292,31 @@
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-globals": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -294,11 +324,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -1639,7 +1671,9 @@
}
},
"node_modules/@remix-run/router": {
"version": "1.23.2",
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
"integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -1671,9 +1705,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
"integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
"cpu": [
"arm"
],
@@ -1684,9 +1718,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
"integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
"cpu": [
"arm64"
],
@@ -1697,9 +1731,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
"integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
"cpu": [
"arm64"
],
@@ -1710,9 +1744,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
"integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
"cpu": [
"x64"
],
@@ -1723,9 +1757,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
"integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
"cpu": [
"arm64"
],
@@ -1736,9 +1770,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
"integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
"cpu": [
"x64"
],
@@ -1749,9 +1783,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
"integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
"cpu": [
"arm"
],
@@ -1762,9 +1796,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
"integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
"cpu": [
"arm"
],
@@ -1775,9 +1809,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
"integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
"cpu": [
"arm64"
],
@@ -1788,9 +1822,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
"integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
"cpu": [
"arm64"
],
@@ -1801,9 +1835,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
"integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
"cpu": [
"loong64"
],
@@ -1814,9 +1848,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
"integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
"cpu": [
"loong64"
],
@@ -1827,9 +1861,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
"integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
"cpu": [
"ppc64"
],
@@ -1840,9 +1874,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
"integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
"cpu": [
"ppc64"
],
@@ -1853,9 +1887,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
"integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
"cpu": [
"riscv64"
],
@@ -1866,9 +1900,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
"integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
"cpu": [
"riscv64"
],
@@ -1879,9 +1913,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
"integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
"cpu": [
"s390x"
],
@@ -1892,9 +1926,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
"integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
"cpu": [
"x64"
],
@@ -1905,9 +1939,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
"integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
"cpu": [
"x64"
],
@@ -1918,9 +1952,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
"integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
"cpu": [
"x64"
],
@@ -1931,9 +1965,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
"integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
"cpu": [
"arm64"
],
@@ -1944,9 +1978,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
"integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
"cpu": [
"arm64"
],
@@ -1957,9 +1991,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
"integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
"cpu": [
"ia32"
],
@@ -1970,9 +2004,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
"integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
"cpu": [
"x64"
],
@@ -1983,9 +2017,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"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==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
"cpu": [
"x64"
],
@@ -2318,7 +2352,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
"node_modules/@types/geojson": {
@@ -2744,6 +2780,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@@ -2982,13 +3030,14 @@
}
},
"node_modules/axios": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
@@ -3018,14 +3067,21 @@
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"version": "2.10.42",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
"integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3047,7 +3103,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
"integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
"funding": [
{
"type": "opencollective",
@@ -3065,11 +3123,11 @@
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
"baseline-browser-mapping": "^2.10.42",
"caniuse-lite": "^1.0.30001800",
"electron-to-chromium": "^1.5.387",
"node-releases": "^2.0.50",
"update-browserslist-db": "^1.2.3"
},
"bin": {
"browserslist": "cli.js"
@@ -3168,7 +3226,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001766",
"version": "1.0.30001803",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
"integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
"funding": [
{
"type": "opencollective",
@@ -3644,7 +3704,9 @@
"license": "ISC"
},
"node_modules/electron-to-chromium": {
"version": "1.5.283",
"version": "1.5.388",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.388.tgz",
"integrity": "sha512-Pl/aJaqOOxYxda3vcx1IKSJimwYXHDkEnGn0F+kG2EE68dDtx2uCinaS+Vih8Z91B9t8CSAbiF/HKyWcnXjhzw==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -3882,6 +3944,8 @@
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4302,14 +4366,16 @@
}
},
"node_modules/form-data": {
"version": "4.0.5",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
@@ -4635,7 +4701,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -4673,6 +4741,19 @@
"node": ">=8.0.0"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-interval": {
"version": "2.0.1",
"license": "MIT",
@@ -5210,7 +5291,9 @@
}
},
"node_modules/joi": {
"version": "17.13.3",
"version": "17.13.4",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz",
"integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
@@ -5225,7 +5308,19 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -5476,6 +5571,8 @@
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -5807,8 +5904,13 @@
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"license": "MIT"
"version": "2.0.50",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/numbered": {
"version": "1.1.0",
@@ -6383,10 +6485,12 @@
}
},
"node_modules/react-router": {
"version": "6.30.3",
"version": "6.30.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
"integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.2"
"@remix-run/router": "1.23.3"
},
"engines": {
"node": ">=14.0.0"
@@ -6396,11 +6500,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.3",
"version": "6.30.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
"integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
"@remix-run/router": "1.23.3",
"react-router": "6.30.4"
},
"engines": {
"node": ">=14.0.0"
@@ -6629,13 +6735,13 @@
"license": "Unlicense"
},
"node_modules/rollup": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
"@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -6645,31 +6751,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@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",
"@rollup/rollup-android-arm-eabi": "4.62.2",
"@rollup/rollup-android-arm64": "4.62.2",
"@rollup/rollup-darwin-arm64": "4.62.2",
"@rollup/rollup-darwin-x64": "4.62.2",
"@rollup/rollup-freebsd-arm64": "4.62.2",
"@rollup/rollup-freebsd-x64": "4.62.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
"@rollup/rollup-linux-arm-musleabihf": "4.62.2",
"@rollup/rollup-linux-arm64-gnu": "4.62.2",
"@rollup/rollup-linux-arm64-musl": "4.62.2",
"@rollup/rollup-linux-loong64-gnu": "4.62.2",
"@rollup/rollup-linux-loong64-musl": "4.62.2",
"@rollup/rollup-linux-ppc64-gnu": "4.62.2",
"@rollup/rollup-linux-ppc64-musl": "4.62.2",
"@rollup/rollup-linux-riscv64-gnu": "4.62.2",
"@rollup/rollup-linux-riscv64-musl": "4.62.2",
"@rollup/rollup-linux-s390x-gnu": "4.62.2",
"@rollup/rollup-linux-x64-gnu": "4.62.2",
"@rollup/rollup-linux-x64-musl": "4.62.2",
"@rollup/rollup-openbsd-x64": "4.62.2",
"@rollup/rollup-openharmony-arm64": "4.62.2",
"@rollup/rollup-win32-arm64-msvc": "4.62.2",
"@rollup/rollup-win32-ia32-msvc": "4.62.2",
"@rollup/rollup-win32-x64-gnu": "4.62.2",
"@rollup/rollup-win32-x64-msvc": "4.62.2",
"fsevents": "~2.3.2"
}
},
@@ -7538,6 +7644,8 @@
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
@@ -7607,9 +7715,9 @@
}
},
"node_modules/vite": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -7912,6 +8020,8 @@
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"license": "ISC"
},
"node_modules/yaml": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "3.9.1",
"version": "3.9.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -33,7 +33,7 @@
"html2canvas": "^1.4.1",
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"joi": "17.13.4",
"lucide-react": "^0.562.0",
"maplibre-gl": "^5.19.0",
"mui-color-input": "^7.0.0",
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:22-slim
# Install ping
RUN apt-get update \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:22-slim
# Install ping
RUN apt-get update \
+1 -1
View File
@@ -19,7 +19,7 @@ RUN npm run build
# ---------------------
# Backend stage
# ---------------------
FROM node:20-slim AS backend
FROM node:22-slim AS backend
# Install ping
RUN apt-get update \
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:20-slim AS frontend-build
FROM node:22-slim AS frontend-build
WORKDIR /app/client
@@ -8,7 +8,7 @@ RUN npm install
COPY client ./
RUN npm run build
FROM node:20-slim AS app
FROM node:22-slim AS app
# Install ping
RUN apt-get update \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:22-slim
# Install ping
RUN apt-get update \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:22-slim
# Install ping
RUN apt-get update \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:22-slim
# Install ping
RUN apt-get update \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:22-slim
# Install ping
RUN apt-get update \
+1036 -608
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -56,8 +56,9 @@
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.2",
"nodemailer": "8.0.5",
"nodemailer": "8.0.11",
"ping": "0.4.4",
"re2": "^1.24.1",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
"swagger-ui-express": "5.0.1",
@@ -10,11 +10,7 @@ import {
import { AppError } from "@/utils/AppError.js";
import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.js";
import { IStatusPageService } from "@/domain/status-pages/status-page.service.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { NormalizeData } from "@/utils/dataUtils.js";
import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js";
import type { StatusPage } from "@/domain/status-pages/status-page.type.js";
const SERVICE_NAME = "statusPageController";
@@ -31,53 +27,14 @@ export interface IStatusPageController {
class StatusPageController implements IStatusPageController {
static SERVICE_NAME = SERVICE_NAME;
private statusPageService: IStatusPageService;
private monitorsRepository: IMonitorsRepository;
private settingsService: ISettingsService;
constructor(statusPageService: IStatusPageService, monitorsRepository: IMonitorsRepository, settingsService: ISettingsService) {
constructor(statusPageService: IStatusPageService) {
this.statusPageService = statusPageService;
this.monitorsRepository = monitorsRepository;
this.settingsService = settingsService;
}
get serviceName() {
return StatusPageController.SERVICE_NAME;
}
private buildStatusPagePayload = async (statusPage: StatusPage, req: Request) => {
if (!statusPage.isPublished) {
const teamId = requireTeamId(req?.user?.teamId);
if (statusPage.teamId !== teamId) {
throw new AppError({ message: "Forbidden", status: 403 });
}
}
const settings = await this.settingsService.getDBSettings();
const showURL = settings.showURL;
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
const monitorOrder = new Map(statusPage.monitors.map((id, index) => [id, index]));
const sortedMonitors = [...monitors].sort((a, b) => {
const orderA = monitorOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
const orderB = monitorOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
const normalizedMonitors = sortedMonitors.map((monitor) => {
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
if (!showURL) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { url, port, secret, notifications, ...rest } = monitor;
return { ...rest, checks: normalizedChecks };
}
return { ...monitor, checks: normalizedChecks };
});
return {
statusPage,
monitors: normalizedMonitors,
};
};
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
try {
createStatusPageBodyValidation.parse(req.body);
@@ -135,7 +92,7 @@ class StatusPageController implements IStatusPageController {
}
const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url as string);
const data = await this.buildStatusPagePayload(statusPage, req);
const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId);
return res.status(200).json({
success: true,
@@ -161,7 +118,7 @@ class StatusPageController implements IStatusPageController {
throw new AppError({ message: "Status page not found", status: 404 });
}
const data = await this.buildStatusPagePayload(statusPage, req);
const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId);
return res.status(200).json({
success: true,
@@ -1,33 +0,0 @@
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, type StatusPage } from "@/domain/status-pages/status-page.type.js";
/**
* When STATUS_PAGE_THEMES_ENABLED is false, the controller drops incoming
* theme fields before persisting so an admin client can't accidentally store
* a value the public page won't honour. Whatever a caller sends here for
* `theme` / `themeMode` is silently ignored — the rest of the body is
* preserved verbatim.
*
* The strip is silent (not a 400) because the kill switch is an ops-side
* concern: API clients should not have to know whether the operator has
* disabled themes today, only that their saved value won't take effect.
* If we later decide we'd rather fail loudly, change this to throw a
* 400 AppError; downstream consumers already handle it.
*/
export const withoutThemeFields = (body: Record<string, unknown>): Record<string, unknown> => {
const rest = { ...body };
delete rest.theme;
delete rest.themeMode;
return rest;
};
/**
* When STATUS_PAGE_THEMES_ENABLED is false, callers reading a status page
* should see the canonical default theme / mode regardless of what's stored
* in Mongo. Whatever the user previously chose stays in the DB so flipping
* the flag back on resurfaces their selection.
*/
export const applyDefaultTheme = (statusPage: StatusPage): StatusPage => ({
...statusPage,
theme: DEFAULT_STATUS_PAGE_THEME,
themeMode: DEFAULT_STATUS_PAGE_THEME_MODE,
});
+21 -3
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import RE2 from "re2";
import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js";
import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js";
import {
@@ -71,6 +72,20 @@ const refineStrategyType = (body: { type?: string; strategy?: string }, ctx: z.R
}
};
// The regex is executed at check time with RE2, reject patterns RE2 won't accept
const refineRegexPattern = (body: { matchMethod?: string; expectedValue?: string }, ctx: z.RefinementCtx) => {
if (body.matchMethod !== "regex" || !body.expectedValue) return;
try {
new RE2(body.expectedValue);
} catch {
ctx.addIssue({
code: "custom",
path: ["expectedValue"],
message: "Invalid regex pattern. Backreferences and lookahead/lookbehind are not supported.",
});
}
};
const refineHeadMatching = (body: { method?: string; useAdvancedMatching?: boolean; jsonPath?: string }, ctx: z.RefinementCtx) => {
if (body.method === "HEAD" && (body.useAdvancedMatching === true || (body.jsonPath ?? "") !== "")) {
ctx.addIssue({
@@ -120,7 +135,8 @@ export const createMonitorBodyValidation = z
})
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType)
.superRefine(refineHeadMatching);
.superRefine(refineHeadMatching)
.superRefine(refineRegexPattern);
export const editMonitorBodyValidation = z
.object({
@@ -159,7 +175,8 @@ export const editMonitorBodyValidation = z
})
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType)
.superRefine(refineHeadMatching);
.superRefine(refineHeadMatching)
.superRefine(refineRegexPattern);
export const pauseMonitorParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
@@ -232,7 +249,8 @@ const importedMonitorSchema = z
})
.superRefine(refineDnsHostname)
.superRefine(refineStrategyType)
.superRefine(refineHeadMatching);
.superRefine(refineHeadMatching)
.superRefine(refineRegexPattern);
export const importMonitorsBodyValidation = z.object({
monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"),
+1 -1
View File
@@ -41,7 +41,7 @@ export const initializeControllers = (apiServices: ApiServices): InitializedCont
maintenanceWindowController: new MaintenanceWindowController(apiServices.maintenanceWindowService),
queueController: new QueueController(apiServices.worker),
logController: new LogController(apiServices.logger),
statusPageController: new StatusPageController(apiServices.statusPageService, apiServices.monitorsRepository, apiServices.settingsService),
statusPageController: new StatusPageController(apiServices.statusPageService),
notificationController: new NotificationController(apiServices.notificationsService, apiServices.monitorsRepository),
tagController: new TagsController(apiServices.tagsService),
diagnosticController: new DiagnosticController(apiServices.diagnosticService),
+1 -1
View File
@@ -91,7 +91,7 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A
emailService,
});
const statusPageService = new StatusPageService(statusPagesRepository, settingsService);
const statusPageService = new StatusPageService(statusPagesRepository, settingsService, monitorsRepository);
const tagsService = new TagsService(tagsRepository, monitorsRepository);
const diagnosticService = new DiagnosticService(db);
@@ -1,26 +1,34 @@
import { type IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, StatusPage } from "@/domain/status-pages/status-page.type.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import {
DEFAULT_STATUS_PAGE_THEME,
DEFAULT_STATUS_PAGE_THEME_MODE,
PublicStatusPagePayload,
StatusPage,
} from "@/domain/status-pages/status-page.type.js";
import { AppError } from "@/utils/AppError.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { Monitor } from "@/domain/monitors/monitor.types.js";
import { NormalizeData } from "@/utils/dataUtils.js";
export interface IStatusPageService {
createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
getStatusPageByUrl(url: string): Promise<StatusPage>;
getStatusPageByCustomDomain(customDomain: string): Promise<StatusPage>;
getStatusPagesByTeamId(teamId: string): Promise<StatusPage[]>;
getPublicStatusPagePayload(statusPage: StatusPage, requesterTeamId: string | undefined): Promise<PublicStatusPagePayload>;
updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
deleteStatusPage(statusPageId: string, teamId: string): Promise<StatusPage>;
}
export class StatusPageService implements IStatusPageService {
private statusPagesRepository: IStatusPagesRepository;
private settingsService: ISettingsService;
constructor(statusPagesRepository: IStatusPagesRepository, settingsService: ISettingsService) {
this.statusPagesRepository = statusPagesRepository;
this.settingsService = settingsService;
}
constructor(
private statusPagesRepository: IStatusPagesRepository,
private settingsService: ISettingsService,
private monitorsRepository: IMonitorsRepository
) {}
private assertCustomDomainAllowed = (customDomain: string | null | undefined) => {
if (!customDomain) {
@@ -63,6 +71,27 @@ export class StatusPageService implements IStatusPageService {
private normalizeInput = (data: Partial<StatusPage>): Partial<StatusPage> =>
this.settingsService.areStatusPageThemesEnabled() ? data : this.withoutThemeFields(data);
private toPublicMonitor = (monitor: Monitor, showURL: boolean) => {
const base = {
id: monitor.id,
name: monitor.name,
type: monitor.type,
status: monitor.status,
uptimePercentage: monitor.uptimePercentage,
recentChecks: monitor.recentChecks,
checks: NormalizeData(monitor.recentChecks, 10, 100),
};
if (showURL) {
return {
...base,
url: monitor.url,
port: monitor.port,
};
}
return base;
};
createStatusPage = async (
userId: string,
teamId: string,
@@ -89,6 +118,21 @@ export class StatusPageService implements IStatusPageService {
return statusPages.map((sp) => this.normalizeTheme(sp));
};
getPublicStatusPagePayload = async (statusPage: StatusPage, requesterTeamId: string | undefined): Promise<PublicStatusPagePayload> => {
if (!statusPage.isPublished) {
if (!requesterTeamId || statusPage.teamId !== requesterTeamId) {
throw new AppError({ message: "Forbidden", status: 403 });
}
}
const showURL = (await this.settingsService.getDBSettings()).showURL;
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
const order = new Map(statusPage.monitors.map((id, i) => [id, i]));
const sorted = [...monitors].sort((a, b) => (order.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.id) ?? Number.MAX_SAFE_INTEGER));
return { statusPage, monitors: sorted.map((monitor) => this.toPublicMonitor(monitor, showURL)) };
};
updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
const normalizedData = this.normalizeCustomDomainInput(this.normalizeInput(data));
const updated = await this.statusPagesRepository.updateById(id, teamId, image, normalizedData);
@@ -1,3 +1,5 @@
import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { NormalizedCheck, CheckSnapshot } from "@/domain/checks/check.type.js";
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
@@ -44,3 +46,11 @@ export interface StatusPage {
createdAt: string;
updatedAt: string;
}
export type PublicStatusPageMonitor = Pick<Monitor, "id" | "name" | "type" | "status" | "uptimePercentage" | "recentChecks"> &
Partial<Pick<Monitor, "url" | "port">> & { checks: NormalizedCheck<CheckSnapshot>[] };
export interface PublicStatusPagePayload {
statusPage: StatusPage;
monitors: PublicStatusPageMonitor[];
}
+27 -14
View File
@@ -1,5 +1,6 @@
import { Monitor } from "@/domain/monitors/monitor.types.js";
import jmespath from "jmespath";
import RE2 from "re2";
type JmesPath = typeof jmespath;
export interface IAdvancedMatcher {
@@ -9,13 +10,6 @@ export interface IAdvancedMatcher {
export class AdvancedMatcher implements IAdvancedMatcher {
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<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined } {
const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
if (!useAdvancedMatching) return { ok: true, message: "Success" };
@@ -30,19 +24,38 @@ export class AdvancedMatcher implements IAdvancedMatcher {
}
}
if (expectedValue) {
const ok = this.compare(dataToValidate, expectedValue, matchMethod);
if (!expectedValue) {
const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
return {
ok,
message: ok ? "Success" : "Expected value did not match",
ok: !isFalsy,
message: !isFalsy ? "Success" : "Extracted value is falsy",
extracted: dataToValidate,
};
}
const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
let actual: string;
try {
actual = String(dataToValidate);
} catch {
return { ok: false, message: "Error evaluating response value", extracted: dataToValidate };
}
let ok: boolean;
if (matchMethod === "include") {
ok = actual.includes(expectedValue);
} else if (matchMethod === "regex") {
try {
ok = new RE2(expectedValue).test(actual);
} catch {
return { ok: false, message: "Invalid regex pattern", extracted: dataToValidate };
}
} else {
ok = actual === expectedValue;
}
return {
ok: !isFalsy,
message: !isFalsy ? "Success" : "Extracted value is falsy",
ok,
message: ok ? "Success" : "Expected value did not match",
extracted: dataToValidate,
};
}
+15 -5
View File
@@ -17,7 +17,7 @@ export const initShutdownListener = (server: Server | null, shutdownTargets: Shu
let isShuttingDown = false;
const shutdown = async () => {
const shutdown = async (exitCode: number = 0) => {
if (isShuttingDown) {
return;
}
@@ -31,7 +31,7 @@ export const initShutdownListener = (server: Server | null, shutdownTargets: Shu
await shutdownTargets.healthServer?.close();
await shutdownTargets.db.disconnect();
logger.info({ message: "Graceful shutdown complete" });
process.exit(0);
process.exit(exitCode);
} catch (error) {
const err = error as Error;
logger.error({
@@ -43,7 +43,17 @@ export const initShutdownListener = (server: Server | null, shutdownTargets: Shu
process.exit(1);
}
};
process.on("SIGUSR2", shutdown);
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("SIGUSR2", () => shutdown(0));
process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));
process.on("unhandledRejection", (reason: unknown) => {
const err = reason instanceof Error ? reason : new Error(String(reason));
logger.error({ message: `Unhandled promise rejection: ${err.message}`, service: SERVICE_NAME, method: "unhandledRejection", stack: err.stack });
shutdown(1);
});
process.on("uncaughtException", (error: Error) => {
logger.error({ message: `Uncaught exception: ${error.message}`, service: SERVICE_NAME, method: "uncaughtException", stack: error.stack });
shutdown(1);
});
};
+19 -4
View File
@@ -195,12 +195,18 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
await this.jobsRepository.recordSuccess(job.id, job.nextScheduledAt, job.intervalMs, Date.now());
}
} catch (error: unknown) {
await this.jobsRepository.recordFailure(job.id, error, Date.now()); // Record failure and release lock
this.logger.warn({
message: error instanceof Error ? error.message : String(error),
service: SERVICE_NAME,
method: `runJob:${job.type}`,
});
await this.jobsRepository.recordFailure(job.id, error, Date.now()).catch((recordError: unknown) => {
this.logger.error({
message: `recordFailure failed for job ${job.id}: ${recordError instanceof Error ? recordError.message : String(recordError)}`,
service: SERVICE_NAME,
method: `runJob:${job.type}`,
});
}); // Record failure and release lock
} finally {
clearInterval(renewTimer);
}
@@ -218,9 +224,18 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
const jobs = await this.jobsRepository.claimDueBatch(type, capacity, Date.now());
for (const job of jobs) {
this.inFlight[type]++; // stay within concurrency limits
this.runJob(job).finally(() => {
this.inFlight[type]--; // job done, free the slot
});
this.runJob(job)
.catch((error: unknown) => {
// defensive, runJob already catches
this.logger.error({
message: error instanceof Error ? error.message : String(error),
service: SERVICE_NAME,
method: `runJob:${type}`,
});
})
.finally(() => {
this.inFlight[type]--; // job done, free the slot
});
}
// Back off if no jobs found, reset to base if jobs found
this.pollMs[type] = capacity > 0 && jobs.length === 0 ? Math.min(this.pollMs[type] * 2, POLL_MAX_MS) : POLL_MS;
@@ -104,4 +104,16 @@ describe("JobScheduler.init (scheduler-only primary reconcile)", () => {
expect(row?.mode).toBe("primary");
await scheduler2.shutdown();
});
// Regression for the heartbeat-re-upsert leak: a graceful shutdown must remove the
// worker's registry row outright, not leave a ghost that lingers until the TTL reaps it.
it("removes the worker's queue_workers row immediately on graceful shutdown", async () => {
const scheduler = makeScheduler();
await scheduler.init();
expect(await QueueWorkerModel.findById(WORKER_ID).lean()).not.toBeNull(); // registered while running
await scheduler.shutdown();
expect(await QueueWorkerModel.findById(WORKER_ID).lean()).toBeNull(); // gone immediately, not merely TTL-expired
});
});
@@ -1,91 +0,0 @@
import { describe, expect, it } from "@jest/globals";
import { applyDefaultTheme, withoutThemeFields } from "../../../src/api/controllers/statusPageThemeFlag.ts";
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE } from "../../../src/domain/status-pages/status-page.type.ts";
import { makeStatusPage } from "../../helpers/makeStatusPage.ts";
describe("statusPageThemeFlag", () => {
describe("withoutThemeFields", () => {
it("removes theme and themeMode from a body that has them", () => {
const body = { companyName: "Co", theme: "modern", themeMode: "dark" };
const result = withoutThemeFields(body);
expect(result).toEqual({ companyName: "Co" });
});
it("is a no-op when neither field is present", () => {
const body = { companyName: "Co", url: "page" };
const result = withoutThemeFields(body);
expect(result).toEqual({ companyName: "Co", url: "page" });
});
it("removes only the theme fields when one is present", () => {
const body = { companyName: "Co", theme: "bold" };
const result = withoutThemeFields(body);
expect(result).toEqual({ companyName: "Co" });
});
it("does not mutate the input body", () => {
const body = { companyName: "Co", theme: "modern", themeMode: "dark" };
withoutThemeFields(body);
expect(body).toEqual({ companyName: "Co", theme: "modern", themeMode: "dark" });
});
it("preserves all other fields verbatim", () => {
const body = {
companyName: "Co",
monitors: ["m-1", "m-2"],
isPublished: true,
showCharts: false,
color: "#13715B",
theme: "editorial",
themeMode: "light",
};
const result = withoutThemeFields(body);
expect(result).toEqual({
companyName: "Co",
monitors: ["m-1", "m-2"],
isPublished: true,
showCharts: false,
color: "#13715B",
});
});
});
describe("applyDefaultTheme", () => {
it("overwrites theme and themeMode with the canonical defaults", () => {
const sp = makeStatusPage({ theme: "modern", themeMode: "dark" });
const result = applyDefaultTheme(sp);
expect(result.theme).toBe(DEFAULT_STATUS_PAGE_THEME);
expect(result.themeMode).toBe(DEFAULT_STATUS_PAGE_THEME_MODE);
});
it("leaves all other fields untouched", () => {
const sp = makeStatusPage({
companyName: "Test Co",
url: "my-page",
monitors: ["mon-1", "mon-2"],
isPublished: true,
color: "#13715B",
});
const result = applyDefaultTheme(sp);
expect(result.companyName).toBe("Test Co");
expect(result.url).toBe("my-page");
expect(result.monitors).toEqual(["mon-1", "mon-2"]);
expect(result.isPublished).toBe(true);
expect(result.color).toBe("#13715B");
});
it("does not mutate the input status page", () => {
const sp = makeStatusPage({ theme: "modern", themeMode: "dark" });
applyDefaultTheme(sp);
expect(sp.theme).toBe("modern");
expect(sp.themeMode).toBe("dark");
});
it("works on a status page that has no theme set yet", () => {
const sp = makeStatusPage({ theme: undefined, themeMode: undefined });
const result = applyDefaultTheme(sp);
expect(result.theme).toBe(DEFAULT_STATUS_PAGE_THEME);
expect(result.themeMode).toBe(DEFAULT_STATUS_PAGE_THEME_MODE);
});
});
});
@@ -119,6 +119,35 @@ describe("AdvancedMatcher", () => {
expect(result.ok).toBe(false);
});
it("returns a clear error when the regex pattern is rejected by RE2", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate(
{ code: "200" },
// Backreferences are unsupported by RE2 and previously ReDoS-prone; RE2 throws on construction.
makeMonitor({ useAdvancedMatching: true, jsonPath: "code", matchMethod: "regex", expectedValue: "(a+)\\1" })
);
expect(result.ok).toBe(false);
expect(result.message).toBe("Invalid regex pattern");
expect(result.extracted).toBe("200");
});
it("returns a distinct error when the extracted value cannot be coerced to a string", () => {
// An object with a throwing toString/valueOf (like Object.create(null)) makes String() throw.
const unstringifiable = {
toString: () => {
throw new Error("nope");
},
};
const jmespath = { search: jest.fn().mockReturnValue(unstringifiable) };
const matcher = new AdvancedMatcher(jmespath as any);
const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field", matchMethod: "equal", expectedValue: "x" }));
expect(result.ok).toBe(false);
expect(result.message).toBe("Error evaluating response value");
});
it("defaults to 'equal' comparison when matchMethod is undefined", () => {
const matcher = new AdvancedMatcher(createMockJmespath() as any);
const result = matcher.validate({ val: "exact" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "val", expectedValue: "exact" }));
@@ -306,6 +306,83 @@ describe("DBQueueWorker", () => {
expect(mocks.jobsRepository.recordSuccess).not.toHaveBeenCalled();
});
// Regression for the "worker crashes on a Mongo blip" bug: recordFailure is the last write in a
// job's lifecycle. During a Mongo outage it rejects, and on master that rejection escaped runJob →
// unhandledRejection → the whole worker exited (bypassing graceful drain). It must be swallowed.
it("does not propagate when recordFailure itself rejects (transient Mongo failure)", async () => {
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => unhandled.push(reason);
process.on("unhandledRejection", onUnhandled);
try {
const job = makeJob({ type: "check" });
const failingProducer = { produce: jest.fn<any>().mockRejectedValue(new Error("network down")) };
const { mocks, worker } = await start({
mocks: {
jobsRepository: {
...createWorker().mocks.jobsRepository,
claimDueBatch: claimOnce(job),
recordFailure: jest.fn<any>().mockRejectedValue(new Error("mongo unavailable")),
},
checkProducer: failingProducer,
},
});
await jest.advanceTimersByTimeAsync(1); // let the recordFailure rejection settle
// the failing failure-write is caught and logged, not thrown
expect(mocks.jobsRepository.recordFailure).toHaveBeenCalledWith(job.id, expect.any(Error), expect.any(Number));
expect(mocks.logger.error).toHaveBeenCalledWith(
expect.objectContaining({ message: expect.stringContaining("recordFailure failed for job check:m1") })
);
expect(unhandled).toEqual([]); // nothing escaped to the process
expect(worker.getHealth().inFlight).toBe(0); // slot freed despite the double failure
} finally {
process.off("unhandledRejection", onUnhandled);
}
});
it("swallows the failure when both the success and failure writes reject", async () => {
const job = makeJob({ type: "check", intervalMs: 60000 }); // produce succeeds → recordSuccess path
const { mocks, worker } = await start({
mocks: {
jobsRepository: {
...createWorker().mocks.jobsRepository,
claimDueBatch: claimOnce(job),
recordSuccess: jest.fn<any>().mockRejectedValue(new Error("mongo unavailable")),
recordFailure: jest.fn<any>().mockRejectedValue(new Error("mongo still unavailable")),
},
},
});
await jest.advanceTimersByTimeAsync(1);
// success write is attempted, its rejection falls into the catch, and the failure write is too
expect(mocks.jobsRepository.recordSuccess).toHaveBeenCalled();
expect(mocks.jobsRepository.recordFailure).toHaveBeenCalledWith(job.id, expect.any(Error), expect.any(Number));
expect(mocks.logger.error).toHaveBeenCalledWith(
expect.objectContaining({ message: expect.stringContaining("recordFailure failed for job check:m1") })
);
expect(worker.getHealth().inFlight).toBe(0);
});
it("frees the concurrency slot and keeps polling after a job's completion writes fail", async () => {
const job = makeJob({ type: "check" });
const { mocks, worker } = await start({
mocks: {
jobsRepository: {
...createWorker().mocks.jobsRepository,
claimDueBatch: claimOnce(job),
recordFailure: jest.fn<any>().mockRejectedValue(new Error("mongo unavailable")),
},
checkProducer: { produce: jest.fn<any>().mockRejectedValue(new Error("network down")) },
},
});
await jest.advanceTimersByTimeAsync(1);
expect(worker.getHealth().inFlight).toBe(0); // slot released via .finally
const claimsBefore = mocks.jobsRepository.claimDueBatch.mock.calls.length;
await jest.advanceTimersByTimeAsync(POLL_MS); // the loop re-arms and keeps claiming — not wedged
expect(mocks.jobsRepository.claimDueBatch.mock.calls.length).toBeGreaterThan(claimsBefore);
});
it("parks a one-shot job (intervalMs null) via recordOneShot instead of rescheduling", async () => {
const job = makeJob({ id: "evaluate:m1", type: "evaluate", intervalMs: null });
const { mocks } = await start({ mocks: { jobsRepository: { ...createWorker().mocks.jobsRepository, claimDueBatch: claimOnce(job) } } });
@@ -142,4 +142,52 @@ describe("JobScheduler", () => {
expect((scheduler as any).timers.has("check")).toBe(false);
});
});
// Regression: a heartbeat upsert racing shutdown's deleteById could resurrect the
// registry row, leaving a ghost worker that inflates the alive count until TTL.
// shutdown() must await the in-flight beat and issue the delete as the LAST write.
describe("shutdown (registry deregistration)", () => {
it("awaits an in-flight heartbeat before deregistering, so the delete is the last write", async () => {
const order: string[] = [];
let resolveUpsert: () => void = () => {};
const upsert = jest.fn<any>().mockImplementation(
() =>
new Promise<void>((res) => {
resolveUpsert = () => {
order.push("upsert");
res();
};
})
);
const deleteById = jest.fn<any>().mockImplementation(async () => {
order.push("deleteById");
});
const { scheduler } = createScheduler({ mocks: { queueWorkersRepository: { upsert, deleteById } } });
// A beat fires and its upsert is still in flight (deferred, not yet committed).
const beat = (scheduler as any).heartbeat();
// Shutdown begins while that beat is in flight.
const shutdownPromise = scheduler.shutdown();
// The delete must not run until the in-flight upsert has settled.
expect(deleteById).not.toHaveBeenCalled();
resolveUpsert();
await beat;
await shutdownPromise;
expect(deleteById).toHaveBeenCalledWith("worker-1");
expect(order).toEqual(["upsert", "deleteById"]); // delete is always the last write to the row
});
it("heartbeat is a no-op once stopped, so no beat re-registers during drain/shutdown", async () => {
const { scheduler, mocks } = createScheduler();
await scheduler.drain(); // sets stopped = true
mocks.queueWorkersRepository.upsert.mockClear();
await (scheduler as any).heartbeat();
expect(mocks.queueWorkersRepository.upsert).not.toHaveBeenCalled();
});
});
});
@@ -2,6 +2,8 @@ import { describe, expect, it, jest } from "@jest/globals";
import { StatusPageService } from "../../../src/domain/status-pages/status-page.service.ts";
import type { IStatusPagesRepository } from "../../../src/domain/status-pages/status-page-repository.interface.ts";
import type { ISettingsService } from "../../../src/domain/app-settings/app-settings.service.ts";
import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts";
import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts";
import type { StatusPage } from "../../../src/domain/status-pages/status-page.type.ts";
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE } from "../../../src/domain/status-pages/status-page.type.ts";
@@ -33,17 +35,54 @@ const createRepo = () =>
removeMonitorFromStatusPages: jest.fn().mockResolvedValue(1),
}) as unknown as jest.Mocked<IStatusPagesRepository>;
const createSettingsService = (themesEnabled: boolean, clientHost = "http://localhost:5173") =>
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
name: "API Health",
type: "http",
status: "up",
url: "http://internal.example.com/health",
port: 8080,
uptimePercentage: 99.9,
recentChecks: [],
secret: "super-secret-bearer-token",
userId: "user-1",
teamId: "team-1",
notifications: ["notif-1"],
jsonPath: "$.status",
expectedValue: "ok",
matchMethod: "equal",
useAdvancedMatching: true,
customUpCodes: ["200"],
dnsServer: "10.0.0.53",
dnsRecordType: "A",
ignoreTlsErrors: false,
cpuAlertThreshold: 90,
memoryAlertThreshold: 90,
diskAlertThreshold: 90,
tempAlertThreshold: 90,
selectedDisks: ["sda"],
...overrides,
}) as Monitor;
const createSettingsService = (themesEnabled: boolean, clientHost = "http://localhost:5173", showURL = false) =>
({
areStatusPageThemesEnabled: jest.fn().mockReturnValue(themesEnabled),
getSettings: jest.fn().mockReturnValue({ clientHost }),
getDBSettings: jest.fn().mockResolvedValue({ showURL }),
}) as unknown as jest.Mocked<ISettingsService>;
const createService = (themesEnabled = true, clientHost = "http://localhost:5173") => {
const createMonitorsRepo = () =>
({
findByIds: jest.fn().mockResolvedValue([makeMonitor()]),
}) as unknown as jest.Mocked<IMonitorsRepository>;
const createService = (themesEnabled = true, clientHost = "http://localhost:5173", showURL = false) => {
const repo = createRepo();
const settingsService = createSettingsService(themesEnabled, clientHost);
const service = new StatusPageService(repo, settingsService);
return { service, repo, settingsService };
const settingsService = createSettingsService(themesEnabled, clientHost, showURL);
const monitorsRepo = createMonitorsRepo();
const service = new StatusPageService(repo, settingsService, monitorsRepo);
return { service, repo, settingsService, monitorsRepo };
};
// ── Tests ────────────────────────────────────────────────────────────────────
@@ -206,6 +245,116 @@ describe("StatusPageService", () => {
});
});
describe("getPublicStatusPagePayload", () => {
const SENSITIVE_FIELDS = [
"secret",
"userId",
"teamId",
"notifications",
"jsonPath",
"expectedValue",
"matchMethod",
"useAdvancedMatching",
"customUpCodes",
"dnsServer",
"dnsRecordType",
"ignoreTlsErrors",
"cpuAlertThreshold",
"memoryAlertThreshold",
"diskAlertThreshold",
"tempAlertThreshold",
"selectedDisks",
] as const;
const publishedPage = () => makeStatusPage({ isPublished: true });
it("never exposes secret or internal fields — even when showURL is ON (regression)", async () => {
const { service } = createService(true, "http://localhost:5173", true);
const { monitors } = await service.getPublicStatusPagePayload(publishedPage(), undefined);
expect(monitors).toHaveLength(1);
for (const field of SENSITIVE_FIELDS) {
expect(monitors[0]).not.toHaveProperty(field);
}
});
it("withholds the same fields when showURL is OFF", async () => {
const { service } = createService(true, "http://localhost:5173", false);
const { monitors } = await service.getPublicStatusPagePayload(publishedPage(), undefined);
for (const field of SENSITIVE_FIELDS) {
expect(monitors[0]).not.toHaveProperty(field);
}
});
it("includes url/port only when showURL is ON", async () => {
const withShowURL = createService(true, "http://localhost:5173", true);
const withoutShowURL = createService(true, "http://localhost:5173", false);
const shown = await withShowURL.service.getPublicStatusPagePayload(publishedPage(), undefined);
const hidden = await withoutShowURL.service.getPublicStatusPagePayload(publishedPage(), undefined);
expect(shown.monitors[0]).toHaveProperty("url", "http://internal.example.com/health");
expect(shown.monitors[0]).toHaveProperty("port", 8080);
expect(shown.monitors[0]).not.toHaveProperty("secret");
expect(hidden.monitors[0]).not.toHaveProperty("url");
expect(hidden.monitors[0]).not.toHaveProperty("port");
});
it("keeps the display fields the public themes consume", async () => {
const { service } = createService();
const { monitors } = await service.getPublicStatusPagePayload(publishedPage(), undefined);
expect(monitors[0]).toMatchObject({
id: "mon-1",
name: "API Health",
type: "http",
status: "up",
uptimePercentage: 99.9,
recentChecks: [],
checks: [],
});
});
it("orders monitors to match the status page's monitor list", async () => {
const { service, monitorsRepo } = createService();
(monitorsRepo.findByIds as jest.Mock).mockResolvedValue([makeMonitor({ id: "mon-1" }), makeMonitor({ id: "mon-2" })]);
const page = makeStatusPage({ isPublished: true, monitors: ["mon-2", "mon-1"] });
const { monitors } = await service.getPublicStatusPagePayload(page, undefined);
expect(monitors.map((monitor) => monitor.id)).toEqual(["mon-2", "mon-1"]);
});
it("rejects an unpublished page for a mismatched or absent requester team (403)", async () => {
const { service } = createService();
const unpublished = makeStatusPage({ isPublished: false, teamId: "team-A" });
await expect(service.getPublicStatusPagePayload(unpublished, "team-B")).rejects.toMatchObject({ status: 403 });
await expect(service.getPublicStatusPagePayload(unpublished, undefined)).rejects.toMatchObject({ status: 403 });
});
it("serves an unpublished page to its own team", async () => {
const { service } = createService();
const unpublished = makeStatusPage({ isPublished: false, teamId: "team-A" });
const { monitors } = await service.getPublicStatusPagePayload(unpublished, "team-A");
expect(monitors).toHaveLength(1);
});
it("serves a published page to an anonymous requester", async () => {
const { service } = createService();
await expect(service.getPublicStatusPagePayload(publishedPage(), undefined)).resolves.toMatchObject({
statusPage: expect.objectContaining({ id: "sp-1" }),
});
});
});
describe("deleteStatusPage", () => {
it("delegates to repository and returns deleted page", async () => {
const { service, repo } = createService(true);
@@ -278,6 +278,53 @@ describe("monitorValidation — HEAD method gating", () => {
});
});
describe("monitorValidation — regex pattern gating", () => {
const baseRegex = {
name: "regex check",
type: "http" as const,
url: "https://example.com",
useAdvancedMatching: true,
matchMethod: "regex" as const,
};
describe("createMonitorBodyValidation", () => {
it("accepts an RE2-supported regex pattern", () => {
const parsed = createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "^2\\d{2}$" });
expect(parsed.expectedValue).toBe("^2\\d{2}$");
});
it("rejects a pattern using a backreference", () => {
expect(() => createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "(a+)\\1" })).toThrow();
});
it("rejects a pattern using lookahead", () => {
expect(() => createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "foo(?=bar)" })).toThrow();
});
it("ignores an invalid regex when matchMethod is not 'regex'", () => {
const parsed = createMonitorBodyValidation.parse({ ...baseRegex, matchMethod: "include", expectedValue: "(a+)\\1" });
expect(parsed.expectedValue).toBe("(a+)\\1");
});
it("ignores an empty expectedValue", () => {
const parsed = createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "" });
expect(parsed.matchMethod).toBe("regex");
});
});
describe("editMonitorBodyValidation", () => {
it("rejects a pattern using a backreference", () => {
expect(() => editMonitorBodyValidation.parse({ matchMethod: "regex", expectedValue: "(a+)\\1" })).toThrow();
});
});
describe("importMonitorsBodyValidation", () => {
it("rejects a pattern using a backreference on imported monitors", () => {
expect(() => importMonitorsBodyValidation.parse({ monitors: [{ ...baseRegex, expectedValue: "(a+)\\1" }] })).toThrow();
});
});
});
describe("monitorValidation — customUpCodes", () => {
const baseHttpBody = {
name: "HTTP check",