diff --git a/.gitignore b/.gitignore index 4b56acf..368425b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ /node_modules /build +package-lock.json + # Logs logs *.log diff --git a/package-lock.json b/package-lock.json index dc76e24..244808e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-cloudfront": "^3.975.0", + "@aws-sdk/client-kms": "^3.978.0", "@aws-sdk/client-s3": "^3.975.0", "@elastic/elasticsearch": "^8.19.1", "@langchain/community": "^0.3.55", @@ -59,7 +60,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdf-parse": "^1.1.1", - "pg": "^8.16.0", + "pg": "^8.17.2", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -315,7 +316,6 @@ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.27.3.tgz", "integrity": "sha512-IjLt0gd3L4jlOfilxVXTifn42FnVffMgDC04RJK1KDZpmkBWLv0XC92MVVmkxrFZNS/7l3xWgP/I3nqtX1sQHw==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -331,7 +331,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -340,8 +339,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", @@ -683,10 +681,557 @@ "integrity": "sha512-dLZVNhM7wSgVUFsgVYgI5hb5Z/9PUkT46pk/SHrSmUqfx6YDvoV4YcPtaiRqviPpEGGiRtdQMEadyOKIRqulUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.974.0", - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/token-providers": "3.974.0", - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/client-sso": "3.974.0", + "@aws-sdk/core": "^3.973.0", + "@aws-sdk/token-providers": "3.974.0", + "@aws-sdk/types": "^3.973.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.1.tgz", + "integrity": "sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.0", + "@aws-sdk/nested-clients": "3.974.0", + "@aws-sdk/types": "^3.973.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.1.tgz", + "integrity": "sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.1.tgz", + "integrity": "sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.0", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.1.tgz", + "integrity": "sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.2.tgz", + "integrity": "sha512-d+Exq074wy0X6wvShg/kmZVtkah+28vMuqCtuY3cydg8LUZOJBtbAolCpEJizSyb8mJJZF9BjWaTANXL4OYnkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.1", + "@aws-sdk/types": "^3.973.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@smithy/core": "^3.21.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/nested-clients": { + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.974.0.tgz", + "integrity": "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.0", + "@aws-sdk/middleware-host-header": "^3.972.1", + "@aws-sdk/middleware-logger": "^3.972.1", + "@aws-sdk/middleware-recursion-detection": "^3.972.1", + "@aws-sdk/middleware-user-agent": "^3.972.1", + "@aws-sdk/region-config-resolver": "^3.972.1", + "@aws-sdk/types": "^3.973.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "^3.972.1", + "@aws-sdk/util-user-agent-node": "^3.972.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.21.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.10", + "@smithy/middleware-retry": "^4.4.26", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.11", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.25", + "@smithy/util-defaults-mode-node": "^4.2.28", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.1.tgz", + "integrity": "sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/token-providers": { + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.974.0.tgz", + "integrity": "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.0", + "@aws-sdk/nested-clients": "3.974.0", + "@aws-sdk/types": "^3.973.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/types": { + "version": "3.973.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.0.tgz", + "integrity": "sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.1.tgz", + "integrity": "sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.0", + "@smithy/types": "^4.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.1.tgz", + "integrity": "sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.1", + "@aws-sdk/types": "^3.973.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.1.tgz", + "integrity": "sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/protocol-http": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/signature-v4": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-kms": { + "version": "3.978.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.978.0.tgz", + "integrity": "sha512-KTCHctiTS4RJ/MeJrI/hbm5F0AhsBayjnzzfYlGAasNIGW3ucwCc2qTZ2QzKHaR3zPyysSQiTDzNUg/RmdfExA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.4", + "@aws-sdk/credential-provider-node": "^3.972.2", + "@aws-sdk/middleware-host-header": "^3.972.2", + "@aws-sdk/middleware-logger": "^3.972.2", + "@aws-sdk/middleware-recursion-detection": "^3.972.2", + "@aws-sdk/middleware-user-agent": "^3.972.4", + "@aws-sdk/region-config-resolver": "^3.972.2", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "^3.972.2", + "@aws-sdk/util-user-agent-node": "^3.972.2", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/client-sso": { + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.975.0.tgz", + "integrity": "sha512-HpgJuleH7P6uILxzJKQOmlHdwaCY+xYC6VgRDzlwVEqU/HXjo4m2gOAyjUbpXlBOCWfGgMUzfBlNJ9z3MboqEQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.1", + "@aws-sdk/middleware-host-header": "^3.972.1", + "@aws-sdk/middleware-logger": "^3.972.1", + "@aws-sdk/middleware-recursion-detection": "^3.972.1", + "@aws-sdk/middleware-user-agent": "^3.972.2", + "@aws-sdk/region-config-resolver": "^3.972.1", + "@aws-sdk/types": "^3.973.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "^3.972.1", + "@aws-sdk/util-user-agent-node": "^3.972.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.21.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.11", + "@smithy/middleware-retry": "^4.4.27", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.12", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.26", + "@smithy/util-defaults-mode-node": "^4.2.29", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/core": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.4.tgz", + "integrity": "sha512-8Rk+kPP74YiR47x54bxYlKZswsaSh0a4XvvRUMLvyS/koNawhsGu/+qSZxREqUeTO+GkKpFvSQIsAZR+deUP+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.2", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.2.tgz", + "integrity": "sha512-wzH1EdrZsytG1xN9UHaK12J9+kfrnd2+c8y0LVoS4O4laEjPoie1qVK3k8/rZe7KOtvULzyMnO3FT4Krr9Z0Dg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.2", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.4.tgz", + "integrity": "sha512-OC7F3ipXV12QfDEWybQGHLzoeHBlAdx/nLzPfHP0Wsabu3JBffu5nlzSaJNf7to9HGtOW8Bpu8NX0ugmDrCbtw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.4", + "@aws-sdk/types": "^3.973.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.2.tgz", + "integrity": "sha512-Jrb8sLm6k8+L7520irBrvCtdLxNtrG7arIxe9TCeMJt/HxqMGJdbIjw8wILzkEHLMIi4MecF2FbXCln7OT1Tag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.2", + "@aws-sdk/credential-provider-env": "^3.972.2", + "@aws-sdk/credential-provider-http": "^3.972.3", + "@aws-sdk/credential-provider-login": "^3.972.2", + "@aws-sdk/credential-provider-process": "^3.972.2", + "@aws-sdk/credential-provider-sso": "^3.972.2", + "@aws-sdk/credential-provider-web-identity": "^3.972.2", + "@aws-sdk/nested-clients": "3.975.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.2.tgz", + "integrity": "sha512-mlaw2aiI3DrimW85ZMn3g7qrtHueidS58IGytZ+mbFpsYLK5wMjCAKZQtt7VatLMtSBG/dn/EY4njbnYXIDKeQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.2", + "@aws-sdk/nested-clients": "3.975.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.3.tgz", + "integrity": "sha512-iu+JwWHM7tHowKqE+8wNmI3sM6mPEiI9Egscz2BEV7adyKmV95oR9tBO4VIOl72FGDi7X9mXg19VtqIpSkEEsA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.2", + "@aws-sdk/credential-provider-http": "^3.972.4", + "@aws-sdk/credential-provider-ini": "^3.972.2", + "@aws-sdk/credential-provider-process": "^3.972.2", + "@aws-sdk/credential-provider-sso": "^3.972.2", + "@aws-sdk/credential-provider-web-identity": "^3.972.2", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.2.tgz", + "integrity": "sha512-NLKLTT7jnUe9GpQAVkPTJO+cs2FjlQDt5fArIYS7h/Iw/CvamzgGYGFRVD2SE05nOHCMwafUSi42If8esGFV+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.2", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.2.tgz", + "integrity": "sha512-YpwDn8g3gCGUl61cCV0sRxP2pFIwg+ZsMfWQ/GalSyjXtRkctCMFA+u0yPb/Q4uTfNEiya1Y4nm0C5rIHyPW5Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.975.0", + "@aws-sdk/core": "^3.973.2", + "@aws-sdk/token-providers": "3.975.0", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", @@ -696,15 +1241,15 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.1.tgz", - "integrity": "sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.2.tgz", + "integrity": "sha512-x9DAiN9Qz+NjJ99ltDiVQ8d511M/tuF/9MFbe2jUgo7HZhD6+x4S3iT1YcP07ndwDUjmzKGmeOEgE24k4qvfdg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/nested-clients": "3.974.0", - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/core": "^3.973.2", + "@aws-sdk/nested-clients": "3.975.0", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", @@ -714,13 +1259,13 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.1.tgz", - "integrity": "sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.2.tgz", + "integrity": "sha512-42hZ8jEXT2uR6YybCzNq9OomqHPw43YIfRfz17biZjMQA4jKSQUaHIl6VvqO2Ddl5904pXg2Yd/ku78S0Ikgog==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -729,13 +1274,13 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.1.tgz", - "integrity": "sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.2.tgz", + "integrity": "sha512-iUzdXKOgi4JVDDEG/VvoNw50FryRCEm0qAudw12DcZoiNJWl0rN6SYVLcL1xwugMfQncCXieK5UBlG6mhH7iYA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -743,13 +1288,13 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.1.tgz", - "integrity": "sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.2.tgz", + "integrity": "sha512-/mzlyzJDtngNFd/rAYvqx29a2d0VuiYKN84Y/Mu9mGw7cfMOCyRK+896tb9wV6MoPRHUX7IXuKCIL8nzz2Pz5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", @@ -759,16 +1304,16 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.2.tgz", - "integrity": "sha512-d+Exq074wy0X6wvShg/kmZVtkah+28vMuqCtuY3cydg8LUZOJBtbAolCpEJizSyb8mJJZF9BjWaTANXL4OYnkg==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.4.tgz", + "integrity": "sha512-6sU8jrSJvY/lqSnU6IYsa8SrCKwOZ4Enl6O4xVJo8RCq9Bdr5Giuw2eUaJAk9GPcpr4OFcmSFv3JOLhpKGeRZA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.1", - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/core": "^3.973.4", + "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.972.0", - "@smithy/core": "^3.21.1", + "@smithy/core": "^3.22.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -777,45 +1322,45 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/nested-clients": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.974.0.tgz", - "integrity": "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/nested-clients": { + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.975.0.tgz", + "integrity": "sha512-OkeFHPlQj2c/Y5bQGkX14pxhDWUGUFt3LRHhjcDKsSCw6lrxKcxN3WFZN0qbJwKNydP+knL5nxvfgKiCLpTLRA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.0", + "@aws-sdk/core": "^3.973.1", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", - "@aws-sdk/middleware-user-agent": "^3.972.1", + "@aws-sdk/middleware-user-agent": "^3.972.2", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.21.0", + "@smithy/core": "^3.21.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.10", - "@smithy/middleware-retry": "^4.4.26", + "@smithy/middleware-endpoint": "^4.4.11", + "@smithy/middleware-retry": "^4.4.27", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.11", + "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.25", - "@smithy/util-defaults-mode-node": "^4.2.28", + "@smithy/util-defaults-mode-browser": "^4.3.26", + "@smithy/util-defaults-mode-node": "^4.2.29", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -826,13 +1371,13 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.1.tgz", - "integrity": "sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.2.tgz", + "integrity": "sha512-/7vRBsfmiOlg2X67EdKrzzQGw5/SbkXb7ALHQmlQLkZh8qNgvS2G2dDC6NtF3hzFlpP3j2k+KIEtql/6VrI6JA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", @@ -842,14 +1387,14 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/token-providers": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.974.0.tgz", - "integrity": "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/token-providers": { + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.975.0.tgz", + "integrity": "sha512-AWQt64hkVbDQ+CmM09wnvSk2mVyH4iRROkmYkr3/lmUtFNbE2L/fnw26sckZnUcFCsHPqbkQrcsZAnTcBLbH4w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/nested-clients": "3.974.0", + "@aws-sdk/core": "^3.973.1", + "@aws-sdk/nested-clients": "3.975.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -860,10 +1405,10 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/types": { - "version": "3.973.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.0.tgz", - "integrity": "sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/types": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -873,26 +1418,26 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.1.tgz", - "integrity": "sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.2.tgz", + "integrity": "sha512-gz76bUyebPZRxIsBHJUd/v+yiyFzm9adHbr8NykP2nm+z/rFyvQneOHajrUejtmnc5tTBeaDPL4X25TnagRk4A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.1.tgz", - "integrity": "sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.2.tgz", + "integrity": "sha512-vnxOc4C6AR7hVbwyFo1YuH0GB6dgJlWt8nIOOJpnzJAWJPkUMPJ9Zv2lnKsSU7TTZbhP2hEO8OZ4PYH59XFv8Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.1", - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/middleware-user-agent": "^3.972.3", + "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -909,10 +1454,10 @@ } } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.1.tgz", - "integrity": "sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg==", + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.2.tgz", + "integrity": "sha512-jGOOV/bV1DhkkUhHiZ3/1GZ67cZyOXaDb7d1rYD6ZiXf5V9tBNOcgqXwRRPvrCbYaFRa1pPMFb3ZjqjWpR3YfA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -923,7 +1468,7 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/is-array-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", @@ -935,7 +1480,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/protocol-http": { + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/protocol-http": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", @@ -948,7 +1493,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/signature-v4": { + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/signature-v4": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", @@ -967,7 +1512,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudfront/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-utf8": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", @@ -1938,6 +2483,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.0.tgz", "integrity": "sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/credential-provider-env": "3.972.0", "@aws-sdk/credential-provider-http": "3.972.0", @@ -2756,7 +3302,6 @@ "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.3.1" } @@ -2815,6 +3360,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3305,7 +3851,6 @@ "resolved": "https://registry.npmjs.org/@browserbasehq/sdk/-/sdk-2.6.0.tgz", "integrity": "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -3321,7 +3866,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3330,8 +3874,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@browserbasehq/stagehand": { "version": "1.14.0", @@ -3357,8 +3900,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@colors/colors": { "version": "1.5.0", @@ -3400,6 +3942,7 @@ "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.19.1.tgz", "integrity": "sha512-+1j9NnQVOX+lbWB8LhCM7IkUmjU05Y4+BmSLfusq0msCsQb1Va+OUKFCoOXjCJqQrcgdRdQCjYYyolQ/npQALQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@elastic/transport": "^8.9.6", "apache-arrow": "18.x - 21.x", @@ -3656,7 +4199,6 @@ "resolved": "https://registry.npmjs.org/@ibm-cloud/watsonx-ai/-/watsonx-ai-1.6.13.tgz", "integrity": "sha512-INaaD7EKpycwQg/tsLm3QM5uvDF5mWLPQCj6GTk44gEZhgx1depvVG5bxwjfqkx1tbJMFuozz2p6VHOE21S+8g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/node": "^18.0.0", "extend": "3.0.2", @@ -3672,7 +4214,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3681,8 +4222,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.3", @@ -5339,7 +5879,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -5352,7 +5891,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -5369,7 +5907,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -5668,6 +6205,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.19.tgz", "integrity": "sha512-0TZJ8H+7qtaqZt6YfZJkDRp0e+v6jjo5/pevPAjUy0WYxaTy16bNNQxFPRKLMe/v1hUr2oGV9imvL2477zNt5g==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", @@ -5714,6 +6252,7 @@ "integrity": "sha512-gahghu0y4Rn4gn/xPjTgNHFMpUM8TxfhdeMowVWTGVnYMZtGeEGbIXMFhJS0Dce3E4VKyqAglzgO9ecAZd4Ong==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -5827,6 +6366,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.19.tgz", "integrity": "sha512-IeQkBZUtPeJoO4E0QqSLwkB+60KcThw8/s4gGvAwIRJ5ViuXoxnwU59eBDy84PUuVbNe4VdKjfAF9fuQOEh11Q==", "license": "MIT", + "peer": true, "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", @@ -5959,6 +6499,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.19.tgz", "integrity": "sha512-3HhNZU40/ozB64ZZJ1W1bOOYSbxGuJwmM5Ui8y1uPwbzpL1Uov3wOG3eRp1IflSBK4Ia0wb8Iv3ChpFSTzrNiA==", "license": "MIT", + "peer": true, "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -6051,6 +6592,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -6666,6 +7208,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -6821,9 +7364,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.21.1.tgz", - "integrity": "sha512-NUH8R4O6FkN8HKMojzbGg/5pNjsfTjlMmeFclyPfPaXXUrbr5TzhWgbf7t92wfrpCHRgpjyz7ffASIS3wX28aA==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.0.tgz", + "integrity": "sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -6889,7 +7432,6 @@ "integrity": "sha512-8janZoJw85nJmQZc4L8TuePp2pk1nxLgkxIR0TUjKJ5Dkj5oelB9WtiSSGXCQvNsJl0VSTvK/2ueMXxvpa9GVw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@aws-crypto/crc32": "3.0.0", "@smithy/types": "^2.12.0", @@ -6903,7 +7445,6 @@ "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", @@ -6915,8 +7456,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD", - "optional": true, - "peer": true + "optional": true }, "node_modules/@smithy/eventstream-codec/node_modules/@aws-crypto/util": { "version": "3.0.0", @@ -6924,7 +7464,6 @@ "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", @@ -6936,8 +7475,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD", - "optional": true, - "peer": true + "optional": true }, "node_modules/@smithy/eventstream-codec/node_modules/@smithy/types": { "version": "2.12.0", @@ -6945,7 +7483,6 @@ "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -6959,7 +7496,6 @@ "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -7216,12 +7752,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.11.tgz", - "integrity": "sha512-/WqsrycweGGfb9sSzME4CrsuayjJF6BueBmkKlcbeU5q18OhxRrvvKlmfw3tpDsK5ilx2XUJvoukwxHB0nHs/Q==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.12.tgz", + "integrity": "sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.21.1", + "@smithy/core": "^3.22.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -7235,15 +7771,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.27", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.27.tgz", - "integrity": "sha512-xFUYCGRVsfgiN5EjsJJSzih9+yjStgMTCLANPlf0LVQkPDYCe0hz97qbdTZosFOiYlGBlHYityGRxrQ/hxhfVQ==", + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.29.tgz", + "integrity": "sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.10.12", + "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -7370,7 +7906,6 @@ "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" @@ -7385,7 +7920,6 @@ "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -7451,7 +7985,6 @@ "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "@smithy/types": "^2.12.0", @@ -7471,7 +8004,6 @@ "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -7485,7 +8017,6 @@ "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -7499,7 +8030,6 @@ "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" @@ -7514,7 +8044,6 @@ "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -7523,13 +8052,13 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.12", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.12.tgz", - "integrity": "sha512-VKO/HKoQ5OrSHW6AJUmEnUKeXI1/5LfCwO9cwyao7CmLvGnZeM1i36Lyful3LK1XU7HwTVieTqO1y2C/6t3qtA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.1.tgz", + "integrity": "sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.21.1", - "@smithy/middleware-endpoint": "^4.4.11", + "@smithy/core": "^3.22.0", + "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", @@ -7668,13 +8197,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.26.tgz", - "integrity": "sha512-vva0dzYUTgn7DdE0uaha10uEdAgmdLnNFowKFjpMm6p2R0XDk5FHPX3CBJLzWQkQXuEprsb0hGz9YwbicNWhjw==", + "version": "4.3.28", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.28.tgz", + "integrity": "sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.12", + "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -7683,16 +8212,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.29", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.29.tgz", - "integrity": "sha512-c6D7IUBsZt/aNnTBHMTf+OVh+h/JcxUUgfTcIJaWRe6zhOum1X+pNKSZtZ+7fbOn5I99XVFtmrnXKv8yHHErTQ==", + "version": "4.2.31", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.31.tgz", + "integrity": "sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.12", + "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -8037,7 +8566,6 @@ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/ms": "*" } @@ -8048,6 +8576,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -8237,6 +8766,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.4.tgz", "integrity": "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -8246,7 +8776,6 @@ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -8413,8 +8942,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/uuid": { "version": "10.0.0", @@ -8491,6 +9019,7 @@ "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.1", "@typescript-eslint/types": "8.35.1", @@ -8913,7 +9442,6 @@ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", - "peer": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -8945,6 +9473,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8989,7 +9518,6 @@ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", - "peer": true, "dependencies": { "humanize-ms": "^1.2.1" }, @@ -9003,6 +9531,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -9306,6 +9835,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -9723,6 +10253,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -9787,6 +10318,7 @@ "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", "license": "MIT", + "peer": true, "dependencies": { "cron-parser": "^4.9.0", "get-port": "^5.1.1", @@ -9834,6 +10366,7 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.0.1.tgz", "integrity": "sha512-Hd7FOyTtwhwLgkKeKQWEw6Ixj63VKuUWYwkGgL6g6Q7eISW6uxci5+DtUXlqI0gtbLCPPdhL1+HP9Zht27DbrA==", "license": "MIT", + "peer": true, "dependencies": { "keyv": "^5.3.4" } @@ -10053,13 +10586,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -10560,7 +11095,6 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10615,6 +11149,7 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10754,6 +11289,7 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=12" }, @@ -11058,6 +11594,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -11114,6 +11651,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -11311,7 +11849,6 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -11414,6 +11951,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -11480,8 +12018,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/external-editor": { "version": "3.1.0", @@ -11978,15 +12515,13 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", "license": "MIT", - "peer": true, "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" @@ -12283,6 +12818,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -12453,7 +12989,6 @@ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.0.0" } @@ -12463,7 +12998,6 @@ "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.4.3.tgz", "integrity": "sha512-D0lvClcoCp/HXyaFlCbOT4aTYgGyeIb4ncxZpxRuiuw7Eo79C6c49W53+8WJRD9nxzT5vrIdaky3NBcTdBtaEg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/debug": "^4.1.12", "@types/node": "^18.19.80", @@ -12490,7 +13024,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -12500,7 +13033,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12513,7 +13045,6 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "license": "MIT", - "peer": true, "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", @@ -12544,15 +13075,13 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ibm-cloud-sdk-core/node_modules/strtok3": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", "license": "MIT", - "peer": true, "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" @@ -12570,7 +13099,6 @@ "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", "license": "MIT", - "peer": true, "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -12587,8 +13115,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/iconv-lite": { "version": "0.4.24", @@ -12614,6 +13141,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -12733,6 +13261,7 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", "license": "MIT", + "peer": true, "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", @@ -12989,8 +13518,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -13146,6 +13674,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14038,6 +14567,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.0.3" } @@ -14275,7 +14805,8 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -14704,7 +15235,6 @@ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "license": "MIT", - "peer": true, "bin": { "mustache": "bin/mustache" } @@ -14815,7 +15345,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=10.5.0" } @@ -15119,7 +15648,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -15128,8 +15656,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/openapi-types": { "version": "12.1.3", @@ -15333,6 +15860,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -15447,6 +15975,7 @@ "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^3.1.0", "node-ensure": "^0.0.0" @@ -15469,7 +15998,6 @@ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -15479,14 +16007,15 @@ } }, "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", + "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", + "peer": true, "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", + "pg-connection-string": "^2.10.1", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -15494,7 +16023,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.7" + "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -15506,16 +16035,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", "license": "MIT" }, "node_modules/pg-int8": { @@ -15528,18 +16057,18 @@ } }, "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", "license": "MIT" }, "node_modules/pg-types": { @@ -15677,7 +16206,6 @@ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright-core": "1.55.1" }, @@ -15696,7 +16224,6 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -15714,7 +16241,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -15847,6 +16373,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -15903,7 +16430,6 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6.0" } @@ -15983,7 +16509,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "license": "MIT", - "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -16055,8 +16580,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -16163,7 +16687,6 @@ "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", "license": "MIT", - "peer": true, "dependencies": { "readable-stream": "^4.7.0" }, @@ -16194,7 +16717,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -16205,7 +16727,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -16228,15 +16749,13 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", - "peer": true, "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -16279,6 +16798,7 @@ "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", "license": "MIT", + "peer": true, "workspaces": [ "./packages/*" ], @@ -16365,8 +16885,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.10", @@ -16466,7 +16985,6 @@ "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=10.7.0" }, @@ -17662,6 +18180,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17939,7 +18458,6 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -17955,7 +18473,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -18088,6 +18605,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -18259,6 +18777,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -18410,6 +18929,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18554,7 +19074,6 @@ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "license": "MIT", - "peer": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -18718,7 +19237,6 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "license": "MIT", - "peer": true, "engines": { "node": ">= 14" } @@ -18735,6 +19253,7 @@ "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -18966,7 +19485,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -19097,6 +19615,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19106,7 +19625,6 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "license": "ISC", - "peer": true, "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index 68601af..ae166d5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@aws-sdk/client-cloudfront": "^3.975.0", + "@aws-sdk/client-kms": "^3.978.0", "@aws-sdk/client-s3": "^3.975.0", "@elastic/elasticsearch": "^8.19.1", "@langchain/community": "^0.3.55", @@ -73,7 +74,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdf-parse": "^1.1.1", - "pg": "^8.16.0", + "pg": "^8.17.2", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/src/app.module.ts b/src/app.module.ts index 4f3a63e..bbd26b8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,8 @@ import { MonitoringInterceptor } from './common/interceptors/monitoring.intercep import { TypeOrmMonitoringLogger } from './monitoring/logging/typeorm-logger'; import { MetricsCollectionService } from './monitoring/metrics/metrics-collection.service'; import { SyncModule } from './sync/sync.module'; +import { MediaModule } from './media/media.module'; +import { BackupModule } from './backup/backup.module'; import { BullModule } from '@nestjs/bull'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { CacheModule } from '@nestjs/cache-manager'; @@ -32,7 +34,7 @@ import * as redisStore from 'cache-manager-redis-store'; username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', database: process.env.DB_DATABASE || 'teachlink', - entities: [], + autoLoadEntities: true, synchronize: process.env.NODE_ENV !== 'production', logging: true, logger: new TypeOrmMonitoringLogger(metricsService), @@ -54,6 +56,8 @@ import * as redisStore from 'cache-manager-redis-store'; port: parseInt(process.env.REDIS_PORT || '6379'), }), SyncModule, + MediaModule, + BackupModule, ], controllers: [AppController], providers: [ diff --git a/src/backup/backup.controller.ts b/src/backup/backup.controller.ts new file mode 100644 index 0000000..8437cf8 --- /dev/null +++ b/src/backup/backup.controller.ts @@ -0,0 +1,61 @@ +import { + Controller, + Post, + Get, + Param, + Body, + UseGuards, + ParseUUIDPipe, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { RecoveryTestingService } from './testing/recovery-testing.service'; +import { DisasterRecoveryService } from './disaster-recovery/disaster-recovery.service'; +import { BackupMonitoringService } from './monitoring/backup-monitoring.service'; +import { RestoreBackupDto } from './dto/restore-backup.dto'; +import { TriggerRecoveryTestDto } from './dto/trigger-recovery-test.dto'; +import { RecoveryTestResponseDto } from './dto/recovery-test-response.dto'; + +@ApiTags('backup') +@ApiBearerAuth() +@Controller('backup') +export class BackupController { + constructor( + private readonly recoveryTestingService: RecoveryTestingService, + private readonly disasterRecoveryService: DisasterRecoveryService, + private readonly backupMonitoringService: BackupMonitoringService, + ) {} + + @Post('restore') + @ApiOperation({ summary: 'Restore from backup' }) + @HttpCode(HttpStatus.ACCEPTED) + async restoreBackup( + @Body() dto: RestoreBackupDto, + ): Promise<{ message: string }> { + await this.disasterRecoveryService.executeRestore(dto.backupRecordId); + return { message: 'Restore initiated' }; + } + + @Post('test') + @ApiOperation({ summary: 'Trigger recovery test' }) + async triggerRecoveryTest( + @Body() dto: TriggerRecoveryTestDto, + ): Promise { + return this.recoveryTestingService.createRecoveryTest(dto.backupRecordId); + } + + @Get('test/:testId') + @ApiOperation({ summary: 'Get recovery test results' }) + async getRecoveryTest( + @Param('testId', ParseUUIDPipe) testId: string, + ): Promise { + return this.recoveryTestingService.getTestResults(testId); + } + + @Get('health') + @ApiOperation({ summary: 'Get backup system health' }) + async getBackupHealth(): Promise<{ healthy: boolean; issues: string[] }> { + return this.backupMonitoringService.checkBackupHealth(); + } +} diff --git a/src/backup/backup.module.ts b/src/backup/backup.module.ts new file mode 100644 index 0000000..602141f --- /dev/null +++ b/src/backup/backup.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bull'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; + +// Entities +import { BackupRecord } from './entities/backup-record.entity'; +import { RecoveryTest } from './entities/recovery-test.entity'; + +// Services +import { BackupService } from './backup.service'; +import { DisasterRecoveryService } from './disaster-recovery/disaster-recovery.service'; +import { DataIntegrityService } from './integrity/data-integrity.service'; +import { RecoveryTestingService } from './testing/recovery-testing.service'; +import { BackupMonitoringService } from './monitoring/backup-monitoring.service'; + +// Controller +import { BackupController } from './backup.controller'; + +// Processor +import { BackupQueueProcessor } from './processing/backup-queue.processor'; + +// External modules +import { MediaModule } from '../media/media.module'; +import { MonitoringModule } from '../monitoring/monitoring.module'; + +@Module({ + imports: [ + ConfigModule, + ScheduleModule.forRoot(), + TypeOrmModule.forFeature([BackupRecord, RecoveryTest]), + BullModule.registerQueue({ + name: 'backup-processing', + }), + MediaModule, // For FileStorageService + MonitoringModule, // For AlertingService and MetricsCollectionService + ], + controllers: [BackupController], + providers: [ + BackupService, + DisasterRecoveryService, + DataIntegrityService, + RecoveryTestingService, + BackupMonitoringService, + BackupQueueProcessor, + ], + exports: [BackupService, DisasterRecoveryService], +}) +export class BackupModule {} diff --git a/src/backup/backup.service.ts b/src/backup/backup.service.ts new file mode 100644 index 0000000..a377f33 --- /dev/null +++ b/src/backup/backup.service.ts @@ -0,0 +1,196 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, LessThan } from 'typeorm'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; +import { ConfigService } from '@nestjs/config'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { BackupRecord } from './entities/backup-record.entity'; +import { BackupStatus } from './enums/backup-status.enum'; +import { BackupType } from './enums/backup-type.enum'; +import { Region } from './enums/region.enum'; +import { BackupResponseDto } from './dto/backup-response.dto'; +import { BackupJobData } from './interfaces/backup.interfaces'; +import { AlertingService } from '../monitoring/alerting/alerting.service'; +import { MetricsCollectionService } from '../monitoring/metrics/metrics-collection.service'; + +@Injectable() +export class BackupService { + private readonly logger = new Logger(BackupService.name); + private readonly retentionDays: number; + + constructor( + @InjectRepository(BackupRecord) + private readonly backupRepository: Repository, + @InjectQueue('backup-processing') + private readonly backupQueue: Queue, + private readonly configService: ConfigService, + private readonly alertingService: AlertingService, + private readonly metricsService: MetricsCollectionService, + ) { + this.retentionDays = this.configService.get( + 'BACKUP_RETENTION_DAYS', + 30, + ); + } + + /** + * Scheduled weekly backup (every Sunday at 2 AM UTC) + */ + @Cron('0 2 * * 0', { + name: 'weekly-database-backup', + timeZone: 'UTC', + }) + async handleScheduledBackup(): Promise { + this.logger.log('Starting scheduled weekly backup'); + + try { + const region = + (this.configService.get( + 'BACKUP_PRIMARY_REGION', + ) as Region) || Region.US_EAST_1; + const databaseName = this.configService.get( + 'DB_DATABASE', + 'teachlink', + ); + + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + this.retentionDays); + + const backupRecord = this.backupRepository.create({ + backupType: BackupType.FULL, + status: BackupStatus.PENDING, + region, + databaseName, + storageKey: '', + expiresAt, + metadata: { + startTime: new Date(), + }, + }); + + await this.backupRepository.save(backupRecord); + + // Queue backup job + await this.backupQueue.add( + 'create-backup', + { + backupRecordId: backupRecord.id, + backupType: BackupType.FULL, + region, + databaseName, + } as BackupJobData, + { + attempts: 3, + backoff: { + type: 'exponential', + delay: 10000, + }, + timeout: 3600000, // 1 hour timeout + }, + ); + + this.logger.log(`Scheduled backup ${backupRecord.id} queued`); + } catch (error) { + this.logger.error('Failed to initiate scheduled backup:', error); + this.alertingService.sendAlert( + 'BACKUP_SCHEDULED_FAILED', + `Scheduled backup failed: ${error.message}`, + 'CRITICAL', + ); + } + } + + /** + * Cleanup expired backups (daily at 3 AM UTC) + */ + @Cron('0 3 * * *', { + name: 'cleanup-expired-backups', + timeZone: 'UTC', + }) + async handleBackupCleanup(): Promise { + this.logger.log('Starting backup cleanup job'); + + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() - this.retentionDays); + + const expiredBackups = await this.backupRepository.find({ + where: { + createdAt: LessThan(expirationDate), + status: BackupStatus.COMPLETED, + }, + }); + + this.logger.log( + `Found ${expiredBackups.length} expired backups to cleanup`, + ); + + for (const backup of expiredBackups) { + await this.backupQueue.add( + 'delete-backup', + { backupRecordId: backup.id }, + { + attempts: 3, + backoff: { type: 'exponential', delay: 5000 }, + }, + ); + } + } + + async getLatestBackup(region?: Region): Promise { + const where: any = { + status: BackupStatus.COMPLETED, + integrityVerified: true, + }; + if (region) { + where.region = region; + } + + return this.backupRepository.findOne({ + where, + order: { completedAt: 'DESC' }, + }); + } + + async updateBackupStatus( + backupId: string, + status: BackupStatus, + updates: Partial = {}, + ): Promise { + await this.backupRepository.update(backupId, { + status, + ...updates, + updatedAt: new Date(), + }); + + if (status === BackupStatus.COMPLETED) { + this.alertingService.sendAlert( + 'BACKUP_COMPLETED', + `Backup ${backupId} completed successfully`, + 'INFO', + ); + } else if (status === BackupStatus.FAILED) { + this.alertingService.sendAlert( + 'BACKUP_FAILED', + `Backup ${backupId} failed: ${updates.errorMessage}`, + 'CRITICAL', + ); + } + } + + toResponseDto(backup: BackupRecord): BackupResponseDto { + return { + id: backup.id, + backupType: backup.backupType, + status: backup.status, + region: backup.region, + databaseName: backup.databaseName, + backupSizeBytes: backup.backupSizeBytes, + integrityVerified: backup.integrityVerified, + completedAt: backup.completedAt, + expiresAt: backup.expiresAt, + createdAt: backup.createdAt, + metadata: backup.metadata, + }; + } +} diff --git a/src/backup/disaster-recovery/disaster-recovery.service.ts b/src/backup/disaster-recovery/disaster-recovery.service.ts new file mode 100644 index 0000000..ef7528d --- /dev/null +++ b/src/backup/disaster-recovery/disaster-recovery.service.ts @@ -0,0 +1,108 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { BackupService } from '../backup.service'; +import { AlertingService } from '../../monitoring/alerting/alerting.service'; +import { FileStorageService } from '../../media/storage/file-storage.service'; +import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; + +const execAsync = promisify(exec); +const RTO_THRESHOLD_SECONDS = 900; // 15 minutes + +@Injectable() +export class DisasterRecoveryService { + private readonly logger = new Logger(DisasterRecoveryService.name); + private readonly kmsClient: KMSClient; + + constructor( + private readonly backupService: BackupService, + private readonly alertingService: AlertingService, + private readonly fileStorageService: FileStorageService, + private readonly configService: ConfigService, + ) { + const awsRegion = this.configService.get('AWS_REGION', 'us-east-1'); + this.kmsClient = new KMSClient({ region: awsRegion }); + } + + async executeRestore(backupId: string): Promise { + this.logger.log(`Starting disaster recovery restore for backup: ${backupId}`); + + const rtoStartTime = Date.now(); + + try { + // Step 1: Get latest verified backup + const backup = await this.backupService.getLatestBackup(); + if (!backup) { + throw new Error('No verified backup available for restore'); + } + + // Step 2: Download from secondary region + this.logger.log('Downloading backup from secondary region'); + const backupData = await this.fileStorageService.downloadFile( + backup.replicatedStorageKey || backup.encryptedStorageKey, + ); + + // Step 3: Decrypt with AWS KMS + this.logger.log('Decrypting backup'); + const decryptCommand = new DecryptCommand({ + CiphertextBlob: backupData, + }); + const decryptResponse = await this.kmsClient.send(decryptCommand); + const decryptedData = Buffer.from(decryptResponse.Plaintext); + + // Save to temp file + const tempFile = `/tmp/disaster-recovery-${backupId}.sql`; + await fs.promises.writeFile(tempFile, decryptedData); + + // Step 4: Execute pg_restore to primary database + this.logger.log('Restoring to primary database'); + const databaseName = this.configService.get('DB_DATABASE', 'teachlink'); + await this.restoreDatabase(databaseName, tempFile); + + // Cleanup + await fs.promises.unlink(tempFile); + + // Step 5: Check RTO + const rtoActual = Math.floor((Date.now() - rtoStartTime) / 1000); + this.logger.log(`Disaster recovery completed. RTO: ${rtoActual} seconds`); + + if (rtoActual > RTO_THRESHOLD_SECONDS) { + this.alertingService.sendAlert( + 'DISASTER_RECOVERY_RTO_EXCEEDED', + `Disaster recovery completed but RTO exceeded: ${rtoActual}s > ${RTO_THRESHOLD_SECONDS}s`, + 'CRITICAL', + ); + } else { + this.alertingService.sendAlert( + 'DISASTER_RECOVERY_SUCCESS', + `Disaster recovery completed successfully in ${rtoActual} seconds`, + 'INFO', + ); + } + } catch (error) { + this.logger.error('Disaster recovery failed:', error); + this.alertingService.sendAlert( + 'DISASTER_RECOVERY_FAILED', + `Disaster recovery failed: ${error.message}`, + 'CRITICAL', + ); + throw error; + } + } + + private async restoreDatabase( + dbName: string, + backupFile: string, + ): Promise { + const host = this.configService.get('DB_HOST', 'localhost'); + const port = this.configService.get('DB_PORT', '5432'); + const username = this.configService.get('DB_USERNAME', 'postgres'); + const password = this.configService.get('DB_PASSWORD', ''); + + const restoreCommand = `PGPASSWORD="${password}" pg_restore -h ${host} -p ${port} -U ${username} -d ${dbName} -c ${backupFile}`; + + await execAsync(restoreCommand); + } +} diff --git a/src/backup/dto/backup-response.dto.ts b/src/backup/dto/backup-response.dto.ts new file mode 100644 index 0000000..1dc4581 --- /dev/null +++ b/src/backup/dto/backup-response.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { BackupStatus } from '../enums/backup-status.enum'; +import { BackupType } from '../enums/backup-type.enum'; +import { Region } from '../enums/region.enum'; + +export class BackupResponseDto { + @ApiProperty() + id: string; + + @ApiProperty({ enum: BackupType }) + backupType: BackupType; + + @ApiProperty({ enum: BackupStatus }) + status: BackupStatus; + + @ApiProperty({ enum: Region }) + region: Region; + + @ApiProperty() + databaseName: string; + + @ApiPropertyOptional() + backupSizeBytes?: number; + + @ApiPropertyOptional() + integrityVerified?: boolean; + + @ApiPropertyOptional() + completedAt?: Date; + + @ApiPropertyOptional() + expiresAt?: Date; + + @ApiProperty() + createdAt: Date; + + @ApiPropertyOptional() + metadata?: { + pgVersion?: string; + tableCounts?: Record; + totalRows?: number; + dumpDuration?: number; + }; +} diff --git a/src/backup/dto/recovery-test-response.dto.ts b/src/backup/dto/recovery-test-response.dto.ts new file mode 100644 index 0000000..7d5003b --- /dev/null +++ b/src/backup/dto/recovery-test-response.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { RecoveryTestStatus } from '../enums/recovery-test-status.enum'; + +export class RecoveryTestResponseDto { + @ApiProperty() + id: string; + + @ApiProperty() + backupRecordId: string; + + @ApiProperty({ enum: RecoveryTestStatus }) + status: RecoveryTestStatus; + + @ApiProperty() + testDatabaseName: string; + + @ApiPropertyOptional() + validationResults?: { + tableCountMatch?: boolean; + rowCountMatch?: boolean; + checksumMatch?: boolean; + schemaValid?: boolean; + connectionSuccessful?: boolean; + queriesExecuted?: number; + errors?: string[]; + }; + + @ApiPropertyOptional() + performanceMetrics?: { + totalDuration?: number; + }; + + @ApiProperty() + createdAt: Date; + + @ApiPropertyOptional() + testCompletedAt?: Date; +} diff --git a/src/backup/dto/restore-backup.dto.ts b/src/backup/dto/restore-backup.dto.ts new file mode 100644 index 0000000..40122ab --- /dev/null +++ b/src/backup/dto/restore-backup.dto.ts @@ -0,0 +1,8 @@ +import { IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RestoreBackupDto { + @ApiProperty({ description: 'Backup record ID to restore from' }) + @IsUUID() + backupRecordId: string; +} diff --git a/src/backup/dto/trigger-recovery-test.dto.ts b/src/backup/dto/trigger-recovery-test.dto.ts new file mode 100644 index 0000000..67e042b --- /dev/null +++ b/src/backup/dto/trigger-recovery-test.dto.ts @@ -0,0 +1,8 @@ +import { IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class TriggerRecoveryTestDto { + @ApiProperty({ description: 'Backup record ID to test' }) + @IsUUID() + backupRecordId: string; +} diff --git a/src/backup/entities/backup-record.entity.ts b/src/backup/entities/backup-record.entity.ts new file mode 100644 index 0000000..35d9797 --- /dev/null +++ b/src/backup/entities/backup-record.entity.ts @@ -0,0 +1,90 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; +import { BackupStatus } from '../enums/backup-status.enum'; +import { BackupType } from '../enums/backup-type.enum'; +import { Region } from '../enums/region.enum'; + +@Entity('backup_records') +@Index(['status', 'createdAt']) +@Index(['region']) +@Index(['completedAt']) +export class BackupRecord { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'enum', enum: BackupType, default: BackupType.FULL }) + backupType: BackupType; + + @Column({ type: 'enum', enum: BackupStatus, default: BackupStatus.PENDING }) + status: BackupStatus; + + @Column({ type: 'enum', enum: Region }) + region: Region; + + @Column() + databaseName: string; + + @Column() + storageKey: string; + + @Column({ nullable: true }) + encryptedStorageKey: string; + + @Column({ nullable: true }) + replicatedStorageKey: string; + + @Column({ nullable: true }) + kmsKeyId: string; + + @Column({ type: 'bigint', nullable: true }) + backupSizeBytes: number; + + @Column({ type: 'json', nullable: true }) + metadata: { + pgVersion?: string; + tableCounts?: Record; + totalRows?: number; + startTime?: Date; + endTime?: Date; + dumpDuration?: number; + uploadDuration?: number; + encryptionDuration?: number; + replicationDuration?: number; + }; + + @Column({ nullable: true }) + errorMessage: string; + + @Column({ default: 0 }) + retryCount: number; + + @Column({ nullable: true }) + checksumMd5: string; + + @Column({ nullable: true }) + checksumSha256: string; + + @Column({ default: false }) + integrityVerified: boolean; + + @Column({ nullable: true }) + verifiedAt: Date; + + @Column({ nullable: true }) + completedAt: Date; + + @Column({ nullable: true }) + expiresAt: Date; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/backup/entities/recovery-test.entity.ts b/src/backup/entities/recovery-test.entity.ts new file mode 100644 index 0000000..ed564a9 --- /dev/null +++ b/src/backup/entities/recovery-test.entity.ts @@ -0,0 +1,73 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, + Index, + JoinColumn, +} from 'typeorm'; +import { RecoveryTestStatus } from '../enums/recovery-test-status.enum'; +import { BackupRecord } from './backup-record.entity'; + +@Entity('recovery_tests') +@Index(['status', 'createdAt']) +@Index(['backupRecordId']) +@Index(['testCompletedAt']) +export class RecoveryTest { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'backup_record_id' }) + backupRecordId: string; + + @ManyToOne(() => BackupRecord, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'backup_record_id' }) + backupRecord: BackupRecord; + + @Column({ + type: 'enum', + enum: RecoveryTestStatus, + default: RecoveryTestStatus.PENDING, + }) + status: RecoveryTestStatus; + + @Column() + testDatabaseName: string; + + @Column({ type: 'json', nullable: true }) + validationResults: { + tableCountMatch?: boolean; + rowCountMatch?: boolean; + checksumMatch?: boolean; + schemaValid?: boolean; + connectionSuccessful?: boolean; + queriesExecuted?: number; + errors?: string[]; + }; + + @Column({ type: 'json', nullable: true }) + performanceMetrics: { + downloadDuration?: number; + decryptionDuration?: number; + restoreDuration?: number; + validationDuration?: number; + totalDuration?: number; + }; + + @Column({ nullable: true }) + errorMessage: string; + + @Column({ default: 0 }) + retryCount: number; + + @Column({ nullable: true }) + testCompletedAt: Date; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/backup/enums/backup-status.enum.ts b/src/backup/enums/backup-status.enum.ts new file mode 100644 index 0000000..193d09d --- /dev/null +++ b/src/backup/enums/backup-status.enum.ts @@ -0,0 +1,6 @@ +export enum BackupStatus { + PENDING = 'pending', + IN_PROGRESS = 'in_progress', + COMPLETED = 'completed', + FAILED = 'failed', +} diff --git a/src/backup/enums/backup-type.enum.ts b/src/backup/enums/backup-type.enum.ts new file mode 100644 index 0000000..6de7758 --- /dev/null +++ b/src/backup/enums/backup-type.enum.ts @@ -0,0 +1,3 @@ +export enum BackupType { + FULL = 'full', +} diff --git a/src/backup/enums/recovery-test-status.enum.ts b/src/backup/enums/recovery-test-status.enum.ts new file mode 100644 index 0000000..9f6f08e --- /dev/null +++ b/src/backup/enums/recovery-test-status.enum.ts @@ -0,0 +1,6 @@ +export enum RecoveryTestStatus { + PENDING = 'pending', + RUNNING = 'running', + PASSED = 'passed', + FAILED = 'failed', +} diff --git a/src/backup/enums/region.enum.ts b/src/backup/enums/region.enum.ts new file mode 100644 index 0000000..132a26e --- /dev/null +++ b/src/backup/enums/region.enum.ts @@ -0,0 +1,4 @@ +export enum Region { + US_EAST_1 = 'us-east-1', + US_WEST_2 = 'us-west-2', +} diff --git a/src/backup/integrity/data-integrity.service.ts b/src/backup/integrity/data-integrity.service.ts new file mode 100644 index 0000000..23b73aa --- /dev/null +++ b/src/backup/integrity/data-integrity.service.ts @@ -0,0 +1,88 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BackupRecord } from '../entities/backup-record.entity'; +import { FileStorageService } from '../../media/storage/file-storage.service'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; + +@Injectable() +export class DataIntegrityService { + private readonly logger = new Logger(DataIntegrityService.name); + + constructor( + @InjectRepository(BackupRecord) + private readonly backupRepository: Repository, + private readonly fileStorageService: FileStorageService, + ) {} + + async verifyBackupIntegrity(backupId: string): Promise { + this.logger.log(`Verifying backup integrity for: ${backupId}`); + + const backup = await this.backupRepository.findOne({ + where: { id: backupId }, + }); + + if (!backup) { + throw new NotFoundException(`Backup ${backupId} not found`); + } + + if (!backup.encryptedStorageKey) { + this.logger.error( + `Backup ${backupId} has no encrypted storage key, cannot verify`, + ); + return false; + } + + try { + // Download backup from S3 + const backupData = await this.fileStorageService.downloadFile( + backup.encryptedStorageKey, + ); + + // Save to temp file + const tempFile = `/tmp/verify-${backupId}.backup`; + await fs.promises.writeFile(tempFile, backupData); + + // Calculate checksums + const checksums = await this.calculateChecksums(tempFile); + + // Clean up temp file + await fs.promises.unlink(tempFile); + + // Compare checksums + const md5Match = checksums.md5 === backup.checksumMd5; + const sha256Match = checksums.sha256 === backup.checksumSha256; + + if (md5Match && sha256Match) { + this.logger.log(`Backup ${backupId} integrity verified successfully`); + return true; + } else { + this.logger.error( + `Backup ${backupId} integrity verification failed. MD5: ${md5Match}, SHA256: ${sha256Match}`, + ); + return false; + } + } catch (error) { + this.logger.error( + `Error verifying backup ${backupId} integrity:`, + error, + ); + return false; + } + } + + async calculateChecksums( + filePath: string, + ): Promise<{ md5: string; sha256: string }> { + const fileBuffer = await fs.promises.readFile(filePath); + + const md5 = crypto.createHash('md5').update(fileBuffer).digest('hex'); + const sha256 = crypto + .createHash('sha256') + .update(fileBuffer) + .digest('hex'); + + return { md5, sha256 }; + } +} diff --git a/src/backup/interfaces/backup.interfaces.ts b/src/backup/interfaces/backup.interfaces.ts new file mode 100644 index 0000000..39c93fd --- /dev/null +++ b/src/backup/interfaces/backup.interfaces.ts @@ -0,0 +1,19 @@ +import { Region } from '../enums/region.enum'; + +export interface BackupJobData { + backupRecordId: string; + backupType: string; + region: Region; + databaseName: string; +} + +export interface VerificationJobData { + backupRecordId: string; + storageKey: string; +} + +export interface RecoveryTestJobData { + recoveryTestId: string; + backupRecordId: string; + testDatabaseName: string; +} diff --git a/src/backup/monitoring/backup-monitoring.service.ts b/src/backup/monitoring/backup-monitoring.service.ts new file mode 100644 index 0000000..c2359cf --- /dev/null +++ b/src/backup/monitoring/backup-monitoring.service.ts @@ -0,0 +1,110 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BackupRecord } from '../entities/backup-record.entity'; +import { RecoveryTest } from '../entities/recovery-test.entity'; +import { BackupStatus } from '../enums/backup-status.enum'; +import { RecoveryTestStatus } from '../enums/recovery-test-status.enum'; +import { MetricsCollectionService } from '../../monitoring/metrics/metrics-collection.service'; +import { AlertingService } from '../../monitoring/alerting/alerting.service'; +import { Histogram, Counter } from 'prom-client'; + +@Injectable() +export class BackupMonitoringService { + private readonly logger = new Logger(BackupMonitoringService.name); + private backupDuration: Histogram; + private backupTotal: Counter; + + constructor( + @InjectRepository(BackupRecord) + private readonly backupRepository: Repository, + @InjectRepository(RecoveryTest) + private readonly recoveryTestRepository: Repository, + private readonly metricsService: MetricsCollectionService, + private readonly alertingService: AlertingService, + ) { + this.initializeMetrics(); + } + + private initializeMetrics(): void { + const registry = this.metricsService.getRegistry(); + + this.backupDuration = new Histogram({ + name: 'backup_duration_seconds', + help: 'Duration of backup operations in seconds', + labelNames: ['status'], + buckets: [60, 300, 600, 900, 1200], + registers: [registry], + }); + + this.backupTotal = new Counter({ + name: 'backup_total', + help: 'Total number of backups', + labelNames: ['status'], + registers: [registry], + }); + } + + async checkBackupHealth(): Promise<{ healthy: boolean; issues: string[] }> { + const issues: string[] = []; + + // Check last backup was successful + const lastBackup = await this.backupRepository.findOne({ + where: {}, + order: { createdAt: 'DESC' }, + }); + + if (!lastBackup) { + issues.push('No backups found'); + } else { + if (lastBackup.status === BackupStatus.FAILED) { + issues.push(`Last backup failed: ${lastBackup.errorMessage}`); + } + + // Check last backup was within 7 days + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + if (lastBackup.createdAt < sevenDaysAgo) { + issues.push('Last backup is older than 7 days'); + } + + // Check replication + if (lastBackup.status === BackupStatus.COMPLETED && !lastBackup.replicatedStorageKey) { + issues.push('Last backup was not replicated to secondary region'); + } + } + + // Check recent recovery tests + const lastTest = await this.recoveryTestRepository.findOne({ + where: {}, + order: { createdAt: 'DESC' }, + }); + + if (lastTest && lastTest.status === RecoveryTestStatus.FAILED) { + issues.push(`Last recovery test failed: ${lastTest.errorMessage}`); + } + + return { + healthy: issues.length === 0, + issues, + }; + } + + async recordBackupMetrics(backupId: string, duration: number): Promise { + const backup = await this.backupRepository.findOne({ + where: { id: backupId }, + }); + + if (!backup) { + return; + } + + const status = backup.status === BackupStatus.COMPLETED ? 'success' : 'failure'; + + this.backupDuration.observe({ status }, duration / 1000); + this.backupTotal.inc({ status }); + + this.logger.log(`Recorded backup metrics for ${backupId}: ${duration}ms, status: ${status}`); + } +} diff --git a/src/backup/processing/backup-queue.processor.ts b/src/backup/processing/backup-queue.processor.ts new file mode 100644 index 0000000..1ba0b21 --- /dev/null +++ b/src/backup/processing/backup-queue.processor.ts @@ -0,0 +1,339 @@ +import { Process, Processor } from '@nestjs/bull'; +import { Logger } from '@nestjs/common'; +import { Job } from 'bull'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import { BackupRecord } from '../entities/backup-record.entity'; +import { BackupStatus } from '../enums/backup-status.enum'; +import { BackupService } from '../backup.service'; +import { FileStorageService } from '../../media/storage/file-storage.service'; +import { DataIntegrityService } from '../integrity/data-integrity.service'; +import { + BackupJobData, + VerificationJobData, + RecoveryTestJobData, +} from '../interfaces/backup.interfaces'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; +import { KMSClient, EncryptCommand, DecryptCommand } from '@aws-sdk/client-kms'; +import { S3Client, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; + +const execAsync = promisify(exec); +const MAX_RETRIES = 3; + +@Processor('backup-processing') +export class BackupQueueProcessor { + private readonly logger = new Logger(BackupQueueProcessor.name); + private readonly kmsClient: KMSClient; + private readonly s3Client: S3Client; + + constructor( + @InjectRepository(BackupRecord) + private readonly backupRepository: Repository, + private readonly backupService: BackupService, + private readonly fileStorageService: FileStorageService, + private readonly dataIntegrityService: DataIntegrityService, + private readonly configService: ConfigService, + ) { + const awsRegion = this.configService.get('AWS_REGION', 'us-east-1'); + + this.kmsClient = new KMSClient({ + region: awsRegion, + credentials: { + accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), + secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY', ''), + }, + }); + + this.s3Client = new S3Client({ + region: awsRegion, + credentials: { + accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), + secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY', ''), + }, + }); + } + + @Process('create-backup') + async handleCreateBackup(job: Job) { + const { backupRecordId, databaseName } = job.data; + this.logger.log(`Processing backup creation for: ${backupRecordId}`); + + const backup = await this.backupRepository.findOne({ + where: { id: backupRecordId }, + }); + if (!backup) { + this.logger.warn(`Backup ${backupRecordId} not found, skipping`); + return; + } + + try { + // Step 1: Create pg_dump + await this.backupService.updateBackupStatus( + backupRecordId, + BackupStatus.IN_PROGRESS, + ); + const dumpStartTime = Date.now(); + + const tempFile = path.join('/tmp', `backup-${backupRecordId}.sql`); + const pgDumpCommand = this.buildPgDumpCommand(databaseName, tempFile); + + await execAsync(pgDumpCommand); + const dumpDuration = Date.now() - dumpStartTime; + + const stats = await fs.promises.stat(tempFile); + backup.backupSizeBytes = stats.size; + + // Step 2: Upload to S3 + const uploadStartTime = Date.now(); + const storageKey = `backups/${backup.region}/${databaseName}/${backupRecordId}.sql`; + const fileBuffer = await fs.promises.readFile(tempFile); + + await this.fileStorageService.uploadProcessedFile( + fileBuffer, + storageKey, + 'application/sql', + ); + + const uploadDuration = Date.now() - uploadStartTime; + backup.storageKey = storageKey; + + // Step 3: Encrypt with AWS KMS + const encryptionStartTime = Date.now(); + const kmsKeyId = this.configService.get('AWS_KMS_KEY_ID'); + const encryptedKey = await this.encryptBackup(storageKey, kmsKeyId); + backup.encryptedStorageKey = encryptedKey; + backup.kmsKeyId = kmsKeyId; + + const encryptionDuration = Date.now() - encryptionStartTime; + + // Step 4: Replicate to secondary region + const replicationStartTime = Date.now(); + const secondaryRegion = this.configService.get( + 'BACKUP_SECONDARY_REGION', + 'us-west-2', + ); + const replicatedKey = await this.replicateToRegion( + encryptedKey, + secondaryRegion, + ); + backup.replicatedStorageKey = replicatedKey; + + const replicationDuration = Date.now() - replicationStartTime; + + // Step 5: Calculate checksums + const checksums = await this.dataIntegrityService.calculateChecksums( + tempFile, + ); + backup.checksumMd5 = checksums.md5; + backup.checksumSha256 = checksums.sha256; + + // Cleanup temp file + await fs.promises.unlink(tempFile); + + // Update metadata + backup.metadata = { + ...backup.metadata, + endTime: new Date(), + dumpDuration, + uploadDuration, + encryptionDuration, + replicationDuration, + }; + + await this.backupRepository.save(backup); + + // Queue verification job + await (job.queue as any).add( + 'verify-backup', + { backupRecordId, storageKey: encryptedKey }, + { + attempts: 3, + backoff: { type: 'exponential', delay: 5000 }, + }, + ); + + this.logger.log(`Backup ${backupRecordId} completed successfully`); + } catch (error) { + await this.handleProcessingError(backup, error, job.attemptsMade); + throw error; // Re-throw to trigger retry + } + } + + @Process('verify-backup') + async handleVerifyBackup(job: Job) { + const { backupRecordId } = job.data; + this.logger.log(`Verifying backup integrity: ${backupRecordId}`); + + try { + const isValid = await this.dataIntegrityService.verifyBackupIntegrity( + backupRecordId, + ); + + if (isValid) { + await this.backupService.updateBackupStatus( + backupRecordId, + BackupStatus.COMPLETED, + { + integrityVerified: true, + verifiedAt: new Date(), + completedAt: new Date(), + }, + ); + this.logger.log(`Backup ${backupRecordId} verified successfully`); + } else { + throw new Error('Backup integrity verification failed'); + } + } catch (error) { + await this.backupService.updateBackupStatus( + backupRecordId, + BackupStatus.FAILED, + { + errorMessage: `Verification failed: ${error.message}`, + }, + ); + throw error; + } + } + + @Process('recovery-test') + async handleRecoveryTest(job: Job) { + this.logger.log(`Recovery test processing handled by RecoveryTestingService`); + // Delegated to RecoveryTestingService.executeRecoveryTest() + } + + @Process('delete-backup') + async handleDeleteBackup(job: Job<{ backupRecordId: string }>) { + const { backupRecordId } = job.data; + this.logger.log(`Deleting expired backup: ${backupRecordId}`); + + const backup = await this.backupRepository.findOne({ + where: { id: backupRecordId }, + }); + + if (!backup) { + this.logger.warn(`Backup ${backupRecordId} not found, skipping deletion`); + return; + } + + try { + const bucketName = this.configService.get('AWS_S3_BUCKET', ''); + + // Delete from primary region + if (backup.encryptedStorageKey) { + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: bucketName, + Key: backup.encryptedStorageKey, + }), + ); + } + + // Delete from secondary region (if replicated) + if (backup.replicatedStorageKey) { + const secondaryBucket = this.configService.get( + 'AWS_S3_BUCKET_SECONDARY', + bucketName, + ); + const secondaryS3Client = new S3Client({ + region: this.configService.get('BACKUP_SECONDARY_REGION', 'us-west-2'), + }); + + await secondaryS3Client.send( + new DeleteObjectCommand({ + Bucket: secondaryBucket, + Key: backup.replicatedStorageKey, + }), + ); + } + + // Delete from database + await this.backupRepository.remove(backup); + + this.logger.log(`Backup ${backupRecordId} deleted successfully`); + } catch (error) { + this.logger.error(`Failed to delete backup ${backupRecordId}:`, error); + throw error; + } + } + + private buildPgDumpCommand(databaseName: string, outputFile: string): string { + const host = this.configService.get('DB_HOST', 'localhost'); + const port = this.configService.get('DB_PORT', '5432'); + const username = this.configService.get('DB_USERNAME', 'postgres'); + const password = this.configService.get('DB_PASSWORD', ''); + + return `PGPASSWORD="${password}" pg_dump -h ${host} -p ${port} -U ${username} -F c -b -v -f ${outputFile} ${databaseName}`; + } + + private async encryptBackup( + storageKey: string, + kmsKeyId: string, + ): Promise { + const encryptedKey = `${storageKey}.encrypted`; + + // Download from S3, encrypt with KMS, re-upload + const fileBuffer = await this.fileStorageService.downloadFile(storageKey); + + const command = new EncryptCommand({ + KeyId: kmsKeyId, + Plaintext: fileBuffer, + }); + + const response = await this.kmsClient.send(command); + + await this.fileStorageService.uploadProcessedFile( + Buffer.from(response.CiphertextBlob), + encryptedKey, + 'application/octet-stream', + ); + + return encryptedKey; + } + + private async replicateToRegion( + storageKey: string, + targetRegion: string, + ): Promise { + this.logger.log(`Replicating ${storageKey} to ${targetRegion}`); + + const sourceBucket = this.configService.get('AWS_S3_BUCKET', ''); + const targetBucket = this.configService.get( + 'AWS_S3_BUCKET_SECONDARY', + sourceBucket, + ); + + const targetKey = storageKey.replace(`backups/`, `backups-${targetRegion}/`); + + const copyCommand = new CopyObjectCommand({ + CopySource: `${sourceBucket}/${storageKey}`, + Bucket: targetBucket, + Key: targetKey, + }); + + await this.s3Client.send(copyCommand); + + return targetKey; + } + + private async handleProcessingError( + backup: BackupRecord, + error: Error, + attemptsMade: number, + ): Promise { + this.logger.error(`Backup processing failed for ${backup.id}:`, error); + + backup.retryCount = attemptsMade; + backup.errorMessage = error.message; + + if (attemptsMade >= MAX_RETRIES) { + backup.status = BackupStatus.FAILED; + this.logger.error(`Max retries exceeded for backup ${backup.id}`); + } + + await this.backupRepository.save(backup); + } +} diff --git a/src/backup/testing/recovery-testing.service.ts b/src/backup/testing/recovery-testing.service.ts new file mode 100644 index 0000000..555eb8b --- /dev/null +++ b/src/backup/testing/recovery-testing.service.ts @@ -0,0 +1,271 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; +import { ConfigService } from '@nestjs/config'; +import { RecoveryTest } from '../entities/recovery-test.entity'; +import { RecoveryTestStatus } from '../enums/recovery-test-status.enum'; +import { BackupService } from '../backup.service'; +import { RecoveryTestResponseDto } from '../dto/recovery-test-response.dto'; +import { RecoveryTestJobData } from '../interfaces/backup.interfaces'; +import { FileStorageService } from '../../media/storage/file-storage.service'; +import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms'; +import { AlertingService } from '../../monitoring/alerting/alerting.service'; +import { Client } from 'pg'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; + +const execAsync = promisify(exec); + +@Injectable() +export class RecoveryTestingService { + private readonly logger = new Logger(RecoveryTestingService.name); + private readonly kmsClient: KMSClient; + + constructor( + @InjectRepository(RecoveryTest) + private readonly recoveryTestRepository: Repository, + @InjectQueue('backup-processing') + private readonly backupQueue: Queue, + private readonly backupService: BackupService, + private readonly fileStorageService: FileStorageService, + private readonly configService: ConfigService, + private readonly alertingService: AlertingService, + ) { + const awsRegion = this.configService.get('AWS_REGION', 'us-east-1'); + this.kmsClient = new KMSClient({ region: awsRegion }); + } + + async createRecoveryTest(backupId: string): Promise { + const backup = await this.backupService.getLatestBackup(); + if (!backup) { + throw new NotFoundException(`No verified backup found`); + } + + const testDatabaseName = this.configService.get( + 'BACKUP_TEST_DATABASE', + 'teachlink_backup_test', + ); + + const recoveryTest = this.recoveryTestRepository.create({ + backupRecordId: backupId, + status: RecoveryTestStatus.PENDING, + testDatabaseName, + }); + + await this.recoveryTestRepository.save(recoveryTest); + + // Queue recovery test job + await this.backupQueue.add( + 'recovery-test', + { + recoveryTestId: recoveryTest.id, + backupRecordId: backupId, + testDatabaseName, + } as RecoveryTestJobData, + { + attempts: 3, + backoff: { type: 'exponential', delay: 10000 }, + }, + ); + + return this.toResponseDto(recoveryTest); + } + + async executeRecoveryTest(testId: string): Promise { + const test = await this.recoveryTestRepository.findOne({ + where: { id: testId }, + relations: ['backupRecord'], + }); + + if (!test) { + throw new NotFoundException(`Recovery test ${testId} not found`); + } + + const totalStartTime = Date.now(); + + try { + test.status = RecoveryTestStatus.RUNNING; + await this.recoveryTestRepository.save(test); + + // Step 1: Download encrypted backup + const downloadStartTime = Date.now(); + const backupData = await this.fileStorageService.downloadFile( + test.backupRecord.encryptedStorageKey, + ); + const downloadDuration = Date.now() - downloadStartTime; + + // Step 2: Decrypt + const decryptStartTime = Date.now(); + const decryptCommand = new DecryptCommand({ + CiphertextBlob: backupData, + }); + const decryptResponse = await this.kmsClient.send(decryptCommand); + const decryptedData = Buffer.from(decryptResponse.Plaintext); + const decryptionDuration = Date.now() - decryptStartTime; + + // Save to temp file + const tempFile = `/tmp/recovery-test-${testId}.sql`; + await fs.promises.writeFile(tempFile, decryptedData); + + // Step 3: Create test database + await this.createTestDatabase(test.testDatabaseName); + + // Step 4: Restore + const restoreStartTime = Date.now(); + await this.restoreDatabase(test.testDatabaseName, tempFile); + const restoreDuration = Date.now() - restoreStartTime; + + // Step 5: Validate + const validationStartTime = Date.now(); + const validationResults = await this.validateRestoredDatabase( + test.testDatabaseName, + ); + const validationDuration = Date.now() - validationStartTime; + + // Step 6: Cleanup + await this.dropTestDatabase(test.testDatabaseName); + await fs.promises.unlink(tempFile); + + // Update test results + test.status = validationResults.connectionSuccessful + ? RecoveryTestStatus.PASSED + : RecoveryTestStatus.FAILED; + test.validationResults = validationResults; + test.performanceMetrics = { + downloadDuration, + decryptionDuration, + restoreDuration, + validationDuration, + totalDuration: Date.now() - totalStartTime, + }; + test.testCompletedAt = new Date(); + + await this.recoveryTestRepository.save(test); + + // Send alert + this.alertingService.sendAlert( + 'RECOVERY_TEST_COMPLETED', + `Recovery test ${testId} ${test.status}`, + test.status === RecoveryTestStatus.PASSED ? 'INFO' : 'CRITICAL', + ); + } catch (error) { + this.logger.error(`Recovery test ${testId} failed:`, error); + test.status = RecoveryTestStatus.FAILED; + test.errorMessage = error.message; + await this.recoveryTestRepository.save(test); + + this.alertingService.sendAlert( + 'RECOVERY_TEST_FAILED', + `Recovery test ${testId} failed: ${error.message}`, + 'CRITICAL', + ); + } + } + + async getTestResults(testId: string): Promise { + const test = await this.recoveryTestRepository.findOne({ + where: { id: testId }, + relations: ['backupRecord'], + }); + + if (!test) { + throw new NotFoundException(`Recovery test ${testId} not found`); + } + + return this.toResponseDto(test); + } + + private async createTestDatabase(dbName: string): Promise { + const client = new Client({ + host: this.configService.get('DB_HOST', 'localhost'), + port: parseInt(this.configService.get('DB_PORT', '5432')), + user: this.configService.get('DB_USERNAME', 'postgres'), + password: this.configService.get('DB_PASSWORD', ''), + database: 'postgres', + }); + + await client.connect(); + await client.query(`DROP DATABASE IF EXISTS ${dbName}`); + await client.query(`CREATE DATABASE ${dbName}`); + await client.end(); + } + + private async restoreDatabase( + dbName: string, + backupFile: string, + ): Promise { + const host = this.configService.get('DB_HOST', 'localhost'); + const port = this.configService.get('DB_PORT', '5432'); + const username = this.configService.get('DB_USERNAME', 'postgres'); + const password = this.configService.get('DB_PASSWORD', ''); + + const restoreCommand = `PGPASSWORD="${password}" pg_restore -h ${host} -p ${port} -U ${username} -d ${dbName} ${backupFile}`; + + await execAsync(restoreCommand); + } + + private async validateRestoredDatabase(dbName: string): Promise { + const client = new Client({ + host: this.configService.get('DB_HOST', 'localhost'), + port: parseInt(this.configService.get('DB_PORT', '5432')), + user: this.configService.get('DB_USERNAME', 'postgres'), + password: this.configService.get('DB_PASSWORD', ''), + database: dbName, + }); + + try { + await client.connect(); + + // Run validation queries + const tableCountResult = await client.query( + `SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'`, + ); + const tableCount = parseInt(tableCountResult.rows[0].count); + + await client.end(); + + return { + tableCountMatch: tableCount > 0, + connectionSuccessful: true, + queriesExecuted: 1, + }; + } catch (error) { + return { + connectionSuccessful: false, + errors: [error.message], + }; + } + } + + private async dropTestDatabase(dbName: string): Promise { + const client = new Client({ + host: this.configService.get('DB_HOST', 'localhost'), + port: parseInt(this.configService.get('DB_PORT', '5432')), + user: this.configService.get('DB_USERNAME', 'postgres'), + password: this.configService.get('DB_PASSWORD', ''), + database: 'postgres', + }); + + await client.connect(); + await client.query(`DROP DATABASE IF EXISTS ${dbName}`); + await client.end(); + } + + private toResponseDto(test: RecoveryTest): RecoveryTestResponseDto { + return { + id: test.id, + backupRecordId: test.backupRecordId, + status: test.status, + testDatabaseName: test.testDatabaseName, + validationResults: test.validationResults, + performanceMetrics: test.performanceMetrics + ? { totalDuration: test.performanceMetrics.totalDuration } + : undefined, + createdAt: test.createdAt, + testCompletedAt: test.testCompletedAt, + }; + } +} diff --git a/src/media/media.module.ts b/src/media/media.module.ts new file mode 100644 index 0000000..48c4c77 --- /dev/null +++ b/src/media/media.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { FileStorageService } from './storage/file-storage.service'; + +@Module({ + imports: [ConfigModule], + providers: [FileStorageService], + exports: [FileStorageService], +}) +export class MediaModule {} diff --git a/src/media/storage/file-storage.service.ts b/src/media/storage/file-storage.service.ts new file mode 100644 index 0000000..1789f2c --- /dev/null +++ b/src/media/storage/file-storage.service.ts @@ -0,0 +1,74 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + S3Client, + GetObjectCommand, + PutObjectCommand, + DeleteObjectCommand, +} from '@aws-sdk/client-s3'; +import { Readable } from 'stream'; + +@Injectable() +export class FileStorageService { + private readonly logger = new Logger(FileStorageService.name); + private readonly s3Client: S3Client; + private readonly bucketName: string; + + constructor(private configService: ConfigService) { + this.bucketName = this.configService.get('AWS_S3_BUCKET', ''); + + this.s3Client = new S3Client({ + region: this.configService.get('AWS_REGION', 'us-east-1'), + credentials: { + accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID', ''), + secretAccessKey: this.configService.get( + 'AWS_SECRET_ACCESS_KEY', + '', + ), + }, + }); + } + + async uploadProcessedFile( + buffer: Buffer, + key: string, + contentType: string, + ): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucketName, + Key: key, + Body: buffer, + ContentType: contentType, + }); + + await this.s3Client.send(command); + this.logger.log(`Uploaded file to ${key}`); + } + + async downloadFile(storageKey: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucketName, + Key: storageKey, + }); + + const response = await this.s3Client.send(command); + const stream = response.Body as Readable; + const chunks: Buffer[] = []; + + for await (const chunk of stream) { + chunks.push(chunk); + } + + return Buffer.concat(chunks); + } + + async deleteFile(storageKey: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: storageKey, + }); + + await this.s3Client.send(command); + this.logger.log(`Deleted file ${storageKey}`); + } +} diff --git a/src/monitoring/monitoring.module.ts b/src/monitoring/monitoring.module.ts index 80e56cc..be7504e 100644 --- a/src/monitoring/monitoring.module.ts +++ b/src/monitoring/monitoring.module.ts @@ -17,6 +17,6 @@ import { AlertingService } from './alerting/alerting.service'; OptimizationService, AlertingService, ], - exports: [MetricsCollectionService], + exports: [MetricsCollectionService, AlertingService], }) export class MonitoringModule {}