diff --git a/server/package-lock.json b/server/package-lock.json index 06c1c30b3..92d258433 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,6 +45,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/nodemailer": "7.0.1", + "@types/ping": "0.4.4", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", @@ -72,6 +74,751 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.895.0.tgz", + "integrity": "sha512-tYfgwoG4n8LEc0NHVriNNS+YqeDMJHHcUzJxJJ7Nols5Y3EJzv76QRvu2rdLa9XoxvTSFGXJSuqrd8s0ElbCQQ==", + "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.894.0", + "@aws-sdk/credential-provider-node": "3.895.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/signature-v4-multi-region": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.895.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.11.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.3", + "@smithy/middleware-retry": "^4.2.4", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.3", + "@smithy/util-defaults-mode-node": "^4.1.3", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.895.0.tgz", + "integrity": "sha512-AQHk6iJrwce/NwZa5/Njy0ZGoHdxWCajkgufhXk53L0kRiC3vUPPWEV1m1F3etQWhaUsatcO2xtRuKvLpe4zgA==", + "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.894.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.895.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.11.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.3", + "@smithy/middleware-retry": "^4.2.4", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.3", + "@smithy/util-defaults-mode-node": "^4.1.3", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.894.0.tgz", + "integrity": "sha512-7zbO31NV2FaocmMtWOg/fuTk3PC2Ji2AC0Fi2KqrppEDIcwLlTTuT9w/rdu/93Pz+wyUhCxWnDc0tPbwtCLs+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@aws-sdk/xml-builder": "3.894.0", + "@smithy/core": "^3.11.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.894.0.tgz", + "integrity": "sha512-2aiQJIRWOuROPPISKgzQnH/HqSfucdk5z5VMemVH3Mm2EYOrzBwmmiiFpmSMN3ST+sE8c7gusqycUchP+KfALQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.894.0.tgz", + "integrity": "sha512-Z5QQpqFRflszrT+lUq6+ORuu4jRDcpgCUSoTtlhczidMqfdOSckKmK3chZEfmUUJPSwoFQZ7EiVTsX3c886fBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.895.0.tgz", + "integrity": "sha512-uIh7N4IN/yIk+qYMAkVpVkjhB90SGKSfaXEVcnmxzBDG6e5304HKT0esqoCVZvtFfLKasjm2TOpalM5l3fi/dA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/credential-provider-env": "3.894.0", + "@aws-sdk/credential-provider-http": "3.894.0", + "@aws-sdk/credential-provider-process": "3.894.0", + "@aws-sdk/credential-provider-sso": "3.895.0", + "@aws-sdk/credential-provider-web-identity": "3.895.0", + "@aws-sdk/nested-clients": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.895.0.tgz", + "integrity": "sha512-7xsBCmkBUz+2sNqNsDJ1uyQsBvwhNFzwFt8wX39WrFJTpTQh3uNQ5g8QH21BbkKqIFKCLdvgHgwt3Ub5RGVuPA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.894.0", + "@aws-sdk/credential-provider-http": "3.894.0", + "@aws-sdk/credential-provider-ini": "3.895.0", + "@aws-sdk/credential-provider-process": "3.894.0", + "@aws-sdk/credential-provider-sso": "3.895.0", + "@aws-sdk/credential-provider-web-identity": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.894.0.tgz", + "integrity": "sha512-VU74GNsj+SsO+pl4d+JimlQ7+AcderZaC6bFndQssQdFZ5NRad8yFNz5Xbec8CPJr+z/VAwHib6431F5nYF46g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.895.0.tgz", + "integrity": "sha512-bZCcHUZGz+XlCaK0KEOHGHkMtlwIvnpxJvlZtSCVaBdX/IgouxaB42fxChflxSMRWF45ygdezfky4i17f6vC4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.895.0", + "@aws-sdk/core": "3.894.0", + "@aws-sdk/token-providers": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.895.0.tgz", + "integrity": "sha512-tKbXbOp2xrL02fxKvB7ko1E4Uvyy5TF9qi5pT2MVWNnfSsBlUM80aJ6tyUPKWXdUTdAlPrU3XcwgQl/DnnRa9A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/nested-clients": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz", + "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz", + "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz", + "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.894.0.tgz", + "integrity": "sha512-0C3lTVdTuv5CkJ4LulpA7FmGFSKrGUKxnFZ6+qGjYjNzbdiHXfq0TyEBiDmVqDkoV2k4AT2H/m0Xw//rTkcNEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.11.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.895.0.tgz", + "integrity": "sha512-JUqQW2RPp4I95wZ/Im9fTiaX3DF55oJgeoiNlLdHkQZPSNNS/pT1WMWMReSvJdcfSNU3xSUaLtI+h4mQjQUDbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@smithy/core": "^3.11.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.895.0.tgz", + "integrity": "sha512-8w1ihfYgvds6kfal/qJXQQrHRsKYh2nujSyzWMo2TMKMze9WPZA93G4mRbRtKtbSuQ66mVWePH8Cksq35ABu2Q==", + "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.894.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.895.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.11.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.3", + "@smithy/middleware-retry": "^4.2.4", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.3", + "@smithy/util-defaults-mode-node": "^4.1.3", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz", + "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.894.0.tgz", + "integrity": "sha512-Te5b3fSbatkZrh3eYNmpOadZFKsCLNSwiolQKQeEeKHxdnqORwYXa+0ypcTHle6ukic+tFRRd9n3NuMVo9uiVg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.895.0.tgz", + "integrity": "sha512-vJqrEHFFGRZ3ok5T+jII00sa2DQ3HdVkTBIfM0DcrcPssqDV18VKdA767qiBdIEN/cygjdBg8Ri/cuq6ER9BeQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/nested-clients": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz", + "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz", + "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-endpoints": "^3.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz", + "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.895.0.tgz", + "integrity": "sha512-lLRC7BAFOPtJk4cZC0Q0MZBMCGF109QpGnug3L3n/2TJW02Sinz9lzA0ykBpYXe9j60LjIYSENCg+F4DZE5vxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz", + "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder/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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1655,6 +2402,626 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, + "node_modules/@smithy/abort-controller": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", + "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.12.0.tgz", + "integrity": "sha512-zJeAgogZfbwlPGL93y4Z/XNeIN37YCreRUd6YMIRvaq+6RnBK8PPYYIQ85Is/GglPh3kNImD5riDCXbVSDpCiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.2", + "@smithy/util-utf8": "^4.1.0", + "@smithy/uuid": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", + "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", + "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", + "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", + "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", + "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.4.tgz", + "integrity": "sha512-FZ4hzupOmthm8Q8ujYrd0I+/MHwVMuSTdkDtIQE0xVuvJt9pLT6Q+b0p4/t+slDyrpcf+Wj7SN+ZqT5OryaaZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.12.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.3.0.tgz", + "integrity": "sha512-qhEX9745fAxZvtLM4bQJAVC98elWjiMO2OiHl1s6p7hUzS4QfZO1gXUYNwEK8m0J6NoCD5W52ggWxbIDHI0XSg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.2", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/uuid": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", + "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", + "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", + "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", + "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", + "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", + "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-uri-escape": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", + "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz", + "integrity": "sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", + "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", + "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-uri-escape": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.4.tgz", + "integrity": "sha512-qL7O3VDyfzCSN9r+sdbQXGhaHtrfSJL30En6Jboj0I3bobf2g1/T0eP2L4qxqrEW26gWhJ4THI4ElVVLjYyBHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.12.0", + "@smithy/middleware-endpoint": "^4.2.4", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", + "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", + "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", + "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", + "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", + "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", + "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", + "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.4.tgz", + "integrity": "sha512-mLDJ1s4eA3vwOGaQOEPlg5LB4LdZUUMpB5UMOMofeGhWqiS7WR7dTpLiNi9zVn+YziKUd3Af5NLfxDs7NJqmIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.4.tgz", + "integrity": "sha512-pjX2iMTcOASaSanAd7bu6i3fcMMezr3NTr8Rh64etB0uHRZi+Aw86DoCxPESjY4UTIuA06hhqtTtw95o//imYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.2.2", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", + "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", + "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.2.tgz", + "integrity": "sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.1.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz", + "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", + "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", + "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.0.0.tgz", + "integrity": "sha512-OlA/yZHh0ekYFnbUkmYBDQPE6fGfdrvgz39ktp8Xf+FA6BfxLejPTMDOG0Nfk5/rDySAz1dRbFf24zaAFYVXlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -1714,6 +3081,24 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", + "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, + "node_modules/@types/ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/relateurl": { "version": "0.2.33", "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", @@ -2073,6 +3458,13 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "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==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", diff --git a/server/package.json b/server/package.json index 637c785cc..2bee352e0 100755 --- a/server/package.json +++ b/server/package.json @@ -54,6 +54,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/nodemailer": "7.0.1", + "@types/ping": "0.4.4", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", diff --git a/server/src/config/index.ts b/server/src/config/index.ts new file mode 100644 index 000000000..7ace6f99c --- /dev/null +++ b/server/src/config/index.ts @@ -0,0 +1,25 @@ +import dotenv from "dotenv"; + +export interface IEevConfig { + NODE_ENV: string; + JWT_SECRET: string; + PORT: number; + PAGESPEED_API_KEY: string; + SMTP_HOST: string; + SMTP_PORT: number; + SMTP_USER: string; + SMTP_PASS: string; +} + +dotenv.config(); + +export const config: IEevConfig = { + NODE_ENV: process.env.NODE_ENV || "development", + JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret", + PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, + PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "", + SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com", + SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587, + SMTP_USER: process.env.SMTP_USER || "user@example.com", + SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password", +}; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts new file mode 100644 index 000000000..cf49a71c6 --- /dev/null +++ b/server/src/service/v2/business/AuthService.ts @@ -0,0 +1,206 @@ +import bcrypt from "bcryptjs"; +import { User, Role, ITokenizedUser, Monitor, Check, NotificationChannel } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; +import { Types } from "mongoose"; +import { IJobQueue } from "../infrastructure/JobQueue.js"; + +export const PERMISSIONS = { + users: { + all: "users.*", + create: "users.create", + view: "users.view", + update: "users.update", + delete: "users.delete", + }, + + monitors: { + all: "monitors.*", + create: "monitors.create", + view: "monitors.view", + update: "monitors.update", + delete: "monitors.delete", + }, + notifications: { + all: "notifications.*", + create: "notifications.create", + view: "notifications.view", + update: "notifications.update", + delete: "notifications.delete", + }, + checks: { + all: "checks.*", + create: "checks.create", + view: "checks.view", + update: "checks.update", + delete: "checks.delete", + }, + statusPages: { + all: "statusPages.*", + create: "statusPages.create", + view: "statusPages.view", + update: "statusPages.update", + delete: "statusPages.delete", + }, +}; + +const DEFAULT_ROLES = [ + { + name: "SuperAdmin", + description: "Super admin with all permissions", + permissions: ["*"], + isSystem: true, + }, + { + name: "Admin", + description: "Admin with full permissions", + permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all], + isSystem: true, + }, + { + name: "Manager", + description: "Can manage users", + permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all], + isSystem: true, + }, + { + name: "Member", + description: "Basic team member", + permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update], + isSystem: true, + }, +]; + +export type RegisterData = { + email: string; + firstName: string; + lastName: string; + password: string; + roles?: Types.ObjectId[]; // Optional roles for invite-based registration +}; + +export type LoginData = { + email: string; + password: string; +}; + +export type AuthResult = ITokenizedUser; + +export interface IAuthService { + register(signupData: RegisterData): Promise; + registerWithInvite(signupData: RegisterData): Promise; + login(loginData: LoginData): Promise; + cleanup(): Promise; + cleanMonitors(): Promise; +} + +class AuthService implements IAuthService { + private jobQueue: IJobQueue; + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + } + + async register(signupData: RegisterData): Promise { + const userCount = await User.countDocuments(); + + if (userCount > 0) { + throw new Error("Registration is closed. Please request an invite."); + } + + const { email, firstName, lastName, password } = signupData; + + // Create all default roles + const rolePromises = DEFAULT_ROLES.map((roleData) => + new Role({ + ...roleData, + }).save() + ); + const roles = await Promise.all(rolePromises); + + // Hash password and create user + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // Find admin role and assign to first user + const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); + + const user = new User({ + email, + firstName, + lastName, + passwordHash, + roles: [superAdminRole!._id], + }); + + const savedUser = await user.save(); + return { + sub: savedUser._id.toString(), + roles: savedUser.roles.map((role) => role.toString()), + }; + } + + async registerWithInvite(signupData: RegisterData): Promise { + const { email, firstName, lastName, password, roles } = signupData; + + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + const user = new User({ + email, + firstName, + lastName, + passwordHash, + roles: roles || [], + }); + try { + const savedUser = await user.save(); + return { + sub: savedUser._id.toString(), + roles: savedUser.roles.map((role) => role.toString()), + }; + } catch (error: any) { + if (error?.code === 11000) { + const dupError = new ApiError("Email already in use", 409); + dupError.stack = error?.stack; + throw dupError; + } + throw error; + } + } + + async login(loginData: LoginData): Promise { + const { email, password } = loginData; + + // Find user by email + const user = await User.findOne({ email }); + + if (!user) { + throw new Error("Invalid email or password"); + } + + // Check password + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + if (!isPasswordValid) { + throw new Error("Invalid email or password"); + } + + return { + sub: user._id.toString(), + roles: user.roles.map((role) => role.toString()), + }; + } + + async cleanup() { + await User.deleteMany({}); + await Role.deleteMany({}); + await Monitor.deleteMany({}); + await Check.deleteMany({}); + await NotificationChannel.deleteMany({}); + await this.jobQueue.flush(); + } + + async cleanMonitors() { + await Monitor.deleteMany({}); + await Check.deleteMany({}); + } +} + +export default AuthService; diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts new file mode 100644 index 000000000..5c68891a9 --- /dev/null +++ b/server/src/service/v2/business/CheckService.ts @@ -0,0 +1,132 @@ +import { json } from "stream/consumers"; +import { ICheck, Check, Monitor } from "../../../db/v2/models/index.js"; +import type { ISystemInfo, ICaptureInfo } from "../../../db/v2/models/index.js"; +import { MonitorType } from "../../../db/v2/models/monitors/Monitor.js"; +import { StatusResponse } from "../infrastructure/NetworkService.js"; +import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; +import mongoose from "mongoose"; + +export interface ICheckService { + buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; + cleanupOrphanedChecks: () => Promise; +} + +class CheckService implements ICheckService { + private isCapturePayload = (payload: any): payload is ICapturePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("data" in payload) || typeof payload.data !== "object") { + return false; + } + + const data = payload.data as Partial; + if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") { + return false; + } + + if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") { + return false; + } + + if (data.disk && !Array.isArray(data.disk)) { + return false; + } + if (data.net && !Array.isArray(data.net)) { + return false; + } + + if (!("capture" in payload) || typeof payload.capture !== "object") return false; + const capture = payload.capture as Record; + if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false; + + return true; + }; + + private isPagespeedPayload = (payload: any): payload is ILighthousePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") { + return false; + } + return true; + }; + + private buildBaseCheck = (statusResponse: StatusResponse) => { + const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); + const check = new Check({ + monitorId: monitorId, + type: statusResponse?.type, + status: statusResponse?.status, + message: statusResponse?.message, + responseTime: statusResponse?.responseTime, + timings: statusResponse?.timings, + }); + return check; + }; + + private buildInfrastructureCheck = (statusResponse: StatusResponse) => { + if (!this.isCapturePayload(statusResponse.payload)) { + throw new Error("Invalid payload for infrastructure monitor"); + } + const check = this.buildBaseCheck(statusResponse); + check.system = statusResponse.payload.data; + check.capture = statusResponse.payload.capture; + return check; + }; + + private buildPagespeedCheck = (statusResponse: StatusResponse) => { + if (!this.isPagespeedPayload(statusResponse.payload)) { + throw new Error("Invalid payload for pagespeed monitor"); + } + const check = this.buildBaseCheck(statusResponse); + const lighthouseResult = statusResponse?.payload?.lighthouseResult; + check.lighthouse = { + accessibility: lighthouseResult?.categories?.accessibility?.score || 0, + bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0, + seo: lighthouseResult?.categories?.seo?.score || 0, + performance: lighthouseResult?.categories?.performance?.score || 0, + audits: { + cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, + si: lighthouseResult?.audits?.["speed-index"] || {}, + fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, + lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, + tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, + }, + }; + return check; + }; + + buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => { + switch (type) { + case "infrastructure": + return this.buildInfrastructureCheck(statusResponse as StatusResponse); + + case "pagespeed": + return this.buildPagespeedCheck(statusResponse as StatusResponse); + case "http": + case "https": + return this.buildBaseCheck(statusResponse); + + case "ping": + return this.buildBaseCheck(statusResponse); + default: + throw new Error(`Unsupported monitor type: ${type}`); + } + }; + + cleanupOrphanedChecks = async () => { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await Check.deleteMany({ + monitorId: { $nin: monitorIds }, + }); + console.log(`Deleted ${result.deletedCount} orphaned Checks.`); + return true; + } catch (error) { + console.error("Error cleaning up orphaned Checks:", error); + return false; + } + }; +} + +export default CheckService; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts new file mode 100644 index 000000000..1c954d75f --- /dev/null +++ b/server/src/service/v2/business/InviteService.ts @@ -0,0 +1,61 @@ +import crypto from "node:crypto"; +import { ITokenizedUser, IInvite, Invite } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IInviteService { + create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>; + getAll: () => Promise; + get: (tokenHash: string) => Promise; + delete: (id: string) => Promise; +} + +class InviteService implements IInviteService { + constructor() {} + + create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => { + const token = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + try { + const invite = await Invite.create({ + ...inviteData, + tokenHash, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + if (!invite) { + throw new ApiError("Failed to create invite", 500); + } + return { token }; + } catch (error: any) { + if (error?.code === 11000) { + const dupError = new ApiError("Invite with this email already exists", 409); + dupError.stack = error?.stack; + throw dupError; + } + throw error; + } + }; + + get = async (token: string) => { + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + const invite = await Invite.findOne({ tokenHash }); + if (!invite) { + throw new ApiError("Invite not found", 404); + } + return invite; + }; + + getAll = async () => { + return Invite.find(); + }; + + delete = async (id: string) => { + const result = await Invite.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Invite not found", 404); + } + return result.deletedCount === 1; + }; +} + +export default InviteService; diff --git a/server/src/service/v2/business/MaintenanceService.ts b/server/src/service/v2/business/MaintenanceService.ts new file mode 100644 index 000000000..ef8c8152f --- /dev/null +++ b/server/src/service/v2/business/MaintenanceService.ts @@ -0,0 +1,141 @@ +import { ITokenizedUser, IMaintenance, Maintenance } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IMaintenanceService { + create: ( + tokenizedUser: ITokenizedUser, + + maintenance: IMaintenance + ) => Promise; + getAll: () => Promise; + get: (id: string) => Promise; + toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise; + update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise; + delete: (id: string) => Promise; + isInMaintenance: (monitorId: string) => Promise; +} + +type MaintenanceCache = Map; + +class MaintenanceService implements IMaintenanceService { + private maintenanceCache: MaintenanceCache; + private lastRefresh: number; + private CACHE_TTL_MS = 60 * 1000; + + constructor() { + this.maintenanceCache = new Map(); + this.lastRefresh = 0; + } + + create = async (tokenizedUser: ITokenizedUser, maintenanceData: IMaintenance) => { + const maintenance = await Maintenance.create({ + ...maintenanceData, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + return maintenance; + }; + + get = async (id: string) => { + const maintenance = await Maintenance.findById(id); + if (!maintenance) { + throw new ApiError("Maintenance not found", 404); + } + return maintenance; + }; + + getAll = async () => { + return Maintenance.find(); + }; + + toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => { + const updatedMaintenance = await Maintenance.findOneAndUpdate( + { _id: id }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + updatedBy: tokenizedUser.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + if (!updatedMaintenance) { + throw new ApiError("Maintenance not found", 404); + } + return updatedMaintenance; + }; + + update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => { + const allowedFields: (keyof IMaintenance)[] = ["name", "monitors", "startTime", "endTime", "isActive"]; + const safeUpdate: Partial = {}; + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedMaintenance = await Maintenance.findByIdAndUpdate( + id, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: tokenizedUser.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedMaintenance) { + throw new ApiError("Failed to update maintenance", 500); + } + + return updatedMaintenance; + }; + + delete = async (id: string) => { + const result = await Maintenance.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Maintenance not found", 404); + } + return result.deletedCount === 1; + }; + + private refreshCache = async () => { + const now = new Date(); + + const activeMaintenances = await Maintenance.find({ + isActive: true, + startTime: { $lte: now }, + endTime: { $gte: now }, + }).lean(); + + // Reset cache + const newCache = new Map(); + + for (const m of activeMaintenances) { + for (const monitorId of m.monitors) { + const key = monitorId.toString(); + if (!newCache.has(key)) newCache.set(key, []); + newCache.get(key)!.push(m); + } + } + + this.maintenanceCache = newCache; + this.lastRefresh = Date.now(); + }; + + isInMaintenance = async (monitorId: string) => { + const now = Date.now(); + if (now - this.lastRefresh > this.CACHE_TTL_MS) { + await this.refreshCache(); + } + const maintenances = this.maintenanceCache.get(monitorId) || []; + return maintenances.length > 0; + }; +} + +export default MaintenanceService; diff --git a/server/src/service/v2/business/MonitorStatsService.ts b/server/src/service/v2/business/MonitorStatsService.ts new file mode 100644 index 000000000..dc7d4acb3 --- /dev/null +++ b/server/src/service/v2/business/MonitorStatsService.ts @@ -0,0 +1,25 @@ +import { MonitorStats } from "../../../db/v2/models/index.js"; +import { Monitor } from "../../../db/v2/models/index.js"; +export interface IMonitorStatsService { + cleanupOrphanedMonitorStats: () => Promise; +} + +class MonitorStatsService implements IMonitorStatsService { + constructor() {} + + async cleanupOrphanedMonitorStats() { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await MonitorStats.deleteMany({ + monitorId: { $nin: monitorIds }, + }); + console.log(`Deleted ${result.deletedCount} orphaned MonitorStats.`); + return true; + } catch (error) { + console.error("Error cleaning up orphaned MonitorStats:", error); + return false; + } + } +} + +export default MonitorStatsService; diff --git a/server/src/service/v2/business/NotificationChannelService.ts b/server/src/service/v2/business/NotificationChannelService.ts new file mode 100644 index 000000000..35eae5b12 --- /dev/null +++ b/server/src/service/v2/business/NotificationChannelService.ts @@ -0,0 +1,100 @@ +import { ITokenizedUser, INotificationChannel, NotificationChannel, Monitor } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface INotificationChannelService { + create: ( + tokenizedUser: ITokenizedUser, + + notificationChannel: INotificationChannel + ) => Promise; + getAll: () => Promise; + get: (id: string) => Promise; + toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise; + update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise; + delete: (id: string) => Promise; +} + +class NotificationChannelService implements INotificationChannelService { + constructor() {} + + create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => { + const notificationChannel = await NotificationChannel.create({ + ...notificationChannelData, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + return notificationChannel; + }; + + get = async (id: string) => { + const channel = await NotificationChannel.findById(id); + if (!channel) { + throw new ApiError("Notification channel not found", 404); + } + return channel; + }; + + getAll = async () => { + return NotificationChannel.find(); + }; + + toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => { + const updatedChannel = await NotificationChannel.findOneAndUpdate( + { _id: id }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + updatedBy: tokenizedUser.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + if (!updatedChannel) { + throw new ApiError("Notification channel not found", 404); + } + return updatedChannel; + }; + + update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => { + const allowedFields: (keyof INotificationChannel)[] = ["name", "config", "isActive"]; + const safeUpdate: Partial = {}; + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedChannel = await NotificationChannel.findByIdAndUpdate( + id, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: tokenizedUser.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedChannel) { + throw new ApiError("Failed to update notification channel", 500); + } + + return updatedChannel; + }; + + delete = async (id: string) => { + const result = await NotificationChannel.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Notification channel not found", 404); + } + + await Monitor.updateMany({ notificationChannels: id }, { $pull: { notificationChannels: id } }); + return result.deletedCount === 1; + }; +} + +export default NotificationChannelService; diff --git a/server/src/service/v2/business/QueueService.ts b/server/src/service/v2/business/QueueService.ts new file mode 100644 index 000000000..20069072e --- /dev/null +++ b/server/src/service/v2/business/QueueService.ts @@ -0,0 +1,23 @@ +import { IJobQueue } from "../infrastructure/JobQueue.js"; + +class QueueService { + private jobQueue: IJobQueue; + + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + } + + async getMetrics() { + return await this.jobQueue.getMetrics(); + } + + async getJobs() { + return await this.jobQueue.getJobs(); + } + + async flush() { + return await this.jobQueue.flush(); + } +} + +export default QueueService; diff --git a/server/src/service/v2/business/UserService.ts b/server/src/service/v2/business/UserService.ts new file mode 100644 index 000000000..46d221759 --- /dev/null +++ b/server/src/service/v2/business/UserService.ts @@ -0,0 +1,13 @@ +import { IUser, User } from "../../../db/v2/models/index.js"; + +export interface IUserService { + getAllUsers(): Promise; +} + +class UserService implements IUserService { + async getAllUsers(): Promise { + return await User.find(); + } +} + +export default UserService; diff --git a/server/src/service/v2/infrastructure/JobGenerator.ts b/server/src/service/v2/infrastructure/JobGenerator.ts new file mode 100644 index 000000000..4f2beaf71 --- /dev/null +++ b/server/src/service/v2/infrastructure/JobGenerator.ts @@ -0,0 +1,80 @@ +import { IMonitor } from "../../../db/v2/models/index.js"; +import { INetworkService } from "./NetworkService.js"; +import { ICheckService } from "../business/CheckService.js"; +import { IMonitorStatsService } from "../business/MonitorStatsService.js"; +import { IStatusService } from "./StatusService.js"; +import { INotificationService } from "./NotificationService.js"; +import { IMaintenanceService } from "../business/MaintenanceService.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IJobGenerator { + generateJob: () => (Monitor: IMonitor) => Promise; + generateCleanupJob: () => () => Promise; +} + +class JobGenerator implements IJobGenerator { + private networkService: INetworkService; + private checkService: ICheckService; + private monitorStatsService: IMonitorStatsService; + private statusService: IStatusService; + private notificationService: INotificationService; + private maintenanceService: IMaintenanceService; + + constructor( + networkService: INetworkService, + checkService: ICheckService, + monitorStatsService: IMonitorStatsService, + statusService: IStatusService, + notificationService: INotificationService, + maintenanceService: IMaintenanceService + ) { + this.networkService = networkService; + this.checkService = checkService; + this.monitorStatsService = monitorStatsService; + this.statusService = statusService; + this.notificationService = notificationService; + this.maintenanceService = maintenanceService; + } + + generateJob = () => { + return async (monitor: IMonitor) => { + try { + const monitorId = monitor._id.toString(); + if (!monitorId) { + throw new ApiError("No monitorID for creating job", 400); + } + + // Check for active maintenance window, if found, skip the check + const isInMaintenance = await this.maintenanceService.isInMaintenance(monitorId); + if (isInMaintenance) { + return; + } + + const status = await this.networkService.requestStatus(monitor); + const check = await this.checkService.buildCheck(status, monitor.type); + await check.save(); + const [updatedMonitor, statusChanged] = await this.statusService.updateMonitorStatus(monitor, status); + + if (statusChanged) { + await this.notificationService.handleNotifications(updatedMonitor); + } + await this.statusService.updateMonitorStats(updatedMonitor, status, statusChanged); + } catch (error) { + throw error; + } + }; + }; + + generateCleanupJob = () => { + return async () => { + try { + await this.checkService.cleanupOrphanedChecks(); + await this.monitorStatsService.cleanupOrphanedMonitorStats(); + } catch (error) { + throw error; + } + }; + }; +} + +export default JobGenerator; diff --git a/server/src/service/v2/infrastructure/JobQueue.ts b/server/src/service/v2/infrastructure/JobQueue.ts new file mode 100644 index 000000000..af415d3f7 --- /dev/null +++ b/server/src/service/v2/infrastructure/JobQueue.ts @@ -0,0 +1,227 @@ +import { IJob } from "super-simple-scheduler/dist/job/job.js"; +import { Monitor, IMonitor } from "../../../db/v2/models/index.js"; +import Scheduler from "super-simple-scheduler"; +import { IJobGenerator } from "./JobGenerator.js"; +export interface IJobMetrics { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: Array<{ + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number | null; + failReason: string | null; + }>; + totalRuns: number; + totalFailures: number; +} + +export interface IJobData extends IJob { + lastRunTook: number | null; +} + +export interface IJobQueue { + init: () => Promise; + addJob: (monitor: IMonitor) => Promise; + pauseJob: (monitor: IMonitor) => Promise; + resumeJob: (monitor: IMonitor) => Promise; + updateJob: (monitor: IMonitor) => Promise; + deleteJob: (monitor: IMonitor) => Promise; + getMetrics: () => Promise; + getJobs: () => Promise; + flush: () => Promise; + shutdown: () => Promise; +} + +export default class JobQueue implements IJobQueue { + private scheduler: Scheduler; + private static instance: JobQueue | null = null; + private jobGenerator: any; + constructor() { + this.scheduler = new Scheduler({ + logLevel: "debug", + }); + } + + static async create(jobGenerator: IJobGenerator) { + if (!JobQueue.instance) { + const instance = new JobQueue(); + instance.jobGenerator = jobGenerator; + await instance.init(); + JobQueue.instance = instance; + } + return JobQueue.instance; + } + + static getInstance(): JobQueue | null { + return JobQueue.instance; + } + + init = async () => { + try { + this.scheduler.start(); + // Add template and jobs + this.scheduler.addTemplate("monitor-job", this.jobGenerator.generateJob()); + + // Add a cleanup job + this.scheduler.addTemplate("cleanup-job", this.jobGenerator.generateCleanupJob()); + await this.scheduler.addJob({ + id: "cleanup-orphaned-checks", + template: "cleanup-job", + repeat: 24 * 60 * 60 * 1000, // 24 hours + active: true, + }); + + const monitors = await Monitor.find(); + for (const monitor of monitors) { + this.addJob(monitor); + } + + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + addJob = async (monitor: IMonitor) => { + try { + return await this.scheduler?.addJob({ + id: monitor._id.toString(), + template: "monitor-job", + repeat: monitor.interval, + active: monitor.isActive, + data: monitor, + }); + } catch (error) { + console.error(error); + return false; + } + }; + + pauseJob = async (monitor: IMonitor) => { + try { + return await this.scheduler?.pauseJob(monitor._id.toString()); + } catch (error) { + console.error(error); + return false; + } + }; + + resumeJob = async (monitor: IMonitor) => { + try { + return await this.scheduler.resumeJob(monitor._id.toString()); + } catch (error) { + console.error(error); + return false; + } + }; + + updateJob = async (monitor: IMonitor) => { + try { + return await this.scheduler.updateJob(monitor._id.toString(), { + repeat: monitor.interval, + data: monitor, + }); + } catch (error) { + console.error(error); + return false; + } + }; + + deleteJob = async (monitor: IMonitor) => { + try { + this.scheduler?.removeJob(monitor._id.toString()); + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + getMetrics = async (): Promise => { + try { + const jobs = await this.scheduler.getJobs(); + const metrics: IJobMetrics = jobs.reduce( + (acc, job) => { + if (!job.data) return acc; + + acc.totalRuns += job.runCount || 0; + acc.totalFailures += job.failCount || 0; + acc.jobs++; + + // Check if job is currently failing (has recent failures) + const hasFailures = job.failCount && job.failCount > 0; + const isCurrentlyFailing = hasFailures && job.lastFailedAt && (!job.lastRunAt || job.lastFailedAt > job.lastRunAt); + + if (isCurrentlyFailing) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (hasFailures) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job.data?.url || null, + monitorType: job.data?.type || null, + failedAt: job.lastFailedAt || null, + failCount: job.failCount || null, + failReason: job.lastFailReason || null, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [], + totalRuns: 0, + totalFailures: 0, + } + ); + return metrics; + } catch (error) { + console.error(error); + return null; + } + }; + + getJobs = async (): Promise => { + try { + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + ...job, + lastRunTook: job.lockedAt || !job.lastFinishedAt || !job.lastRunAt ? null : job.lastFinishedAt - job.lastRunAt, + }; + }); + } catch (error) { + console.error(error); + return null; + } + }; + + flush = async () => { + try { + return await this.scheduler.flushJobs(); + } catch (error) { + console.error(error); + return false; + } + }; + + shutdown = async () => { + try { + return await this.scheduler.stop(); + } catch (error) { + console.error(error); + return false; + } + }; +} diff --git a/server/src/service/v2/infrastructure/NetworkService.ts b/server/src/service/v2/infrastructure/NetworkService.ts new file mode 100644 index 000000000..90657bd2d --- /dev/null +++ b/server/src/service/v2/infrastructure/NetworkService.ts @@ -0,0 +1,193 @@ +import { Got, HTTPError } from "got"; +import ping from "ping"; +import { IMonitor } from "../../../db/v2/models/index.js"; +import { GotTimings } from "../../../db/v2/models/checks/Check.js"; +import type { Response } from "got"; +import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v2/models/index.js"; +import { MonitorType, MonitorStatus } from "../../../db/v2/models/monitors/Monitor.js"; +import ApiError from "../../../utils/ApiError.js"; +import { config } from "../../../config/index.js"; +export interface INetworkService { + requestHttp: (monitor: IMonitor) => Promise; + requestInfrastructure: (monitor: IMonitor) => Promise; + requestStatus: (monitor: IMonitor) => Promise; + requestPagespeed: (monitor: IMonitor) => Promise; + requestPing: (monitor: IMonitor) => Promise; +} + +export interface ICapturePayload { + data: ISystemInfo; + capture: ICaptureInfo; +} + +export interface ILighthousePayload { + lighthouseResult: ILighthouseResult; +} + +export interface StatusResponse { + monitorId: string; + type: MonitorType; + code?: number; + status: MonitorStatus; + message: string; + responseTime: number; + timings?: GotTimings; + payload?: TPayload; +} + +class NetworkService implements INetworkService { + private got: Got; + private NETWORK_ERROR: number; + constructor(got: Got) { + this.got = got; + this.NETWORK_ERROR = 5000; + } + + private buildStatusResponse = (monitor: IMonitor, response: Response | null, error: any | null): StatusResponse => { + if (error) { + const statusResponse: StatusResponse = { + monitorId: monitor._id.toString(), + type: monitor.type, + status: "down" as MonitorStatus, + code: this.NETWORK_ERROR, + message: error.message || "Network error", + responseTime: 0, + timings: { phases: {} } as GotTimings, + }; + if (error instanceof HTTPError) { + statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR; + statusResponse.message = error.message || "HTTP error"; + statusResponse.responseTime = error.timings?.phases?.total || 0; + statusResponse.timings = error.timings; + } + return statusResponse; + } + + const statusResponse: StatusResponse = { + monitorId: monitor._id.toString(), + type: monitor.type, + code: response?.statusCode || this.NETWORK_ERROR, + status: response?.ok === true ? "up" : "down", + message: response?.statusMessage || "", + responseTime: response?.timings?.phases?.total || 0, + timings: response?.timings || ({ phases: {} } as GotTimings), + }; + + return statusResponse; + }; + + requestHttp = async (monitor: IMonitor) => { + try { + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + + try { + const response: Response = await this.got(url); + return this.buildStatusResponse(monitor, response, null); + } catch (error) { + return this.buildStatusResponse(monitor, null, error); + } + } catch (error) { + throw error; + } + }; + + requestInfrastructure = async (monitor: IMonitor) => { + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + const secret = monitor.secret; + if (!secret) { + throw new Error("No secret provided for infrastructure monitor"); + } + + let statusResponse: StatusResponse; + try { + const response: Response | null = await this.got(url, { + headers: { Authorization: `Bearer ${secret}` }, + responseType: "json", + }); + + statusResponse = this.buildStatusResponse(monitor, response, null); + if (!response?.body) { + throw new ApiError("No payload received from infrastructure monitor", 500); + } + statusResponse.payload = response?.body; + return statusResponse; + } catch (error) { + statusResponse = this.buildStatusResponse(monitor, null, error); + } + return statusResponse; + }; + + requestPagespeed = async (monitor: IMonitor) => { + const apiKey = config.PAGESPEED_API_KEY; + if (!apiKey) { + throw new Error("No API key provided for pagespeed monitor"); + } + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + + let statusResponse: StatusResponse; + + try { + const response: Response = await this.got(url); + statusResponse = this.buildStatusResponse(monitor, response, null) as StatusResponse; + } catch (error) { + statusResponse = this.buildStatusResponse(monitor, null, error); + } + + const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`; + const pagespeedResponse = await this.got(pagespeedUrl, { + responseType: "json", + }); + const payload = pagespeedResponse.body; + if (payload) { + statusResponse.payload = payload; + return statusResponse; + } else { + throw new ApiError("No payload received from pagespeed monitor", 500); + } + }; + + requestPing = async (monitor: IMonitor) => { + const response = await ping.promise.probe(monitor.url); + const status = response?.alive === true ? "up" : "down"; + + const rawTime = typeof response?.time === "string" ? parseFloat(response.time) : Number(response?.time); + const responseTime = Number.isFinite(rawTime) ? rawTime : 0; + + return { + monitorId: monitor._id.toString(), + type: monitor.type, + status: status as MonitorStatus, + message: "Ping successful", + responseTime, + timings: { phases: {} } as GotTimings, + }; + }; + + requestStatus = async (monitor: IMonitor) => { + switch (monitor?.type) { + case "http": + return await this.requestHttp(monitor); // uses GOT + case "https": + return await this.requestHttp(monitor); // uses GOT + case "infrastructure": + return await this.requestInfrastructure(monitor); // uses GOT + case "pagespeed": + return await this.requestPagespeed(monitor); // uses GOT + case "ping": + return await this.requestPing(monitor); // uses PING + default: + throw new Error("Not implemented"); + } + }; +} + +export default NetworkService; diff --git a/server/src/service/v2/infrastructure/NotificationService.ts b/server/src/service/v2/infrastructure/NotificationService.ts new file mode 100644 index 000000000..4195958bd --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationService.ts @@ -0,0 +1,58 @@ +import UserService from "../business/UserService.js"; +import { IMonitor, NotificationChannel } from "../../../db/v2/models/index.js"; +import { EmailService, SlackService, DiscordService, WebhookService } from "./NotificationServices/index.js"; +export interface INotificationService { + handleNotifications: (monitor: IMonitor) => Promise; +} + +class NotificationService implements INotificationService { + private emailService: EmailService; + private slackService: SlackService; + private discordService: DiscordService; + private webhookService: WebhookService; + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + this.emailService = new EmailService(userService); + this.slackService = new SlackService(); + this.discordService = new DiscordService(); + this.webhookService = new WebhookService(); + } + + handleNotifications = async (monitor: IMonitor) => { + const notificationIds = monitor.notificationChannels || []; + + if (notificationIds.length === 0) { + return; + } + + const notificationChannels = await NotificationChannel.find({ + _id: { $in: notificationIds }, + }); + + for (const channel of notificationChannels) { + // Implement sending logic based on channel.type and channel.config + let service; + switch (channel.type) { + case "email": + await this.emailService.sendMessage(this.emailService.buildAlert(monitor), channel); + break; + case "slack": + await this.slackService.sendMessage(this.slackService.buildAlert(monitor), channel); + break; + case "discord": + await this.discordService.sendMessage(this.discordService.buildAlert(monitor), channel); + break; + case "webhook": + await this.webhookService.sendMessage(this.webhookService.buildAlert(monitor), channel); + break; + default: + console.warn(`Unknown notification channel type: ${channel.type}`); + } + } + return; + }; +} + +export default NotificationService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts new file mode 100644 index 000000000..c9ec30700 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts @@ -0,0 +1,73 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import got from "got"; +import ApiError from "../../../../utils/ApiError.js"; +class DiscordService implements IMessageService { + constructor() {} + + private toDiscordEmbeds = (alert: IAlert) => { + return { + color: alert.status === "up" ? 65280 : 16711680, + title: `Monitor name: ${alert.name}`, + description: `Status: **${alert.status}**`, + fields: [ + { + name: "Url", + value: alert.url, + }, + { + name: "Checked at", + value: alert.checkTime ? alert.checkTime.toISOString() : "N/A", + }, + { name: "Alert time", value: alert.alertTime.toISOString() }, + ...(alert.details + ? Object.entries(alert.details).map(([key, value]) => ({ + name: key, + value, + })) + : []), + ], + }; + }; + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new ApiError("Webhook URL not configured", 400); + } + + try { + const payload = { + content: "Status Alert", + embeds: [this.toDiscordEmbeds(alert)], + }; + await got.post(notificationUrl, { json: payload }); + } catch (error) { + console.warn("Failed to send Discord message", error); + return false; + } + + return true; + }; + + testMessage = async () => { + return true; + }; +} + +export default DiscordService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Email.ts b/server/src/service/v2/infrastructure/NotificationServices/Email.ts new file mode 100644 index 000000000..d1443ce08 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Email.ts @@ -0,0 +1,65 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IMessageService, IAlert } from "./IMessageService.js"; +import nodemailer, { Transporter } from "nodemailer"; +import { config } from "../../../../config/index.js"; +import UserService from "../../business/UserService.js"; +import ApiError from "../../../../utils/ApiError.js"; +class EmailService implements IMessageService { + private transporter: Transporter; + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + this.transporter = nodemailer.createTransport({ + host: config.SMTP_HOST, + port: config.SMTP_PORT, + secure: config.SMTP_PORT === 465, + auth: { + user: config.SMTP_USER, + pass: config.SMTP_PASS, + }, + }); + } + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: string | IAlert, channel: INotificationChannel) => { + try { + const users = await this.userService.getAllUsers(); + const emails = users.map((u) => u.email).join(","); + + if (!emails || emails.length === 0) { + throw new ApiError("No user emails found", 500); + } + + await this.transporter.sendMail({ + from: `"Checkmate" <${config.SMTP_USER}>`, + to: emails, + subject: "Monitor Alert", + text: JSON.stringify(alert, null, 2), + }); + return true; + } catch (error) { + return false; + } + }; + + testMessage = async () => { + return true; + }; +} + +export default EmailService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts new file mode 100644 index 000000000..dbafc59fb --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts @@ -0,0 +1,15 @@ +import { IMonitor, INotificationChannel } from "../../../db/v2/models/index.js"; + +export interface IAlert { + name: string; + url: string; + status: string; + details?: Record; + checkTime: Date | null; + alertTime: Date; +} +export interface IMessageService { + buildAlert: (monitor: IMonitor) => IAlert; + sendMessage: (alert: IAlert, channel: INotificationChannel) => Promise; + testMessage: (message: string, channel: INotificationChannel) => Promise; +} diff --git a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts new file mode 100644 index 000000000..50e2021e4 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts @@ -0,0 +1,116 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import got from "got"; + +class SlackService implements IMessageService { + constructor() {} + + private toSlackBlocks = (alert: IAlert) => { + return [ + { + type: "header", + text: { + type: "plain_text", + text: "Status Alert", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Monitor name:* ${alert.name}`, + }, + }, + + { + type: "section", + text: { + type: "mrkdwn", + text: `*Status:* ${alert.status}`, + }, + }, + + { + type: "section", + text: { + type: "mrkdwn", + text: `*URL:* ${alert.url}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`, + }, + }, + + { + type: "divider", + }, + + ...(alert.details + ? Object.entries(alert.details).map(([key, value]) => ({ + type: "section", + fields: [ + { + type: "mrkdwn", + text: `*${key}:* ${value}`, + }, + ], + })) + : []), + + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `*Alert generated at:* ${alert?.alertTime?.toISOString() || "N/A"}`, + }, + ], + }, + ]; + }; + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new Error("Webhook URL not configured"); + } + + try { + const payload = { + text: "Status Alert", + blocks: this.toSlackBlocks(alert), + }; + await got.post(notificationUrl, { json: payload }); + } catch (error) { + console.warn("Error sending Slack message:", error); + return false; + } + + return true; + }; + + testMessage = async () => { + return true; + }; +} + +export default SlackService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts new file mode 100644 index 000000000..b84b2b220 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts @@ -0,0 +1,43 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import ApiError from "../../../../utils/ApiError.js"; +import got from "got"; +class WebhookService implements IMessageService { + constructor() {} + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new ApiError("Webhook URL not configured", 400); + } + try { + await got.post(notificationUrl, { json: { ...alert } }); + } catch (error) { + console.warn("Failed to send webhook notification:", error); + return false; + } + + return true; + }; + + testMessage = async () => { + return true; + }; +} + +export default WebhookService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/index.ts b/server/src/service/v2/infrastructure/NotificationServices/index.ts new file mode 100644 index 000000000..2b949f716 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/index.ts @@ -0,0 +1,5 @@ +export { default as DiscordService } from "./Discord.js"; +export { default as EmailService } from "./Email.js"; +export { default as SlackService } from "./Slack.js"; +export { default as WebhookService } from "./Webhook.js"; +export * from "./IMessageService.js"; diff --git a/server/src/service/v2/infrastructure/StatusService.ts b/server/src/service/v2/infrastructure/StatusService.ts new file mode 100644 index 000000000..87b0df0af --- /dev/null +++ b/server/src/service/v2/infrastructure/StatusService.ts @@ -0,0 +1,100 @@ +import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/index.js"; +import { StatusResponse } from "./NetworkService.js"; +import ApiError from "../../../utils/ApiError.js"; + +const MAX_LATEST_CHECKS = 25; +export interface IStatusService { + updateMonitorStatus: (monitor: IMonitor, status: StatusResponse) => Promise; + + calculateAvgResponseTime: (stats: IMonitorStats, statusResponse: StatusResponse) => number; + + updateMonitorStats: (monitor: IMonitor, status: StatusResponse, statusChanged: boolean) => Promise; +} + +export type StatusChangeResult = [updatedMonitor: IMonitor, statusChanged: boolean]; + +class StatusService implements IStatusService { + updateMonitorStatus = async (monitor: IMonitor, statusResponse: StatusResponse): Promise => { + const newStatus = statusResponse.status; + monitor.lastCheckedAt = new Date(); + + // Store latest checks for display + monitor.latestChecks = monitor.latestChecks || []; + monitor.latestChecks.push({ + status: newStatus, + responseTime: statusResponse.responseTime, + checkedAt: monitor.lastCheckedAt, + }); + while (monitor.latestChecks.length > MAX_LATEST_CHECKS) { + monitor.latestChecks.shift(); + } + + // Update monitor status + if (monitor.status === "initializing") { + monitor.status = newStatus; + return [await monitor.save(), true]; + } else { + const { n } = monitor; + const latestChecks = monitor.latestChecks.slice(-n); + // Return early if not enough statuses to evaluate + if (latestChecks.length < n) { + return [await monitor.save(), false]; + } + + // If all different than current status, update status + const allDifferent = latestChecks.every((check) => check.status !== monitor.status); + if (allDifferent && monitor.status !== newStatus) { + monitor.status = newStatus; + } + return [await monitor.save(), allDifferent]; + } + }; + + calculateAvgResponseTime = (stats: IMonitorStats, statusResponse: StatusResponse): number => { + let avgResponseTime = stats.avgResponseTime; + // Set initial + if (avgResponseTime === 0) { + avgResponseTime = statusResponse.responseTime; + } else { + avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + statusResponse.responseTime) / stats.totalChecks; + } + return avgResponseTime; + }; + + updateMonitorStats = async (monitor: IMonitor, statusResponse: StatusResponse, statusChanged: boolean) => { + const stats = await MonitorStats.findOne({ monitorId: monitor._id }); + if (!stats) { + throw new ApiError("MonitorStats not found", 500); + } + + // Update check counts + stats.totalChecks += 1; + stats.totalUpChecks += statusResponse.status === "up" ? 1 : 0; + stats.totalDownChecks += statusResponse.status === "down" ? 1 : 0; + + // Update streak + if (!statusChanged) { + stats.currentStreak += 1; + } else { + stats.currentStreak = 1; + stats.currentStreakStatus = statusResponse.status; + stats.currentStreakStartedAt = Date.now(); + } + + // Update time stamps + stats.lastCheckTimestamp = Date.now(); + stats.timeOfLastFailure = statusResponse.status === "down" ? Date.now() : stats.timeOfLastFailure; + + // Update stats that need updated check counts + stats.avgResponseTime = this.calculateAvgResponseTime(stats, statusResponse); + stats.uptimePercentage = stats.totalUpChecks / stats.totalChecks; + + // Other + stats.lastResponseTime = statusResponse.responseTime; + stats.maxResponseTime = Math.max(stats.maxResponseTime, statusResponse.responseTime); + + return await stats.save(); + }; +} + +export default StatusService; diff --git a/server/src/utils/ApiError.ts b/server/src/utils/ApiError.ts new file mode 100644 index 000000000..313d01c2a --- /dev/null +++ b/server/src/utils/ApiError.ts @@ -0,0 +1,15 @@ +class ApiError extends Error { + public status: number; + + constructor(message: string, status: number = 500) { + super(message); + this.status = status; + this.name = this.constructor.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export default ApiError; diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts new file mode 100644 index 000000000..4eebb78a0 --- /dev/null +++ b/server/src/utils/JWTUtils.ts @@ -0,0 +1,22 @@ +import jwt from "jsonwebtoken"; +import { AuthResult } from "../service/v2/business/AuthService.js"; + +const encode = (data: AuthResult): string => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const token = jwt.sign(data, secret, { expiresIn: "99d" }); + return token; +}; + +const decode = (token: string): AuthResult => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const decoded = jwt.verify(token, secret) as AuthResult; + return decoded; +}; + +export { encode, decode };