diff --git a/backend/package-lock.json b/backend/package-lock.json index 808cf3204..d25407aca 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -53,7 +53,7 @@ "@opentelemetry/semantic-conventions": "^1.27.0", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.12.1", - "@react-email/components": "0.0.36", + "@react-email/components": "^1.0.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.2", @@ -145,7 +145,7 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", - "@react-email/preview-server": "^4.3.0", + "@react-email/preview-server": "^5.0.6", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -183,7 +183,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "^4.3.0", + "react-email": "^5.0.6", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -203,19 +203,6 @@ "node": ">=0.10.0" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -674,6 +661,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.637.0.tgz", "integrity": "sha512-xUi7x4qDubtA8QREtlblPuAcn91GS/09YVEY/RwU7xCY0aqGuFwgszAANlha4OUIqva8oVj2WO4gJuG+iaSnhw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2120,6 +2108,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2173,6 +2162,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2672,6 +2662,7 @@ "version": "3.632.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.632.0.tgz", "integrity": "sha512-Oh1fIWaoZluihOCb/zDEpRTi+6an82fgJz7fyRBugyLhEtDjmvpCQ3oKjzaOhoN+4EvXAm1ZS/ZgpvXBlIRTgw==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2748,6 +2739,7 @@ "version": "3.632.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.632.0.tgz", "integrity": "sha512-Ss5cBH09icpTvT+jtGGuQlRdwtO7RyE9BF4ZV/CEPATdd9whtJt4Qxdya8BUnkWR7h5HHTrQHqai3YVYjku41A==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -5185,6 +5177,7 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -7216,6 +7209,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -7237,6 +7231,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -7446,9 +7441,9 @@ "license": "BSD-3-Clause" }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -7813,23 +7808,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", @@ -8336,48 +8314,6 @@ "p-limit": "^3.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@gitbeaker/core": { "version": "42.5.0", "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz", @@ -8685,14 +8621,15 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -8709,13 +8646,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -8732,13 +8669,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -8753,9 +8690,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -8770,9 +8707,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -8787,9 +8724,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -8804,9 +8741,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -8820,10 +8757,27 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -8838,9 +8792,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -8855,9 +8809,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -8872,9 +8826,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -8889,9 +8843,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -8908,13 +8862,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -8931,13 +8885,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], @@ -8954,13 +8908,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -8977,13 +8954,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -9000,13 +8977,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -9023,13 +9000,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -9046,13 +9023,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], @@ -9060,7 +9037,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.5.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -9070,9 +9047,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -9090,9 +9067,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -9110,9 +9087,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -9235,6 +9212,9 @@ "win32" ] }, + "node_modules/@infisical/quic/node_modules/@infisical/quic-linux-arm": { + "optional": true + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -9338,28 +9318,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -9468,26 +9426,6 @@ "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" }, - "node_modules/@lottiefiles/dotlottie-react": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.3.tgz", - "integrity": "sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@lottiefiles/dotlottie-web": "0.42.0" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19" - } - }, - "node_modules/@lottiefiles/dotlottie-web": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.42.0.tgz", - "integrity": "sha512-Zr2LCaOAoPCsdAQgeLyCSiQ1+xrAJtRCyuEYDj0qR5heUwpc+Pxbb88JyTVumcXFfKOBMOMmrlsTScLz2mrvQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -9727,16 +9665,16 @@ } }, "node_modules/@next/env": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", - "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", + "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", "dev": true, "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", - "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", + "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", "cpu": [ "arm64" ], @@ -9751,9 +9689,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", - "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", + "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", "cpu": [ "x64" ], @@ -9768,9 +9706,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", - "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", + "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", "cpu": [ "arm64" ], @@ -9785,9 +9723,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", - "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", + "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", "cpu": [ "arm64" ], @@ -9802,9 +9740,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", - "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", + "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", "cpu": [ "x64" ], @@ -9819,9 +9757,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", - "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", + "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", "cpu": [ "x64" ], @@ -9836,9 +9774,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", - "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", + "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", "cpu": [ "arm64" ], @@ -9853,9 +9791,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", - "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", + "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", "cpu": [ "x64" ], @@ -10321,6 +10259,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -10764,6 +10703,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -11399,801 +11339,22 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@react-email/body": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz", - "integrity": "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.0.tgz", + "integrity": "sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/button": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz", - "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", + "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12202,15 +11363,16 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.12.tgz", - "integrity": "sha512-Faw3Ij9+/Qwq6moWaeHnV8Hn7ekc/EqyAzPi6yUar21dhcqYugCC4Da1x4d9nA9zC0H9KU3lYVJczh8D3cA+Eg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.0.tgz", + "integrity": "sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==", "license": "MIT", + "peer": true, "dependencies": { - "prismjs": "1.30.0" + "prismjs": "^1.30.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -12221,6 +11383,7 @@ "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz", "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12241,14 +11404,14 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.36.tgz", - "integrity": "sha512-VMh+OQplAnG8JMLlJjdnjt+ThJZ+JVkp0q2YMS2NEz+T88N22bLD2p7DZO0QgtNaKgumOhJI/0a2Q7VzCrwu5g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.1.tgz", + "integrity": "sha512-HnL0Y/up61sOBQT2cQg9N/kCoW0bP727gDs2MkFWQYELg6+iIHidMDvENXFC0f1ZE6hTB+4t7sszptvTcJWsDA==", "license": "MIT", "dependencies": { - "@react-email/body": "0.0.11", - "@react-email/button": "0.0.19", - "@react-email/code-block": "0.0.12", + "@react-email/body": "0.2.0", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.2.0", "@react-email/code-inline": "0.0.5", "@react-email/column": "0.0.13", "@react-email/container": "0.0.15", @@ -12259,26 +11422,44 @@ "@react-email/html": "0.0.11", "@react-email/img": "0.0.11", "@react-email/link": "0.0.12", - "@react-email/markdown": "0.0.14", - "@react-email/preview": "0.0.12", - "@react-email/render": "1.0.6", + "@react-email/markdown": "0.0.17", + "@react-email/preview": "0.0.13", + "@react-email/render": "2.0.0", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "1.0.4", - "@react-email/text": "0.1.1" + "@react-email/tailwind": "2.0.1", + "@react-email/text": "0.1.5" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-email/components/node_modules/@react-email/render": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.0.tgz", + "integrity": "sha512-rdjNj6iVzv8kRKDPFas+47nnoe6B40+nwukuXwY4FCwM7XBg6tmYr+chQryCuavUj2J65MMf6fztk1bxOUiSVA==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@react-email/container": { "version": "0.0.15", "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12312,6 +11493,7 @@ "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz", "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12324,6 +11506,7 @@ "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz", "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12348,6 +11531,7 @@ "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz", "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12360,6 +11544,7 @@ "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz", "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12368,25 +11553,26 @@ } }, "node_modules/@react-email/markdown": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz", - "integrity": "sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.17.tgz", + "integrity": "sha512-6op3AfsBC9BJKkhG+eoMFRFWlr0/f3FYbtQrK+VhGzJocEAY0WINIFN+W8xzXr//3IL0K/aKtnH3FtpIuescQQ==", "license": "MIT", "dependencies": { - "md-to-react-email": "5.0.5" + "marked": "^15.0.12" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/preview": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz", - "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", + "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12395,864 +11581,13 @@ } }, "node_modules/@react-email/preview-server": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.0.tgz", - "integrity": "sha512-cUaSrxezCzdg2hF6PzIxVrtagLdw3z3ovHeB3y2RDkmDZpp7EeIoNyJm22Ch2S0uAqTZNAgqu67aroLn3mFC1A==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-5.0.6.tgz", + "integrity": "sha512-hyaQyNeDTJKHrzdnPFdvw7nbohS+jBRzgdQLVVyBcRhiSV3iltqLlsaDVI/x+GJBWxtigbneL9SQ4v/EtOcHKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "7.26.10", - "@babel/parser": "7.27.0", - "@babel/traverse": "7.27.0", - "@lottiefiles/dotlottie-react": "0.13.3", - "@radix-ui/colors": "3.0.0", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@types/node": "22.14.1", - "@types/normalize-path": "3.0.2", - "@types/react": "19.0.10", - "@types/react-dom": "19.0.4", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.21", - "clsx": "2.1.1", - "esbuild": "0.25.10", - "framer-motion": "12.23.22", - "json5": "2.2.3", - "log-symbols": "4.1.0", - "module-punycode": "npm:punycode@2.3.1", - "next": "15.5.2", - "node-html-parser": "7.0.1", - "ora": "5.4.1", - "pretty-bytes": "6.1.1", - "prism-react-renderer": "2.4.1", - "react": "19.0.0", - "react-dom": "19.0.0", - "sharp": "0.34.4", - "socket.io-client": "4.8.1", - "sonner": "2.0.3", - "source-map-js": "1.2.1", - "spamc": "0.0.5", - "stacktrace-parser": "0.1.11", - "tailwind-merge": "3.2.0", - "tailwindcss": "3.4.0", - "use-debounce": "10.0.4", - "zod": "3.24.3" - } - }, - "node_modules/@react-email/preview-server/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@react-email/preview-server/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@react-email/preview-server/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@react-email/preview-server/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@react-email/preview-server/node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" - } - }, - "node_modules/@react-email/preview-server/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-email/preview-server/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-email/preview-server/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-email/preview-server/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-email/preview-server/node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.25.0" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@react-email/preview-server/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/preview-server/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/render": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", - "integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==", - "license": "MIT", - "dependencies": { - "html-to-text": "9.0.5", - "prettier": "3.5.3", - "react-promise-suspense": "0.3.4" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + "next": "16.0.7" } }, "node_modules/@react-email/row": { @@ -13280,22 +11615,69 @@ } }, "node_modules/@react-email/tailwind": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz", - "integrity": "sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.1.tgz", + "integrity": "sha512-/xq0IDYVY7863xPY7cdI45Xoz7M6CnIQBJcQvbqN7MNVpopfH9f+mhjayV1JGfKaxlGWuxfLKhgi9T2shsnEFg==", "license": "MIT", + "dependencies": { + "tailwindcss": "^4.1.12" + }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { + "@react-email/body": "0.2.0", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.2.0", + "@react-email/code-inline": "0.0.5", + "@react-email/container": "0.0.15", + "@react-email/heading": "0.0.15", + "@react-email/hr": "0.0.11", + "@react-email/img": "0.0.11", + "@react-email/link": "0.0.12", + "@react-email/preview": "0.0.13", + "@react-email/text": "0.1.5", "react": "^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@react-email/body": { + "optional": true + }, + "@react-email/button": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } } }, "node_modules/@react-email/text": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.1.tgz", - "integrity": "sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", + "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -15138,28 +13520,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -15306,6 +13666,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -15344,13 +13705,6 @@ "@types/node": "*" } }, - "node_modules/@types/normalize-path": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.2.tgz", - "integrity": "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/oauth": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.4.tgz", @@ -15494,13 +13848,6 @@ "pkcs11js": "*" } }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -15528,16 +13875,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, "node_modules/@types/readable-stream": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", @@ -15710,18 +14047,6 @@ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", @@ -15820,6 +14145,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.20.0", "@typescript-eslint/types": "6.20.0", @@ -16188,167 +14514,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@xmldom/is-dom-node": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz", @@ -16367,20 +14532,6 @@ "node": ">=10.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@yao-pkg/pkg": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.12.0.tgz", @@ -16659,6 +14810,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" }, @@ -16674,19 +14826,6 @@ "acorn": "^8" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -16764,14 +14903,15 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -16794,18 +14934,21 @@ } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/ansi-regex": { "version": "6.0.1", @@ -17002,19 +15145,6 @@ "node": ">=0.8.0" } }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/array-back": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", @@ -17169,6 +15299,7 @@ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "license": "MIT", + "peer": true, "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -17265,42 +15396,15 @@ "node": ">=8.0.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "node_modules/atomically": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", + "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" } }, "node_modules/available-typed-arrays": { @@ -17420,6 +15524,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", @@ -17723,13 +15828,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, "node_modules/botbuilder": { "version": "4.23.2", "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz", @@ -18028,6 +16126,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -18083,13 +16182,6 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -18343,16 +16435,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001748", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", @@ -18513,16 +16595,6 @@ "node": ">=10" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/cipher-base": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.5.tgz", @@ -18660,26 +16732,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -18825,6 +16877,61 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/conf": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz", + "integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/conf/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -19018,49 +17125,6 @@ "resolved": "https://registry.npmjs.org/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz", "integrity": "sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==" }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cssstyle": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", @@ -19237,6 +17301,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -19327,19 +17407,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -19477,26 +17544,12 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "dev": true, - "license": "MIT" - }, "node_modules/dev-null": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -19518,13 +17571,6 @@ "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -19600,10 +17646,43 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", + "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "peer": true, "engines": { "node": ">=12" }, @@ -19750,60 +17829,6 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -20043,6 +18068,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -20125,6 +18151,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -20223,6 +18250,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -20311,6 +18339,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -20768,7 +18797,6 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", @@ -20786,14 +18814,12 @@ "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "peer": true + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, "node_modules/express-session/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -20801,8 +18827,7 @@ "node_modules/express-session/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", @@ -21424,48 +19449,6 @@ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "12.23.22", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", - "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "motion-dom": "^12.23.21", - "motion-utils": "^12.23.6", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -21820,16 +19803,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -21929,13 +19902,6 @@ "node": ">= 6" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -22330,16 +20296,6 @@ "node": ">=0.10.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/helmet": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", @@ -23291,47 +21247,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -23541,13 +21456,6 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -23604,6 +21512,13 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -24105,16 +22020,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -24369,15 +22274,15 @@ } }, "node_modules/marked": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", - "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 16" + "node": ">= 18" } }, "node_modules/math-intrinsics": { @@ -24388,18 +22293,6 @@ "node": ">= 0.4" } }, - "node_modules/md-to-react-email": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz", - "integrity": "sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==", - "license": "MIT", - "dependencies": { - "marked": "7.0.4" - }, - "peerDependencies": { - "react": "^18.0 || ^19.0" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -24433,12 +22326,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -24763,17 +22650,6 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, - "node_modules/module-punycode": { - "name": "punycode", - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -24880,23 +22756,6 @@ "node": ">=16" } }, - "node_modules/motion-dom": { - "version": "12.23.21", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", - "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "motion-utils": "^12.23.6" - } - }, - "node_modules/motion-utils": { - "version": "12.23.6", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", - "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -25103,13 +22962,13 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", - "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", + "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", "dev": true, "license": "MIT", "dependencies": { - "@next/env": "15.5.2", + "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -25119,18 +22978,18 @@ "next": "dist/bin/next" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.2", - "@next/swc-darwin-x64": "15.5.2", - "@next/swc-linux-arm64-gnu": "15.5.2", - "@next/swc-linux-arm64-musl": "15.5.2", - "@next/swc-linux-x64-gnu": "15.5.2", - "@next/swc-linux-x64-musl": "15.5.2", - "@next/swc-win32-arm64-msvc": "15.5.2", - "@next/swc-win32-x64-msvc": "15.5.2", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "16.0.7", + "@next/swc-darwin-x64": "16.0.7", + "@next/swc-linux-arm64-gnu": "16.0.7", + "@next/swc-linux-arm64-musl": "16.0.7", + "@next/swc-linux-x64-gnu": "16.0.7", + "@next/swc-linux-x64-musl": "16.0.7", + "@next/swc-win32-arm64-msvc": "16.0.7", + "@next/swc-win32-x64-msvc": "16.0.7", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -25410,17 +23269,6 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/node-html-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", - "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, "node_modules/node-releases": { "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", @@ -25510,16 +23358,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -25531,19 +23369,6 @@ "set-blocking": "^2.0.0" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/nwsapi": { "version": "2.2.18", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz", @@ -27649,7 +25474,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -28349,6 +26173,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "peer": true, "engines": { "node": ">=10" }, @@ -28682,6 +26507,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -28691,132 +26517,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -28923,6 +26623,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -28945,33 +26646,6 @@ "node": ">=6.0.0" } }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -29413,7 +27087,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -29509,6 +27182,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -29518,6 +27192,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -29526,9 +27201,9 @@ } }, "node_modules/react-email": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.0.tgz", - "integrity": "sha512-XFHCSfhdlO7k5q2TYGwC0HsVh5Yn13YaOdahuJEUEOfOJKHEpSP4PKg7R/RiKFoK9cDvzunhY+58pXxz0vE2zA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.6.tgz", + "integrity": "sha512-DEGzWpEiC3CquPEaaEJuipNT3WZ9mK58rbkpOe4Slbgyf60PLa1wONnt5a3afbBBRbNdW2aYhIvVI41yS6UIRA==", "dev": true, "license": "MIT", "dependencies": { @@ -29536,6 +27211,7 @@ "@babel/traverse": "^7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", + "conf": "^15.0.2", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", @@ -29553,7 +27229,7 @@ "email": "dist/index.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/react-email/node_modules/chokidar": { @@ -29889,113 +27565,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-promise-suspense": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", - "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1" - } - }, - "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "license": "MIT" - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/read-cache/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -30583,26 +28152,6 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/scim-patch": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz", @@ -30649,9 +28198,9 @@ "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -30739,16 +28288,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -30831,16 +28370,17 @@ } }, "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -30849,28 +28389,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -31323,40 +28865,6 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -31412,6 +28920,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -31469,17 +28978,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/sonner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", - "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -31498,23 +28996,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spamc": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/spamc/-/spamc-0.0.5.tgz", - "integrity": "sha512-jYXItuZuiWZyG9fIdvgTUbp2MNRuyhuSwvvhhpPJd4JK/9oSZxkD7zAj53GJtowSlXwCJzLg6sCKAoE9wXsKgg==", - "dev": true - }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -31635,29 +29116,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stacktrace-parser": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -31952,6 +29410,23 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -32143,105 +29618,25 @@ "node": ">=12.17" } }, - "node_modules/tailwind-merge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", - "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "dev": true, "license": "MIT", + "engines": { + "node": ">=20" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "license": "MIT" }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/tailwindcss/node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -32440,78 +29835,6 @@ "node": ">= 6" } }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -33096,6 +30419,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -33273,6 +30597,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -33313,7 +30638,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "peer": true, "dependencies": { "random-bytes": "~1.0.0" }, @@ -33326,6 +30650,19 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -33525,64 +30862,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-debounce": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", - "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -33946,6 +31225,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -34026,118 +31306,11 @@ "node": ">=18" } }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -34172,6 +31345,13 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -34579,15 +31759,6 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xpath": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", @@ -34736,6 +31907,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/backend/package.json b/backend/package.json index 4f9cfdc97..433e7a9b0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -91,7 +91,7 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", - "@react-email/preview-server": "^4.3.0", + "@react-email/preview-server": "^5.0.6", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -129,7 +129,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "^4.3.0", + "react-email": "^5.0.6", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -184,7 +184,7 @@ "@opentelemetry/semantic-conventions": "^1.27.0", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.12.1", - "@react-email/components": "0.0.36", + "@react-email/components": "^1.0.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.2", @@ -267,4 +267,4 @@ "zod": "^3.22.4", "zod-to-json-schema": "^3.24.5" } -} \ No newline at end of file +} diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 02394de4d..be6f45943 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -55,6 +55,7 @@ import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { TApprovalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; @@ -361,6 +362,7 @@ declare module "fastify" { convertor: TConvertorServiceFactory; subOrganization: TSubOrgServiceFactory; pkiAlertV2: TPkiAlertV2ServiceFactory; + approvalPolicy: TApprovalPolicyServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 4bdd3849d..1301000d5 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -26,6 +26,30 @@ import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate, + TApprovalPolicies, + TApprovalPoliciesInsert, + TApprovalPoliciesUpdate, + TApprovalPolicyStepApprovers, + TApprovalPolicyStepApproversInsert, + TApprovalPolicyStepApproversUpdate, + TApprovalPolicySteps, + TApprovalPolicyStepsInsert, + TApprovalPolicyStepsUpdate, + TApprovalRequestApprovals, + TApprovalRequestApprovalsInsert, + TApprovalRequestApprovalsUpdate, + TApprovalRequestGrants, + TApprovalRequestGrantsInsert, + TApprovalRequestGrantsUpdate, + TApprovalRequests, + TApprovalRequestsInsert, + TApprovalRequestStepEligibleApprovers, + TApprovalRequestStepEligibleApproversInsert, + TApprovalRequestStepEligibleApproversUpdate, + TApprovalRequestSteps, + TApprovalRequestStepsInsert, + TApprovalRequestStepsUpdate, + TApprovalRequestsUpdate, TAuditLogs, TAuditLogsInsert, TAuditLogStreams, @@ -573,16 +597,16 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; -import { - TCertificateRequests, - TCertificateRequestsInsert, - TCertificateRequestsUpdate -} from "@app/db/schemas/certificate-requests"; import { TAccessApprovalPoliciesEnvironments, TAccessApprovalPoliciesEnvironmentsInsert, TAccessApprovalPoliciesEnvironmentsUpdate } from "@app/db/schemas/access-approval-policies-environments"; +import { + TCertificateRequests, + TCertificateRequestsInsert, + TCertificateRequestsUpdate +} from "@app/db/schemas/certificate-requests"; import { TIdentityAuthTemplates, TIdentityAuthTemplatesInsert, @@ -1475,5 +1499,45 @@ declare module "knex/types/tables" { TVaultExternalMigrationConfigsInsert, TVaultExternalMigrationConfigsUpdate >; + [TableName.ApprovalPolicies]: KnexOriginal.CompositeTableType< + TApprovalPolicies, + TApprovalPoliciesInsert, + TApprovalPoliciesUpdate + >; + [TableName.ApprovalPolicyStepApprovers]: KnexOriginal.CompositeTableType< + TApprovalPolicyStepApprovers, + TApprovalPolicyStepApproversInsert, + TApprovalPolicyStepApproversUpdate + >; + [TableName.ApprovalPolicySteps]: KnexOriginal.CompositeTableType< + TApprovalPolicySteps, + TApprovalPolicyStepsInsert, + TApprovalPolicyStepsUpdate + >; + [TableName.ApprovalRequestApprovals]: KnexOriginal.CompositeTableType< + TApprovalRequestApprovals, + TApprovalRequestApprovalsInsert, + TApprovalRequestApprovalsUpdate + >; + [TableName.ApprovalRequestGrants]: KnexOriginal.CompositeTableType< + TApprovalRequestGrants, + TApprovalRequestGrantsInsert, + TApprovalRequestGrantsUpdate + >; + [TableName.ApprovalRequestStepEligibleApprovers]: KnexOriginal.CompositeTableType< + TApprovalRequestStepEligibleApprovers, + TApprovalRequestStepEligibleApproversInsert, + TApprovalRequestStepEligibleApproversUpdate + >; + [TableName.ApprovalRequestSteps]: KnexOriginal.CompositeTableType< + TApprovalRequestSteps, + TApprovalRequestStepsInsert, + TApprovalRequestStepsUpdate + >; + [TableName.ApprovalRequests]: KnexOriginal.CompositeTableType< + TApprovalRequests, + TApprovalRequestsInsert, + TApprovalRequestsUpdate + >; } } diff --git a/backend/src/db/migrations/20251203002657_global-approvals.ts b/backend/src/db/migrations/20251203002657_global-approvals.ts new file mode 100644 index 000000000..51610f879 --- /dev/null +++ b/backend/src/db/migrations/20251203002657_global-approvals.ts @@ -0,0 +1,194 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ApprovalPolicies))) { + await knex.schema.createTable(TableName.ApprovalPolicies, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable().index(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + + t.uuid("organizationId").notNullable().index(); + t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.string("type").notNullable().index(); + t.string("name").notNullable(); + + t.boolean("isActive").defaultTo(true); + + t.string("maxRequestTtl").nullable(); // 1hour, 30seconds, etc + + t.jsonb("conditions").notNullable(); + t.jsonb("constraints").notNullable(); + + t.timestamps(true, true, true); + }); + await createOnUpdateTrigger(knex, TableName.ApprovalPolicies); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalPolicySteps))) { + await knex.schema.createTable(TableName.ApprovalPolicySteps, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("policyId").notNullable().index(); + t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("CASCADE"); + + t.string("name").nullable(); + t.integer("stepNumber").notNullable(); + + t.integer("requiredApprovals").notNullable(); + t.boolean("notifyApprovers").defaultTo(false); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalPolicyStepApprovers))) { + await knex.schema.createTable(TableName.ApprovalPolicyStepApprovers, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("policyStepId").notNullable().index(); + t.foreign("policyStepId").references("id").inTable(TableName.ApprovalPolicySteps).onDelete("CASCADE"); + + t.uuid("userId").nullable().index(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + + t.uuid("groupId").nullable().index(); + t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.check('("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)'); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequests))) { + await knex.schema.createTable(TableName.ApprovalRequests, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable().index(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + + t.uuid("organizationId").notNullable().index(); + t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.uuid("policyId").nullable().index(); + t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("SET NULL"); + + t.uuid("requesterId").nullable().index(); + t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + // To be used in the event of requester deletion + t.string("requesterName").notNullable(); + t.string("requesterEmail").notNullable(); + + t.string("type").notNullable().index(); + + t.string("status").notNullable().index(); + t.text("justification").nullable(); + t.integer("currentStep").notNullable(); + + t.jsonb("requestData").notNullable(); + + t.timestamp("expiresAt").nullable(); + t.timestamps(true, true, true); + }); + await createOnUpdateTrigger(knex, TableName.ApprovalRequests); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestSteps))) { + await knex.schema.createTable(TableName.ApprovalRequestSteps, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("requestId").notNullable().index(); + t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("CASCADE"); + + t.integer("stepNumber").notNullable(); + + t.string("name").nullable(); + t.string("status").notNullable().index(); + + t.integer("requiredApprovals").notNullable(); + t.boolean("notifyApprovers").defaultTo(false); + + t.timestamp("startedAt").nullable(); + t.timestamp("completedAt").nullable(); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestStepEligibleApprovers))) { + await knex.schema.createTable(TableName.ApprovalRequestStepEligibleApprovers, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("stepId").notNullable().index(); + t.foreign("stepId").references("id").inTable(TableName.ApprovalRequestSteps).onDelete("CASCADE"); + + t.uuid("userId").nullable().index(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + + t.uuid("groupId").nullable().index(); + t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.check('("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)'); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestApprovals))) { + await knex.schema.createTable(TableName.ApprovalRequestApprovals, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("stepId").notNullable().index(); + t.foreign("stepId").references("id").inTable(TableName.ApprovalRequestSteps).onDelete("CASCADE"); + + t.uuid("approverUserId").notNullable().index(); + t.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + + t.string("decision").notNullable(); + t.text("comment").nullable(); + + t.timestamp("createdAt").defaultTo(knex.fn.now()); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestGrants))) { + await knex.schema.createTable(TableName.ApprovalRequestGrants, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable().index(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + + t.uuid("requestId").nullable().index(); + t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("SET NULL"); + + t.uuid("granteeUserId").nullable().index(); + t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + t.uuid("revokedByUserId").nullable().index(); + t.foreign("revokedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + t.text("revocationReason").nullable(); + + t.string("status").notNullable().index(); + t.string("type").notNullable().index(); + + t.jsonb("attributes").notNullable(); + + t.timestamp("createdAt").defaultTo(knex.fn.now()); + t.timestamp("expiresAt").nullable(); + t.timestamp("revokedAt").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ApprovalRequestGrants); + await knex.schema.dropTableIfExists(TableName.ApprovalRequestApprovals); + await knex.schema.dropTableIfExists(TableName.ApprovalRequestStepEligibleApprovers); + await knex.schema.dropTableIfExists(TableName.ApprovalRequestSteps); + await knex.schema.dropTableIfExists(TableName.ApprovalRequests); + await knex.schema.dropTableIfExists(TableName.ApprovalPolicyStepApprovers); + await knex.schema.dropTableIfExists(TableName.ApprovalPolicySteps); + await knex.schema.dropTableIfExists(TableName.ApprovalPolicies); + + await dropOnUpdateTrigger(knex, TableName.ApprovalRequests); + await dropOnUpdateTrigger(knex, TableName.ApprovalPolicies); +} diff --git a/backend/src/db/migrations/20251203224427_pam-aws-console.ts b/backend/src/db/migrations/20251203224427_pam-aws-console.ts new file mode 100644 index 000000000..adadb9e99 --- /dev/null +++ b/backend/src/db/migrations/20251203224427_pam-aws-console.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasGatewayId = await knex.schema.hasColumn(TableName.PamResource, "gatewayId"); + if (hasGatewayId) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.uuid("gatewayId").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasGatewayId = await knex.schema.hasColumn(TableName.PamResource, "gatewayId"); + if (hasGatewayId) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.uuid("gatewayId").notNullable().alter(); + }); + } +} diff --git a/backend/src/db/schemas/approval-policies.ts b/backend/src/db/schemas/approval-policies.ts new file mode 100644 index 000000000..d7f8fe8da --- /dev/null +++ b/backend/src/db/schemas/approval-policies.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalPoliciesSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + organizationId: z.string().uuid(), + type: z.string(), + name: z.string(), + isActive: z.boolean().default(true).nullable().optional(), + maxRequestTtl: z.string().nullable().optional(), + conditions: z.unknown(), + constraints: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TApprovalPolicies = z.infer; +export type TApprovalPoliciesInsert = Omit, TImmutableDBKeys>; +export type TApprovalPoliciesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-policy-step-approvers.ts b/backend/src/db/schemas/approval-policy-step-approvers.ts new file mode 100644 index 000000000..909d99d15 --- /dev/null +++ b/backend/src/db/schemas/approval-policy-step-approvers.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalPolicyStepApproversSchema = z.object({ + id: z.string().uuid(), + policyStepId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + groupId: z.string().uuid().nullable().optional() +}); + +export type TApprovalPolicyStepApprovers = z.infer; +export type TApprovalPolicyStepApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TApprovalPolicyStepApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/approval-policy-steps.ts b/backend/src/db/schemas/approval-policy-steps.ts new file mode 100644 index 000000000..d4831fa2d --- /dev/null +++ b/backend/src/db/schemas/approval-policy-steps.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalPolicyStepsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + name: z.string().nullable().optional(), + stepNumber: z.number(), + requiredApprovals: z.number(), + notifyApprovers: z.boolean().default(false).nullable().optional() +}); + +export type TApprovalPolicySteps = z.infer; +export type TApprovalPolicyStepsInsert = Omit, TImmutableDBKeys>; +export type TApprovalPolicyStepsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-request-approvals.ts b/backend/src/db/schemas/approval-request-approvals.ts new file mode 100644 index 000000000..25b305d85 --- /dev/null +++ b/backend/src/db/schemas/approval-request-approvals.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestApprovalsSchema = z.object({ + id: z.string().uuid(), + stepId: z.string().uuid(), + approverUserId: z.string().uuid(), + decision: z.string(), + comment: z.string().nullable().optional(), + createdAt: z.date().nullable().optional() +}); + +export type TApprovalRequestApprovals = z.infer; +export type TApprovalRequestApprovalsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestApprovalsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/approval-request-grants.ts b/backend/src/db/schemas/approval-request-grants.ts new file mode 100644 index 000000000..5056a5b1d --- /dev/null +++ b/backend/src/db/schemas/approval-request-grants.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestGrantsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + requestId: z.string().uuid().nullable().optional(), + granteeUserId: z.string().uuid().nullable().optional(), + revokedByUserId: z.string().uuid().nullable().optional(), + revocationReason: z.string().nullable().optional(), + status: z.string(), + type: z.string(), + attributes: z.unknown(), + createdAt: z.date().nullable().optional(), + expiresAt: z.date().nullable().optional(), + revokedAt: z.date().nullable().optional() +}); + +export type TApprovalRequestGrants = z.infer; +export type TApprovalRequestGrantsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestGrantsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-request-step-eligible-approvers.ts b/backend/src/db/schemas/approval-request-step-eligible-approvers.ts new file mode 100644 index 000000000..987861e6b --- /dev/null +++ b/backend/src/db/schemas/approval-request-step-eligible-approvers.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestStepEligibleApproversSchema = z.object({ + id: z.string().uuid(), + stepId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + groupId: z.string().uuid().nullable().optional() +}); + +export type TApprovalRequestStepEligibleApprovers = z.infer; +export type TApprovalRequestStepEligibleApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TApprovalRequestStepEligibleApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/approval-request-steps.ts b/backend/src/db/schemas/approval-request-steps.ts new file mode 100644 index 000000000..7b5233601 --- /dev/null +++ b/backend/src/db/schemas/approval-request-steps.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestStepsSchema = z.object({ + id: z.string().uuid(), + requestId: z.string().uuid(), + stepNumber: z.number(), + name: z.string().nullable().optional(), + status: z.string(), + requiredApprovals: z.number(), + notifyApprovers: z.boolean().default(false).nullable().optional(), + startedAt: z.date().nullable().optional(), + completedAt: z.date().nullable().optional() +}); + +export type TApprovalRequestSteps = z.infer; +export type TApprovalRequestStepsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestStepsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-requests.ts b/backend/src/db/schemas/approval-requests.ts new file mode 100644 index 000000000..c5d53fdf8 --- /dev/null +++ b/backend/src/db/schemas/approval-requests.ts @@ -0,0 +1,30 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + organizationId: z.string().uuid(), + policyId: z.string().uuid().nullable().optional(), + requesterId: z.string().uuid().nullable().optional(), + requesterName: z.string(), + requesterEmail: z.string(), + type: z.string(), + status: z.string(), + justification: z.string().nullable().optional(), + currentStep: z.number(), + requestData: z.unknown(), + expiresAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TApprovalRequests = z.infer; +export type TApprovalRequestsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 7db6e847d..528582c59 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -6,6 +6,14 @@ export * from "./access-approval-requests-reviewers"; export * from "./additional-privileges"; export * from "./api-keys"; export * from "./app-connections"; +export * from "./approval-policies"; +export * from "./approval-policy-step-approvers"; +export * from "./approval-policy-steps"; +export * from "./approval-request-approvals"; +export * from "./approval-request-grants"; +export * from "./approval-request-step-eligible-approvers"; +export * from "./approval-request-steps"; +export * from "./approval-requests"; export * from "./audit-log-streams"; export * from "./audit-logs"; export * from "./auth-token-sessions"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 040d6e278..fe38a9c9b 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -223,7 +223,17 @@ export enum TableName { PkiAcmeOrder = "pki_acme_orders", PkiAcmeOrderAuth = "pki_acme_order_auths", PkiAcmeAuth = "pki_acme_auths", - PkiAcmeChallenge = "pki_acme_challenges" + PkiAcmeChallenge = "pki_acme_challenges", + + // Approval Policies + ApprovalPolicies = "approval_policies", + ApprovalPolicySteps = "approval_policy_steps", + ApprovalPolicyStepApprovers = "approval_policy_step_approvers", + ApprovalRequests = "approval_requests", + ApprovalRequestSteps = "approval_request_steps", + ApprovalRequestStepEligibleApprovers = "approval_request_step_eligible_approvers", + ApprovalRequestApprovals = "approval_request_approvals", + ApprovalRequestGrants = "approval_request_grants" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/db/schemas/pam-resources.ts b/backend/src/db/schemas/pam-resources.ts index 325f6eddc..f59aae5d8 100644 --- a/backend/src/db/schemas/pam-resources.ts +++ b/backend/src/db/schemas/pam-resources.ts @@ -13,7 +13,7 @@ export const PamResourcesSchema = z.object({ id: z.string().uuid(), projectId: z.string(), name: z.string(), - gatewayId: z.string().uuid(), + gatewayId: z.string().uuid().nullable().optional(), resourceType: z.string(), encryptedConnectionDetails: zodBuffer, createdAt: z.date(), diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts index a48e28e3d..b46b525fe 100644 --- a/backend/src/ee/routes/v1/external-kms-router.ts +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -4,15 +4,10 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ExternalKmsAwsSchema, - ExternalKmsGcpCredentialSchema, ExternalKmsGcpSchema, ExternalKmsInputSchema, - ExternalKmsInputUpdateSchema, - KmsGcpKeyFetchAuthType, - KmsProviders, - TExternalKmsGcpCredentialSchema + ExternalKmsInputUpdateSchema } from "@app/ee/services/external-kms/providers/model"; -import { NotFoundError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -293,67 +288,4 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { return { externalKms }; } }); - - server.route({ - method: "POST", - url: "/gcp/keys", - config: { - rateLimit: writeLimit - }, - schema: { - body: z.discriminatedUnion("authMethod", [ - z.object({ - authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential), - region: z.string().trim().min(1), - credential: ExternalKmsGcpCredentialSchema - }), - z.object({ - authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms), - region: z.string().trim().min(1), - kmsId: z.string().trim().min(1) - }) - ]), - response: { - 200: z.object({ - keys: z.string().array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const { region, authMethod } = req.body; - let credentialJson: TExternalKmsGcpCredentialSchema | undefined; - - if (authMethod === KmsGcpKeyFetchAuthType.Credential) { - credentialJson = req.body.credential; - } else if (authMethod === KmsGcpKeyFetchAuthType.Kms) { - const externalKms = await server.services.externalKms.findById({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - id: req.body.kmsId - }); - - if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) { - throw new NotFoundError({ message: "KMS not found or not of type GCP" }); - } - - credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema; - } - - if (!credentialJson) { - throw new NotFoundError({ - message: "Something went wrong while fetching the GCP credential, please check inputs and try again" - }); - } - - const results = await server.services.externalKms.fetchGcpKeys({ - credential: credentialJson, - gcpRegion: region - }); - - return results; - } - }); }; diff --git a/backend/src/ee/routes/v1/external-kms-routers/aws-kms-router.ts b/backend/src/ee/routes/v1/external-kms-routers/aws-kms-router.ts new file mode 100644 index 000000000..518b7947e --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/aws-kms-router.ts @@ -0,0 +1,12 @@ +import { ExternalKmsAwsSchema, KmsProviders } from "@app/ee/services/external-kms/providers/model"; + +import { registerExternalKmsEndpoints } from "./external-kms-endpoints"; + +export const registerAwsKmsRouter = async (server: FastifyZodProvider) => { + registerExternalKmsEndpoints({ + server, + provider: KmsProviders.Aws, + createSchema: ExternalKmsAwsSchema, + updateSchema: ExternalKmsAwsSchema.partial() + }); +}; diff --git a/backend/src/ee/routes/v1/external-kms-routers/external-kms-endpoints.ts b/backend/src/ee/routes/v1/external-kms-routers/external-kms-endpoints.ts new file mode 100644 index 000000000..47b4947f2 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/external-kms-endpoints.ts @@ -0,0 +1,288 @@ +import { z } from "zod"; + +import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + KmsProviders, + SanitizedExternalKmsAwsSchema, + SanitizedExternalKmsGcpSchema, + TExternalKmsInputSchema, + TExternalKmsInputUpdateSchema +} from "@app/ee/services/external-kms/providers/model"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError } from "@app/lib/errors"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedExternalSchema = KmsKeysSchema.extend({ + externalKms: ExternalKmsSchema.pick({ + id: true, + status: true, + statusDetails: true, + provider: true + }).extend({ + configuration: z.union([SanitizedExternalKmsAwsSchema, SanitizedExternalKmsGcpSchema]), + credentialsHash: z.string().optional() + }) +}); + +export const registerExternalKmsEndpoints = < + T extends { type: KmsProviders; inputs: TExternalKmsInputSchema["inputs"] } +>({ + server, + provider, + createSchema, + updateSchema +}: { + server: FastifyZodProvider; + provider: T["type"]; + createSchema: z.ZodType; + updateSchema: z.ZodType>; +}) => { + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + // Validate that the KMS is of the expected provider type + if (externalKms.external.provider !== provider) { + throw new BadRequestError({ + message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}` + }); + } + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_KMS, + metadata: { + kmsId: externalKms.id, + name: externalKms.name + } + } + }); + + const { + external: { providerInput: configuration, ...externalKmsData }, + ...rest + } = externalKms; + + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + name: z.string().min(1).trim().toLowerCase(), + description: z.string().trim().optional(), + configuration: createSchema + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { name, description, configuration } = req.body as { + name: string; + description?: string; + configuration: T["inputs"]; + }; + + const providerInput = { + type: provider, + inputs: configuration + } as TExternalKmsInputSchema; + + const externalKms = await server.services.externalKms.create({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + name, + provider: providerInput, + description + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_KMS, + metadata: { + kmsId: externalKms.id, + provider, + name, + description + } + } + }); + + const { + external: { providerInput: externalKmsConfiguration, ...externalKmsData }, + ...rest + } = externalKms; + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } }; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + body: z.object({ + name: z.string().min(1).trim().toLowerCase().optional(), + description: z.string().trim().optional(), + configuration: updateSchema.optional() + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { name, description, configuration } = req.body as { + name?: string; + description?: string; + configuration: Partial; + }; + + const providerInput = { + type: provider, + inputs: configuration + } as TExternalKmsInputUpdateSchema; + + const externalKms = await server.services.externalKms.updateById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + name, + provider: providerInput, + description, + id: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_KMS, + metadata: { + kmsId: externalKms.id, + provider, + name, + description + } + } + }); + + const { + external: { providerInput: externalKmsConfiguration, ...externalKmsData }, + ...rest + } = externalKms; + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: sanitizedExternalSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.deleteById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + // Validate that the KMS is of the expected provider type + if (externalKms.external.provider !== provider) { + throw new BadRequestError({ + message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}` + }); + } + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.DELETE_KMS, + metadata: { + kmsId: externalKms.id, + name: externalKms.name + } + } + }); + + const { + external: { providerInput: configuration, ...externalKmsData }, + ...rest + } = externalKms; + const credentialsHash = crypto.nativeCrypto + .createHash("sha256") + .update(externalKmsData.encryptedProviderInputs) + .digest("hex"); + + return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/external-kms-routers/gcp-kms-router.ts b/backend/src/ee/routes/v1/external-kms-routers/gcp-kms-router.ts new file mode 100644 index 000000000..97b600c10 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/gcp-kms-router.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; + +import { + ExternalKmsGcpCredentialSchema, + ExternalKmsGcpSchema, + KmsGcpKeyFetchAuthType, + KmsProviders, + TExternalKmsGcpCredentialSchema +} from "@app/ee/services/external-kms/providers/model"; +import { NotFoundError } from "@app/lib/errors"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerExternalKmsEndpoints } from "./external-kms-endpoints"; + +export const registerGcpKmsRouter = async (server: FastifyZodProvider) => { + registerExternalKmsEndpoints({ + server, + provider: KmsProviders.Gcp, + createSchema: ExternalKmsGcpSchema, + updateSchema: ExternalKmsGcpSchema.partial() + }); + + server.route({ + method: "POST", + url: "/keys", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.discriminatedUnion("authMethod", [ + z.object({ + authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential), + region: z.string().trim().min(1), + credential: ExternalKmsGcpCredentialSchema + }), + z.object({ + authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms), + region: z.string().trim().min(1), + kmsId: z.string().trim().min(1) + }) + ]), + response: { + 200: z.object({ + keys: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { region, authMethod } = req.body; + let credentialJson: TExternalKmsGcpCredentialSchema | undefined; + + if (authMethod === KmsGcpKeyFetchAuthType.Credential && "credential" in req.body) { + credentialJson = req.body.credential; + } else if (authMethod === KmsGcpKeyFetchAuthType.Kms && "kmsId" in req.body) { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.kmsId + }); + + if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) { + throw new NotFoundError({ message: "KMS not found or not of type GCP" }); + } + + const providerInput = externalKms.external.providerInput as { credential: TExternalKmsGcpCredentialSchema }; + credentialJson = providerInput.credential; + } + + if (!credentialJson) { + throw new NotFoundError({ + message: "Something went wrong while fetching the GCP credential, please check inputs and try again" + }); + } + + const results = await server.services.externalKms.fetchGcpKeys({ + credential: credentialJson, + gcpRegion: region + }); + + return results; + } + }); +}; diff --git a/backend/src/ee/routes/v1/external-kms-routers/index.ts b/backend/src/ee/routes/v1/external-kms-routers/index.ts new file mode 100644 index 000000000..da70b0f59 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-routers/index.ts @@ -0,0 +1,9 @@ +import { KmsProviders } from "@app/ee/services/external-kms/providers/model"; + +import { registerAwsKmsRouter } from "./aws-kms-router"; +import { registerGcpKmsRouter } from "./gcp-kms-router"; + +export const EXTERNAL_KMS_REGISTER_ROUTER_MAP: Record Promise> = { + [KmsProviders.Aws]: registerAwsKmsRouter, + [KmsProviders.Gcp]: registerGcpKmsRouter +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 367c2833c..0e34ebcd4 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -12,6 +12,7 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router" import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; +import { EXTERNAL_KMS_REGISTER_ROUTER_MAP } from "./external-kms-routers"; import { registerGatewayRouter } from "./gateway-router"; import { registerGithubOrgSyncRouter } from "./github-org-sync-router"; import { registerGroupRouter } from "./group-router"; @@ -162,9 +163,19 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { { prefix: "/additional-privilege" } ); - await server.register(registerExternalKmsRouter, { - prefix: "/external-kms" - }); + await server.register( + async (externalKmsRouter) => { + await externalKmsRouter.register(registerExternalKmsRouter); + + // Provider-specific endpoints + await Promise.all( + Object.entries(EXTERNAL_KMS_REGISTER_ROUTER_MAP).map(([provider, router]) => + externalKmsRouter.register(router, { prefix: `/${provider}` }) + ) + ); + }, + { prefix: "/external-kms" } + ); await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); diff --git a/backend/src/ee/routes/v1/pam-account-routers/index.ts b/backend/src/ee/routes/v1/pam-account-routers/index.ts index d3aadd5a4..6b6a4cbed 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/index.ts @@ -1,3 +1,8 @@ +import { + CreateAwsIamAccountSchema, + SanitizedAwsIamAccountWithResourceSchema, + UpdateAwsIamAccountSchema +} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { CreateMySQLAccountSchema, SanitizedMySQLAccountWithResourceSchema, @@ -44,5 +49,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.AwsIam, + accountResponseSchema: SanitizedAwsIamAccountWithResourceSchema, + createAccountSchema: CreateAwsIamAccountSchema, + updateAccountSchema: UpdateAwsIamAccountSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts index 44e2a5ea1..4043c3cc8 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts @@ -22,7 +22,7 @@ export const registerPamResourceEndpoints = ({ folderId?: C["folderId"]; name: C["name"]; description?: C["description"]; - rotationEnabled: C["rotationEnabled"]; + rotationEnabled?: C["rotationEnabled"]; rotationIntervalSeconds?: C["rotationIntervalSeconds"]; }>; updateAccountSchema: z.ZodType<{ @@ -65,7 +65,7 @@ export const registerPamResourceEndpoints = ({ folderId: req.body.folderId, name: req.body.name, description: req.body.description, - rotationEnabled: req.body.rotationEnabled, + rotationEnabled: req.body.rotationEnabled ?? false, rotationIntervalSeconds: req.body.rotationIntervalSeconds } } diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 0b7f89b6c..0c78b6449 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -3,8 +3,10 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-enums"; +import { SanitizedAwsIamAccountWithResourceSchema } from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { GatewayAccessResponseSchema } from "@app/ee/services/pam-resource/pam-resource-schemas"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { SanitizedSSHAccountWithResourceSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; import { BadRequestError } from "@app/lib/errors"; @@ -18,9 +20,12 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SanitizedAccountSchema = z.union([ SanitizedSSHAccountWithResourceSchema, // ORDER MATTERS SanitizedPostgresAccountWithResourceSchema, - SanitizedMySQLAccountWithResourceSchema + SanitizedMySQLAccountWithResourceSchema, + SanitizedAwsIamAccountWithResourceSchema ]); +type TSanitizedAccount = z.infer; + export const registerPamAccountRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", @@ -93,7 +98,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { } }); - return { accounts, folders, totalCount, folderId, folderPaths }; + return { accounts: accounts as TSanitizedAccount[], folders, totalCount, folderId, folderPaths }; } }); @@ -106,7 +111,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { schema: { description: "Access PAM account", body: z.object({ - accountId: z.string().uuid(), + accountPath: z.string().trim(), + projectId: z.string().uuid(), duration: z .string() .min(1) @@ -124,18 +130,19 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { }) }), response: { - 200: z.object({ - sessionId: z.string(), - resourceType: z.nativeEnum(PamResource), - relayClientCertificate: z.string(), - relayClientPrivateKey: z.string(), - relayServerCertificateChain: z.string(), - gatewayClientCertificate: z.string(), - gatewayClientPrivateKey: z.string(), - gatewayServerCertificateChain: z.string(), - relayHost: z.string(), - metadata: z.record(z.string(), z.string().optional()).optional() - }) + 200: z.discriminatedUnion("resourceType", [ + // Gateway-based resources (Postgres, MySQL, SSH) + GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.Postgres) }), + GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.MySQL) }), + GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.SSH) }), + // AWS IAM (no gateway, returns console URL) + z.object({ + sessionId: z.string(), + resourceType: z.literal(PamResource.AwsIam), + consoleUrl: z.string().url(), + metadata: z.record(z.string(), z.string().optional()).optional() + }) + ]) } }, onRequest: verifyAuth([AuthMode.JWT]), @@ -151,7 +158,9 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { actorIp: req.realIp, actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(), actorUserAgent: req.auditLogInfo.userAgent ?? "", - ...req.body + accountPath: req.body.accountPath, + projectId: req.body.projectId, + duration: req.body.duration }, req.permission ); @@ -159,11 +168,12 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: response.projectId, + projectId: req.body.projectId, event: { type: EventType.PAM_ACCOUNT_ACCESS, metadata: { - accountId: req.body.accountId, + accountId: response.account.id, + accountPath: req.body.accountPath, accountName: response.account.name, duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined } diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index 5dae317da..fcd9840b4 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -1,3 +1,8 @@ +import { + CreateAwsIamResourceSchema, + SanitizedAwsIamResourceSchema, + UpdateAwsIamResourceSchema +} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { CreateMySQLResourceSchema, MySQLResourceSchema, @@ -44,5 +49,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.AwsIam, + resourceResponseSchema: SanitizedAwsIamResourceSchema, + createResourceSchema: CreateAwsIamResourceSchema, + updateResourceSchema: UpdateAwsIamResourceSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts index ffbeae5c0..e3803316e 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts @@ -19,7 +19,7 @@ export const registerPamResourceEndpoints = ({ createResourceSchema: z.ZodType<{ projectId: T["projectId"]; connectionDetails: T["connectionDetails"]; - gatewayId: T["gatewayId"]; + gatewayId?: T["gatewayId"]; name: T["name"]; rotationAccountCredentials?: T["rotationAccountCredentials"]; }>; @@ -103,7 +103,7 @@ export const registerPamResourceEndpoints = ({ type: EventType.PAM_RESOURCE_CREATE, metadata: { resourceType, - gatewayId: req.body.gatewayId, + ...(req.body.gatewayId && { gatewayId: req.body.gatewayId }), name: req.body.name } } @@ -150,8 +150,8 @@ export const registerPamResourceEndpoints = ({ metadata: { resourceId: req.params.resourceId, resourceType, - gatewayId: req.body.gatewayId, - name: req.body.name + ...(req.body.gatewayId && { gatewayId: req.body.gatewayId }), + ...(req.body.name && { name: req.body.name }) } } }); diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index 3536e7a99..b6a7532ed 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + AwsIamResourceListItemSchema, + SanitizedAwsIamResourceSchema +} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { MySQLResourceListItemSchema, SanitizedMySQLResourceSchema @@ -22,13 +26,15 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SanitizedResourceSchema = z.union([ SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema, - SanitizedSSHResourceSchema + SanitizedSSHResourceSchema, + SanitizedAwsIamResourceSchema ]); const ResourceOptionsSchema = z.discriminatedUnion("resource", [ PostgresResourceListItemSchema, MySQLResourceListItemSchema, - SSHResourceListItemSchema + SSHResourceListItemSchema, + AwsIamResourceListItemSchema ]); export const registerPamResourceRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index acf34cb3b..f1ee79481 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -315,6 +315,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { memberships: z .object({ id: z.string(), + actorGroupId: z.string().nullish(), + actorUserId: z.string().nullish(), roles: z .object({ role: z.string() diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index 47b3f5258..23ba27b8a 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -84,7 +84,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: req.body.identityId, - projectMembershipId: req.body.projectId, projectId: req.body.projectId, slug: privilege.name } @@ -168,7 +167,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: privilegeDoc.actorIdentityId as string, - projectMembershipId: privilegeDoc.projectId as string, projectId: privilegeDoc.projectId as string, slug: privilege.name } @@ -222,7 +220,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: privilegeDoc.actorIdentityId as string, - projectMembershipId: privilegeDoc.projectId as string, projectId: privilegeDoc.projectId as string, slug: privilege.name } @@ -276,7 +273,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: privilegeDoc.actorIdentityId as string, - projectMembershipId: privilegeDoc.projectId as string, projectId: privilegeDoc.projectId as string, slug: privilege.name } @@ -339,7 +335,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: req.query.identityId, - projectMembershipId: privilege.projectId as string, projectId, slug: privilege.name } @@ -391,7 +386,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privileges: privileges.map((privilege) => ({ ...privilege, identityId: req.query.identityId, - projectMembershipId: privilege.projectId as string, projectId: req.query.projectId, slug: privilege.name })) diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 8d1702850..d58f3c2f7 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -4,6 +4,7 @@ import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-r import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router"; import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router"; import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; +import { registerMongoDBCredentialsRotationRouter } from "./mongodb-credentials-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router"; import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router"; @@ -26,5 +27,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter, [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter, - [SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter + [SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter, + [SecretRotation.MongoDBCredentials]: registerMongoDBCredentialsRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/mongodb-credentials-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/mongodb-credentials-rotation-router.ts new file mode 100644 index 000000000..0b41f24f9 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/mongodb-credentials-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateMongoDBCredentialsRotationSchema, + MongoDBCredentialsRotationGeneratedCredentialsSchema, + MongoDBCredentialsRotationSchema, + UpdateMongoDBCredentialsRotationSchema +} from "@app/ee/services/secret-rotation-v2/mongodb-credentials"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerMongoDBCredentialsRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.MongoDBCredentials, + server, + responseSchema: MongoDBCredentialsRotationSchema, + createSchema: CreateMongoDBCredentialsRotationSchema, + updateSchema: UpdateMongoDBCredentialsRotationSchema, + generatedCredentialsSchema: MongoDBCredentialsRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index 6ea6497e4..53346657b 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -5,6 +5,7 @@ import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret"; import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret"; import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; +import { MongoDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mongodb-credentials"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; @@ -27,7 +28,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ AwsIamUserSecretRotationListItemSchema, LdapPasswordRotationListItemSchema, OktaClientSecretRotationListItemSchema, - RedisCredentialsRotationListItemSchema + RedisCredentialsRotationListItemSchema, + MongoDBCredentialsRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index f6cb7859a..b89137ec6 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -560,7 +560,21 @@ export enum EventType { PAM_RESOURCE_GET = "pam-resource-get", PAM_RESOURCE_CREATE = "pam-resource-create", PAM_RESOURCE_UPDATE = "pam-resource-update", - PAM_RESOURCE_DELETE = "pam-resource-delete" + PAM_RESOURCE_DELETE = "pam-resource-delete", + APPROVAL_POLICY_CREATE = "approval-policy-create", + APPROVAL_POLICY_UPDATE = "approval-policy-update", + APPROVAL_POLICY_DELETE = "approval-policy-delete", + APPROVAL_POLICY_LIST = "approval-policy-list", + APPROVAL_POLICY_GET = "approval-policy-get", + APPROVAL_REQUEST_GET = "approval-request-get", + APPROVAL_REQUEST_LIST = "approval-request-list", + APPROVAL_REQUEST_CREATE = "approval-request-create", + APPROVAL_REQUEST_APPROVE = "approval-request-approve", + APPROVAL_REQUEST_REJECT = "approval-request-reject", + APPROVAL_REQUEST_CANCEL = "approval-request-cancel", + APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list", + APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get", + APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke" } export const filterableSecretEvents: EventType[] = [ @@ -4086,6 +4100,7 @@ interface PamAccountAccessEvent { type: EventType.PAM_ACCOUNT_ACCESS; metadata: { accountId: string; + accountPath: string; accountName: string; duration?: string; }; @@ -4168,7 +4183,7 @@ interface PamResourceCreateEvent { type: EventType.PAM_RESOURCE_CREATE; metadata: { resourceType: string; - gatewayId: string; + gatewayId?: string; name: string; }; } @@ -4233,6 +4248,126 @@ interface GetCertificateFromRequestEvent { }; } +interface ApprovalPolicyCreateEvent { + type: EventType.APPROVAL_POLICY_CREATE; + metadata: { + policyType: string; + name: string; + }; +} + +interface ApprovalPolicyUpdateEvent { + type: EventType.APPROVAL_POLICY_UPDATE; + metadata: { + policyType: string; + policyId: string; + name: string; + }; +} + +interface ApprovalPolicyDeleteEvent { + type: EventType.APPROVAL_POLICY_DELETE; + metadata: { + policyType: string; + policyId: string; + }; +} + +interface ApprovalPolicyListEvent { + type: EventType.APPROVAL_POLICY_LIST; + metadata: { + policyType: string; + count: number; + }; +} + +interface ApprovalPolicyGetEvent { + type: EventType.APPROVAL_POLICY_GET; + metadata: { + policyType: string; + policyId: string; + name: string; + }; +} + +interface ApprovalRequestGetEvent { + type: EventType.APPROVAL_REQUEST_GET; + metadata: { + policyType: string; + requestId: string; + status: string; + }; +} + +interface ApprovalRequestListEvent { + type: EventType.APPROVAL_REQUEST_LIST; + metadata: { + policyType: string; + count: number; + }; +} + +interface ApprovalRequestCreateEvent { + type: EventType.APPROVAL_REQUEST_CREATE; + metadata: { + policyType: string; + justification?: string; + requestDuration: string; + }; +} + +interface ApprovalRequestApproveEvent { + type: EventType.APPROVAL_REQUEST_APPROVE; + metadata: { + policyType: string; + requestId: string; + comment?: string; + }; +} + +interface ApprovalRequestRejectEvent { + type: EventType.APPROVAL_REQUEST_REJECT; + metadata: { + policyType: string; + requestId: string; + comment?: string; + }; +} + +interface ApprovalRequestCancelEvent { + type: EventType.APPROVAL_REQUEST_CANCEL; + metadata: { + policyType: string; + requestId: string; + }; +} + +interface ApprovalRequestGrantListEvent { + type: EventType.APPROVAL_REQUEST_GRANT_LIST; + metadata: { + policyType: string; + count: number; + }; +} + +interface ApprovalRequestGrantGetEvent { + type: EventType.APPROVAL_REQUEST_GRANT_GET; + metadata: { + policyType: string; + grantId: string; + status: string; + }; +} + +interface ApprovalRequestGrantRevokeEvent { + type: EventType.APPROVAL_REQUEST_GRANT_REVOKE; + metadata: { + policyType: string; + grantId: string; + revocationReason?: string; + }; +} + export type Event = | CreateSubOrganizationEvent | UpdateSubOrganizationEvent @@ -4619,4 +4754,18 @@ export type Event = | AutomatedRenewCertificateFailed | UserLoginEvent | SelectOrganizationEvent - | SelectSubOrganizationEvent; + | SelectSubOrganizationEvent + | ApprovalPolicyCreateEvent + | ApprovalPolicyUpdateEvent + | ApprovalPolicyDeleteEvent + | ApprovalPolicyListEvent + | ApprovalPolicyGetEvent + | ApprovalRequestGetEvent + | ApprovalRequestListEvent + | ApprovalRequestCreateEvent + | ApprovalRequestApproveEvent + | ApprovalRequestRejectEvent + | ApprovalRequestCancelEvent + | ApprovalRequestGrantListEvent + | ApprovalRequestGrantGetEvent + | ApprovalRequestGrantRevokeEvent; diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index 9614f3298..eb595ee02 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -24,7 +24,13 @@ import { } from "./external-kms-types"; import { AwsKmsProviderFactory } from "./providers/aws-kms"; import { GcpKmsProviderFactory } from "./providers/gcp-kms"; -import { ExternalKmsAwsSchema, ExternalKmsGcpSchema, KmsProviders, TExternalKmsGcpSchema } from "./providers/model"; +import { + ExternalKmsAwsSchema, + ExternalKmsGcpSchema, + KmsProviders, + TExternalKmsAwsSchema, + TExternalKmsGcpSchema +} from "./providers/model"; type TExternalKmsServiceFactoryDep = { externalKmsDAL: TExternalKmsDALFactory; @@ -72,6 +78,7 @@ export const externalKmsServiceFactory = ({ const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); let sanitizedProviderInput = ""; + let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema; switch (provider.type) { case KmsProviders.Aws: { @@ -88,9 +95,18 @@ export const externalKmsServiceFactory = ({ try { // if missing kms key this generate a new kms key id and returns new provider input const newProviderInput = await externalKms.generateInputKmsKey(); + sanitizedProviderInputObject = newProviderInput; sanitizedProviderInput = JSON.stringify(newProviderInput); await externalKms.validateConnection(); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection" + }); } finally { await externalKms.cleanup(); } @@ -101,7 +117,16 @@ export const externalKmsServiceFactory = ({ const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs }); try { await externalKms.validateConnection(); + sanitizedProviderInputObject = provider.inputs; sanitizedProviderInput = JSON.stringify(provider.inputs); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection" + }); } finally { await externalKms.cleanup(); } @@ -139,7 +164,10 @@ export const externalKmsServiceFactory = ({ }, tx ); - return { ...kms, external: externalKmsCfg }; + return { + ...kms, + external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } + }; }); return externalKms; @@ -179,6 +207,7 @@ export const externalKmsServiceFactory = ({ if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` }); let sanitizedProviderInput = ""; + let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema; const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -199,7 +228,16 @@ export const externalKmsServiceFactory = ({ const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); try { await externalKms.validateConnection(); + sanitizedProviderInputObject = updatedProviderInput; sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection" + }); } finally { await externalKms.cleanup(); } @@ -214,7 +252,16 @@ export const externalKmsServiceFactory = ({ const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput }); try { await externalKms.validateConnection(); + sanitizedProviderInputObject = updatedProviderInput; sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection" + }); } finally { await externalKms.cleanup(); } @@ -234,14 +281,17 @@ export const externalKmsServiceFactory = ({ } const externalKms = await externalKmsDAL.transaction(async (tx) => { - const kms = await kmsDAL.updateById( - kmsDoc.id, - { - description, - name: kmsName - }, - tx - ); + let kms = kmsDoc; + if (kmsName || description) { + kms = await kmsDAL.updateById( + kmsDoc.id, + { + description, + name: kmsName + }, + tx + ); + } if (encryptedProviderInputs) { const externalKmsCfg = await externalKmsDAL.updateById( externalKmsDoc.id, @@ -250,9 +300,9 @@ export const externalKmsServiceFactory = ({ }, tx ); - return { ...kms, external: externalKmsCfg }; + return { ...kms, external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } }; } - return { ...kms, external: externalKmsDoc }; + return { ...kms, external: { ...externalKmsDoc, providerInput: sanitizedProviderInputObject } }; }); return externalKms; @@ -273,9 +323,40 @@ export const externalKmsServiceFactory = ({ const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` }); + let decryptedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema; + + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const decryptedProviderInputBlob = orgDataKeyDecryptor({ + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + + switch (externalKmsDoc.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + decryptedProviderInputObject = decryptedProviderInput; + break; + } + case KmsProviders.Gcp: { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + + decryptedProviderInputObject = decryptedProviderInput; + break; + } + default: + break; + } + const externalKms = await externalKmsDAL.transaction(async (tx) => { const kms = await kmsDAL.deleteById(kmsDoc.id, tx); - return { ...kms, external: externalKmsDoc }; + return { ...kms, external: { ...externalKmsDoc, providerInput: decryptedProviderInputObject } }; }); return externalKms; @@ -393,6 +474,14 @@ export const externalKmsServiceFactory = ({ const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } }); try { return await externalKms.getKeysList(); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to fetch GCP keys" + }); } finally { await externalKms.cleanup(); } diff --git a/backend/src/ee/services/external-kms/providers/aws-kms.ts b/backend/src/ee/services/external-kms/providers/aws-kms.ts index 2c248992f..82e95f360 100644 --- a/backend/src/ee/services/external-kms/providers/aws-kms.ts +++ b/backend/src/ee/services/external-kms/providers/aws-kms.ts @@ -3,6 +3,7 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; import { CustomAWSHasher } from "@app/lib/aws/hashing"; import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError } from "@app/lib/errors"; import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model"; @@ -22,7 +23,7 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => { }); const response = await stsClient.send(command); if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey) - throw new Error("Failed to assume role"); + throw new BadRequestError({ message: "Failed to assume role" }); const kmsClient = new KMSClient({ region: providerInputs.awsRegion, @@ -67,7 +68,7 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] }); const kmsKey = await awsClient.send(command); - if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key"); + if (!kmsKey.KeyMetadata?.KeyId) throw new BadRequestError({ message: "Failed to generate kms key" }); const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({ ...providerInputs, diff --git a/backend/src/ee/services/external-kms/providers/model.ts b/backend/src/ee/services/external-kms/providers/model.ts index 6cb78a34e..08a9a3fc7 100644 --- a/backend/src/ee/services/external-kms/providers/model.ts +++ b/backend/src/ee/services/external-kms/providers/model.ts @@ -19,27 +19,31 @@ export enum KmsGcpKeyFetchAuthType { Kms = "kmsId" } +const AwsConnectionAssumeRoleCredentialsSchema = z.object({ + assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"), + externalId: z + .string() + .trim() + .min(1) + .optional() + .describe("AWS assume role external id for further security in authentication") +}); + +const AwsConnectionAccessTokenCredentialsSchema = z.object({ + accessKey: z.string().trim().min(1).describe("AWS user account access key"), + secretKey: z.string().trim().min(1).describe("AWS user account secret key") +}); + export const ExternalKmsAwsSchema = z.object({ credential: z .discriminatedUnion("type", [ z.object({ type: z.literal(KmsAwsCredentialType.AccessKey), - data: z.object({ - accessKey: z.string().trim().min(1).describe("AWS user account access key"), - secretKey: z.string().trim().min(1).describe("AWS user account secret key") - }) + data: AwsConnectionAccessTokenCredentialsSchema }), z.object({ type: z.literal(KmsAwsCredentialType.AssumeRole), - data: z.object({ - assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"), - externalId: z - .string() - .trim() - .min(1) - .optional() - .describe("AWS assume role external id for furthur security in authentication") - }) + data: AwsConnectionAssumeRoleCredentialsSchema }) ]) .describe("AWS credential information to connect"), @@ -52,6 +56,22 @@ export const ExternalKmsAwsSchema = z.object({ }); export type TExternalKmsAwsSchema = z.infer; +export const SanitizedExternalKmsAwsSchema = ExternalKmsAwsSchema.extend({ + credential: z.discriminatedUnion("type", [ + z.object({ + type: z.literal(KmsAwsCredentialType.AccessKey), + data: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKey: true }) + }), + z.object({ + type: z.literal(KmsAwsCredentialType.AssumeRole), + data: AwsConnectionAssumeRoleCredentialsSchema.pick({ + assumeRoleArn: true, + externalId: true + }) + }) + ]) +}); + export const ExternalKmsGcpCredentialSchema = z.object({ type: z.literal(KmsGcpCredentialType.ServiceAccount), project_id: z.string().min(1), @@ -75,6 +95,8 @@ export const ExternalKmsGcpSchema = z.object({ }); export type TExternalKmsGcpSchema = z.infer; +export const SanitizedExternalKmsGcpSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true }); + const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({ credential: ExternalKmsGcpCredentialSchema }); diff --git a/backend/src/ee/services/pam-account/pam-account-fns.ts b/backend/src/ee/services/pam-account/pam-account-fns.ts index aae703eeb..71ef0fd7b 100644 --- a/backend/src/ee/services/pam-account/pam-account-fns.ts +++ b/backend/src/ee/services/pam-account/pam-account-fns.ts @@ -72,17 +72,24 @@ export const decryptAccount = async < account: T, projectId: string, kmsService: Pick -): Promise => { +): Promise< + Omit & { + credentials: TPamAccountCredentials; + lastRotationMessage: string | null; + } +> => { + const { encryptedCredentials, encryptedLastRotationMessage, ...rest } = account; + return { - ...account, + ...rest, credentials: await decryptAccountCredentials({ - encryptedCredentials: account.encryptedCredentials, + encryptedCredentials, projectId, kmsService }), - lastRotationMessage: account.encryptedLastRotationMessage + lastRotationMessage: encryptedLastRotationMessage ? await decryptAccountMessage({ - encryptedMessage: account.encryptedLastRotationMessage, + encryptedMessage: encryptedLastRotationMessage, projectId, kmsService }) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 1eae8df15..c3baeff0c 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,13 @@ +import path from "node:path"; + import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamFolders, TPamResources } from "@app/db/schemas"; +import { + extractAwsAccountIdFromArn, + generateConsoleFederationUrl, + TAwsIamAccountCredentials +} from "@app/ee/services/pam-resource/aws-iam"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -10,12 +17,23 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DatabaseErrorCode } from "@app/lib/error-codes"; -import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { + BadRequestError, + DatabaseError, + ForbiddenRequestError, + NotFoundError, + PolicyViolationError +} from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; +import { TApprovalPolicyDALFactory } from "@app/services/approval-policy/approval-policy-dal"; +import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; +import { APPROVAL_POLICY_FACTORY_MAP } from "@app/services/approval-policy/approval-policy-factory"; +import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TPamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -27,7 +45,8 @@ import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns"; import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal"; import { PamResource } from "../pam-resource/pam-resource-enums"; import { TPamAccountCredentials } from "../pam-resource/pam-resource-types"; -import { TSqlResourceConnectionDetails } from "../pam-resource/shared/sql/sql-resource-types"; +import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "../pam-resource/shared/sql/sql-resource-types"; +import { TSSHAccountCredentials } from "../pam-resource/ssh/ssh-resource-types"; import { TPamSessionDALFactory } from "../pam-session/pam-session-dal"; import { PamSessionStatus } from "../pam-session/pam-session-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -51,6 +70,9 @@ type TPamAccountServiceFactoryDep = { >; userDAL: TUserDALFactory; auditLogService: Pick; + approvalPolicyDAL: TApprovalPolicyDALFactory; + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory; + pamSessionExpirationService: Pick; }; export type TPamAccountServiceFactory = ReturnType; @@ -67,7 +89,10 @@ export const pamAccountServiceFactory = ({ licenseService, kmsService, gatewayV2Service, - auditLogService + auditLogService, + approvalPolicyDAL, + approvalRequestGrantsDAL, + pamSessionExpirationService }: TPamAccountServiceFactoryDep) => { const create = async ( { @@ -135,7 +160,8 @@ export const pamAccountServiceFactory = ({ resource.resourceType as PamResource, connectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + resource.projectId ); const validatedCredentials = await factory.validateAccountCredentials(credentials); @@ -250,7 +276,8 @@ export const pamAccountServiceFactory = ({ resource.resourceType as PamResource, connectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + account.projectId ); const decryptedCredentials = await decryptAccountCredentials({ @@ -279,17 +306,27 @@ export const pamAccountServiceFactory = ({ return decryptAccount(account, account.projectId, kmsService); } - const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc); + try { + const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc); - return { - ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), - resource: { - id: resource.id, - name: resource.name, - resourceType: resource.resourceType, - rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + return { + ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `Account with name '${name}' already exists for this path` + }); } - }; + + throw err; + } }; const deleteById = async (id: string, actor: OrgServiceActor) => { @@ -428,7 +465,7 @@ export const pamAccountServiceFactory = ({ const totalCount = totalFolderCount + totalAccountCount; const decryptedAndPermittedAccounts: Array< - TPamAccounts & { + Omit & { resource: Pick & { rotationCredentialsConfigured: boolean }; credentials: TPamAccountCredentials; lastRotationMessage: string | null; @@ -487,7 +524,7 @@ export const pamAccountServiceFactory = ({ }; const access = async ( - { accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, + { accountPath, projectId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -497,50 +534,83 @@ export const pamAccountServiceFactory = ({ }); } - const account = await pamAccountDAL.findById(accountId); - if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` }); + const pathSegments: string[] = accountPath.split("/").filter(Boolean); + if (pathSegments.length === 0) { + throw new BadRequestError({ message: "Invalid accountPath. Path must contain at least the account name." }); + } + + const accountName: string = pathSegments[pathSegments.length - 1] ?? ""; + const folderPathSegments: string[] = pathSegments.slice(0, -1); + + const folderPath: string = folderPathSegments.length > 0 ? `/${folderPathSegments.join("/")}` : "/"; + + let folderId: string | null = null; + if (folderPath !== "/") { + const folder = await pamFolderDAL.findByPath(projectId, folderPath); + if (!folder) { + throw new NotFoundError({ message: `Folder at path '${folderPath}' not found` }); + } + folderId = folder.id; + } + + const account = await pamAccountDAL.findOne({ + projectId, + folderId, + name: accountName + }); + + if (!account) { + throw new NotFoundError({ + message: `Account with name '${accountName}' not found at path '${accountPath}'` + }); + } const resource = await pamResourceDAL.findById(account.resourceId); if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); - const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, - projectId: account.projectId, - actionProjectType: ActionProjectType.PAM - }); + const fac = APPROVAL_POLICY_FACTORY_MAP[ApprovalPolicyType.PamAccess](ApprovalPolicyType.PamAccess); - const accountPath = await getFullPamFolderPath({ - pamFolderDAL, - folderId: account.folderId, - projectId: account.projectId - }); + const inputs = { + resourceId: resource.id, + accountPath: path.join(folderPath, account.name) + }; - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPamAccountActions.Access, - subject(ProjectPermissionSub.PamAccounts, { - resourceName: resource.name, - accountName: account.name, - accountPath - }) - ); + const canAccess = await fac.canAccess(approvalRequestGrantsDAL, resource.projectId, actor.id, inputs); - const session = await pamSessionDAL.create({ - accountName: account.name, - actorEmail, - actorIp, - actorName, - actorUserAgent, - projectId: account.projectId, - resourceName: resource.name, - resourceType: resource.resourceType, - status: PamSessionStatus.Starting, - accountId: account.id, - userId: actor.id, - expiresAt: new Date(Date.now() + duration) - }); + // Grant does not exist, check policy and fallback to permission check + if (!canAccess) { + const policy = await fac.matchPolicy(approvalPolicyDAL, resource.projectId, inputs); + + if (policy) { + throw new PolicyViolationError({ + message: "A policy is in place for this resource", + details: { + policyId: policy.id, + policyName: policy.name, + policyType: policy.type + } + }); + } + + // If there isn't a policy in place, continue with checking permission + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: account.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamAccountActions.Access, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: resource.name, + accountName: account.name, + accountPath: folderPath + }) + ); + } const { connectionDetails, gatewayId, resourceType } = await decryptResource( resource, @@ -551,13 +621,81 @@ export const pamAccountServiceFactory = ({ const user = await userDAL.findById(actor.id); if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` }); + if (resourceType === PamResource.AwsIam) { + const awsCredentials = (await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + kmsService, + projectId: account.projectId + })) as TAwsIamAccountCredentials; + + const { consoleUrl, expiresAt } = await generateConsoleFederationUrl({ + connectionDetails, + targetRoleArn: awsCredentials.targetRoleArn, + roleSessionName: actorEmail, + projectId: account.projectId, // Use project ID as External ID for security + sessionDuration: awsCredentials.defaultSessionDuration + }); + + const session = await pamSessionDAL.create({ + accountName: account.name, + actorEmail, + actorIp, + actorName, + actorUserAgent, + projectId: account.projectId, + resourceName: resource.name, + resourceType: resource.resourceType, + status: PamSessionStatus.Active, // AWS IAM sessions are immediately active + accountId: account.id, + userId: actor.id, + expiresAt, + startedAt: new Date() + }); + + // Schedule session expiration job to run at expiresAt + await pamSessionExpirationService.scheduleSessionExpiration(session.id, expiresAt); + + return { + sessionId: session.id, + resourceType, + account, + consoleUrl, + metadata: { + awsAccountId: extractAwsAccountIdFromArn(connectionDetails.roleArn), + targetRoleArn: awsCredentials.targetRoleArn, + federatedUsername: actorEmail, + expiresAt: expiresAt.toISOString() + } + }; + } + + // For gateway-based resources (Postgres, MySQL, SSH), create session first + const session = await pamSessionDAL.create({ + accountName: account.name, + actorEmail, + actorIp, + actorName, + actorUserAgent, + projectId, + resourceName: resource.name, + resourceType: resource.resourceType, + status: PamSessionStatus.Starting, + accountId: account.id, + userId: actor.id, + expiresAt: new Date(Date.now() + duration) + }); + + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required for this resource type" }); + } + const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({ gatewayId, duration, sessionId: session.id, resourceType: resource.resourceType as PamResource, - host: connectionDetails.host, - port: connectionDetails.port, + host: (connectionDetails as TSqlResourceConnectionDetails).host, + port: (connectionDetails as TSqlResourceConnectionDetails).port, actorMetadata: { id: actor.id, type: actor.type, @@ -578,30 +716,30 @@ export const pamAccountServiceFactory = ({ const connectionCredentials = (await decryptResourceConnectionDetails({ encryptedConnectionDetails: resource.encryptedConnectionDetails, kmsService, - projectId: account.projectId + projectId })) as TSqlResourceConnectionDetails; - const credentials = await decryptAccountCredentials({ + const credentials = (await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, kmsService, - projectId: account.projectId - }); + projectId + })) as TSqlAccountCredentials; metadata = { username: credentials.username, database: connectionCredentials.database, accountName: account.name, - accountPath + accountPath: folderPath }; } break; case PamResource.SSH: { - const credentials = await decryptAccountCredentials({ + const credentials = (await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, kmsService, - projectId: account.projectId - }); + projectId + })) as TSSHAccountCredentials; metadata = { username: credentials.username @@ -622,7 +760,7 @@ export const pamAccountServiceFactory = ({ gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey, gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain, relayHost: gatewayConnectionDetails.relayHost, - projectId: account.projectId, + projectId, account, metadata }; @@ -674,7 +812,7 @@ export const pamAccountServiceFactory = ({ const resource = await pamResourceDAL.findById(account.resourceId); if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); - if (resource.gatewayIdentityId !== actor.id) { + if (resource.gatewayId && resource.gatewayIdentityId !== actor.id) { throw new ForbiddenRequestError({ message: "Identity does not have access to fetch the PAM session credentials" }); @@ -738,7 +876,8 @@ export const pamAccountServiceFactory = ({ resourceType as PamResource, connectionDetails, gatewayId, - gatewayV2Service + gatewayV2Service, + account.projectId ); const newCredentials = await factory.rotateAccountCredentials( diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index b8498036e..a20d2f737 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -6,15 +6,18 @@ import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums"; // DTOs export type TCreateAccountDTO = Pick< TPamAccount, - "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationEnabled" | "rotationIntervalSeconds" ->; + "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationIntervalSeconds" +> & { + rotationEnabled?: boolean; +}; export type TUpdateAccountDTO = Partial> & { accountId: string; }; export type TAccessAccountDTO = { - accountId: string; + accountPath: string; + projectId: string; actorEmail: string; actorIp: string; actorName: string; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts new file mode 100644 index 000000000..97415a088 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts @@ -0,0 +1,245 @@ +import { AssumeRoleCommand, Credentials, STSClient, STSClientConfig } from "@aws-sdk/client-sts"; + +import { CustomAWSHasher } from "@app/lib/aws/hashing"; +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; + +import { TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types"; + +const AWS_STS_MIN_DURATION_SECONDS = 900; + +// We hardcode us-east-1 because: +// 1. IAM is global - roles can be assumed from any STS regional endpoint +// 2. The temporary credentials returned work globally across all AWS regions +// 3. The target account's resources can be in any region - it doesn't affect STS calls +const AWS_STS_DEFAULT_REGION = "us-east-1"; + +const createStsClient = (credentials?: Credentials): STSClient => { + const appCfg = getConfig(); + + const config: STSClientConfig = { + region: AWS_STS_DEFAULT_REGION, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher + }; + + if (credentials) { + // Use provided credentials (for role chaining) + config.credentials = { + accessKeyId: credentials.AccessKeyId!, + secretAccessKey: credentials.SecretAccessKey!, + sessionToken: credentials.SessionToken + }; + } else if (appCfg.PAM_AWS_ACCESS_KEY_ID && appCfg.PAM_AWS_SECRET_ACCESS_KEY) { + // Use configured static credentials + config.credentials = { + accessKeyId: appCfg.PAM_AWS_ACCESS_KEY_ID, + secretAccessKey: appCfg.PAM_AWS_SECRET_ACCESS_KEY + }; + } + // Otherwise uses instance profile if hosting on AWS + + return new STSClient(config); +}; + +/** + * Assumes the PAM role and returns the credentials. + * Returns null if assumption fails (for validation) or throws if throwOnError is true. + */ +const assumePamRole = async ({ + connectionDetails, + projectId, + sessionDuration = AWS_STS_MIN_DURATION_SECONDS, + sessionNameSuffix = "validation", + throwOnError = false +}: { + connectionDetails: TAwsIamResourceConnectionDetails; + projectId: string; + sessionDuration?: number; + sessionNameSuffix?: string; + throwOnError?: boolean; +}): Promise => { + const stsClient = createStsClient(); + + try { + const result = await stsClient.send( + new AssumeRoleCommand({ + RoleArn: connectionDetails.roleArn, + RoleSessionName: `infisical-pam-${sessionNameSuffix}-${Date.now()}`, + DurationSeconds: sessionDuration, + ExternalId: projectId + }) + ); + + if (!result.Credentials) { + if (throwOnError) { + throw new InternalServerError({ + message: "Failed to assume PAM role - AWS STS did not return credentials" + }); + } + return null; + } + + return result.Credentials; + } catch (error) { + if (throwOnError) { + throw new InternalServerError({ + message: `Failed to assume PAM role - AWS STS did not return credentials: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + return null; + } +}; + +/** + * Assumes a target role using PAM role credentials (role chaining). + * Returns null if assumption fails (for validation) or throws if throwOnError is true. + */ +const assumeTargetRole = async ({ + pamCredentials, + targetRoleArn, + projectId, + roleSessionName, + sessionDuration = AWS_STS_MIN_DURATION_SECONDS, + throwOnError = false +}: { + pamCredentials: Credentials; + targetRoleArn: string; + projectId: string; + roleSessionName: string; + sessionDuration?: number; + throwOnError?: boolean; +}): Promise => { + const chainedStsClient = createStsClient(pamCredentials); + + try { + const result = await chainedStsClient.send( + new AssumeRoleCommand({ + RoleArn: targetRoleArn, + RoleSessionName: roleSessionName, + DurationSeconds: sessionDuration, + ExternalId: projectId + }) + ); + + if (!result.Credentials) { + if (throwOnError) { + throw new BadRequestError({ + message: "Failed to assume target role - verify the target role trust policy allows the PAM role to assume it" + }); + } + return null; + } + + return result.Credentials; + } catch (error) { + if (throwOnError) { + throw new InternalServerError({ + message: `Failed to assume target role - AWS STS did not return credentials: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + return null; + } +}; + +export const validatePamRoleConnection = async ( + connectionDetails: TAwsIamResourceConnectionDetails, + projectId: string +): Promise => { + try { + const credentials = await assumePamRole({ connectionDetails, projectId }); + return credentials !== null; + } catch { + return false; + } +}; + +export const validateTargetRoleAssumption = async ({ + connectionDetails, + targetRoleArn, + projectId +}: { + connectionDetails: TAwsIamResourceConnectionDetails; + targetRoleArn: string; + projectId: string; +}): Promise => { + try { + const pamCredentials = await assumePamRole({ connectionDetails, projectId }); + if (!pamCredentials) return false; + + const targetCredentials = await assumeTargetRole({ + pamCredentials, + targetRoleArn, + projectId, + roleSessionName: `infisical-pam-target-validation-${Date.now()}` + }); + return targetCredentials !== null; + } catch { + return false; + } +}; + +/** + * Assumes the target role and generates a federated console sign-in URL. + */ +export const generateConsoleFederationUrl = async ({ + connectionDetails, + targetRoleArn, + roleSessionName, + projectId, + sessionDuration +}: { + connectionDetails: TAwsIamResourceConnectionDetails; + targetRoleArn: string; + roleSessionName: string; + projectId: string; + sessionDuration: number; +}): Promise<{ consoleUrl: string; expiresAt: Date }> => { + const pamCredentials = await assumePamRole({ + connectionDetails, + projectId, + sessionDuration, + sessionNameSuffix: "session", + throwOnError: true + }); + + const targetCredentials = await assumeTargetRole({ + pamCredentials: pamCredentials!, + targetRoleArn, + projectId, + roleSessionName, + sessionDuration, + throwOnError: true + }); + + const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = targetCredentials!; + + // Generate federation URL + const sessionJson = JSON.stringify({ + sessionId: AccessKeyId, + sessionKey: SecretAccessKey, + sessionToken: SessionToken + }); + + const federationEndpoint = "https://signin.aws.amazon.com/federation"; + + const signinTokenUrl = `${federationEndpoint}?Action=getSigninToken&Session=${encodeURIComponent(sessionJson)}`; + + const tokenResponse = await request.get<{ SigninToken?: string }>(signinTokenUrl); + + if (!tokenResponse.data.SigninToken) { + throw new InternalServerError({ + message: `AWS federation endpoint did not return a SigninToken: ${JSON.stringify(tokenResponse.data).substring(0, 200)}` + }); + } + + const consoleDestination = `https://console.aws.amazon.com/`; + const consoleUrl = `${federationEndpoint}?Action=login&SigninToken=${encodeURIComponent(tokenResponse.data.SigninToken)}&Destination=${encodeURIComponent(consoleDestination)}`; + + return { + consoleUrl, + expiresAt: Expiration ?? new Date(Date.now() + sessionDuration * 1000) + }; +}; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts new file mode 100644 index 000000000..844908671 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts @@ -0,0 +1,110 @@ +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { PamResource } from "../pam-resource-enums"; +import { + TPamResourceFactory, + TPamResourceFactoryRotateAccountCredentials, + TPamResourceFactoryValidateAccountCredentials +} from "../pam-resource-types"; +import { validatePamRoleConnection, validateTargetRoleAssumption } from "./aws-iam-federation"; +import { TAwsIamAccountCredentials, TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types"; + +export const awsIamResourceFactory: TPamResourceFactory = ( + resourceType: PamResource, + connectionDetails: TAwsIamResourceConnectionDetails, + // AWS IAM doesn't use gateway + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _gatewayId, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _gatewayV2Service, + projectId +) => { + const validateConnection = async () => { + try { + const isValid = await validatePamRoleConnection(connectionDetails, projectId ?? ""); + + if (!isValid) { + throw new BadRequestError({ + message: + "Unable to assume the PAM role. Verify the role ARN and ensure the trust policy allows Infisical to assume the role." + }); + } + + logger.info( + { roleArn: connectionDetails.roleArn }, + "[AWS IAM Resource Factory] PAM role connection validated successfully" + ); + + return connectionDetails; + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + logger.error(error, "[AWS IAM Resource Factory] Failed to validate PAM role connection"); + + throw new BadRequestError({ + message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials = async ( + credentials + ) => { + try { + const isValid = await validateTargetRoleAssumption({ + connectionDetails, + targetRoleArn: credentials.targetRoleArn, + projectId: projectId ?? "" + }); + + if (!isValid) { + throw new BadRequestError({ + message: `Unable to assume the target role. Verify the target role ARN and ensure the PAM role (ARN: ${connectionDetails.roleArn}) has permission to assume it.` + }); + } + + logger.info( + { targetRoleArn: credentials.targetRoleArn }, + "[AWS IAM Resource Factory] Target role credentials validated successfully" + ); + + return credentials; + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + logger.error(error, "[AWS IAM Resource Factory] Failed to validate target role credentials"); + + throw new BadRequestError({ + message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials = async ( + _rotationAccountCredentials, + currentCredentials + ) => { + return currentCredentials; + }; + + const handleOverwritePreventionForCensoredValues = async ( + updatedAccountCredentials: TAwsIamAccountCredentials, + // AWS IAM has no censored credential values - role ARNs are not secrets + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _currentCredentials: TAwsIamAccountCredentials + ) => { + return updatedAccountCredentials; + }; + + return { + validateConnection, + validateAccountCredentials, + rotateAccountCredentials, + handleOverwritePreventionForCensoredValues + }; +}; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-fns.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-fns.ts new file mode 100644 index 000000000..d04018d49 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-fns.ts @@ -0,0 +1,24 @@ +import RE2 from "re2"; + +import { BadRequestError } from "@app/lib/errors"; + +import { AwsIamResourceListItemSchema } from "./aws-iam-resource-schemas"; + +export const getAwsIamResourceListItem = () => { + return { + name: AwsIamResourceListItemSchema.shape.name.value, + resource: AwsIamResourceListItemSchema.shape.resource.value + }; +}; + +/** + * Extract the AWS Account ID from an IAM Role ARN + * ARN format: arn:aws:iam::123456789012:role/RoleName + */ +export const extractAwsAccountIdFromArn = (roleArn: string): string => { + const match = roleArn.match(new RE2("^arn:aws:iam::(\\d{12}):role/")); + if (!match) { + throw new BadRequestError({ message: "Invalid IAM Role ARN format" }); + } + return match[1]; +}; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas.ts new file mode 100644 index 000000000..2762977eb --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas.ts @@ -0,0 +1,81 @@ +import { z } from "zod"; + +import { PamResource } from "../pam-resource-enums"; +import { + BaseCreatePamAccountSchema, + BaseCreatePamResourceSchema, + BasePamAccountSchema, + BasePamAccountSchemaWithResource, + BasePamResourceSchema, + BaseUpdatePamAccountSchema, + BaseUpdatePamResourceSchema +} from "../pam-resource-schemas"; + +// AWS STS session duration limits (in seconds) +// Role chaining (Infisical → PAM role → target role) limits max session to 1 hour +// @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html +const AWS_STS_MIN_SESSION_DURATION = 900; // 15 minutes +const AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING = 3600; // 1 hour + +export const AwsIamResourceConnectionDetailsSchema = z.object({ + roleArn: z.string().trim().min(1) +}); + +export const AwsIamAccountCredentialsSchema = z.object({ + targetRoleArn: z.string().trim().min(1).max(2048), + defaultSessionDuration: z.coerce + .number() + .min(AWS_STS_MIN_SESSION_DURATION) + .max(AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING) +}); + +const BaseAwsIamResourceSchema = BasePamResourceSchema.extend({ + resourceType: z.literal(PamResource.AwsIam), + gatewayId: z.string().uuid().nullable().optional() +}); + +export const AwsIamResourceSchema = BaseAwsIamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema, + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const SanitizedAwsIamResourceSchema = BaseAwsIamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema, + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const AwsIamResourceListItemSchema = z.object({ + name: z.literal("AWS IAM"), + resource: z.literal(PamResource.AwsIam) +}); + +export const CreateAwsIamResourceSchema = BaseCreatePamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema, + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const UpdateAwsIamResourceSchema = BaseUpdatePamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema.optional(), + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const AwsIamAccountSchema = BasePamAccountSchema.extend({ + credentials: AwsIamAccountCredentialsSchema +}); + +export const CreateAwsIamAccountSchema = BaseCreatePamAccountSchema.extend({ + credentials: AwsIamAccountCredentialsSchema, + // AWS IAM accounts don't support credential rotation - they use role assumption + rotationEnabled: z.boolean().default(false) +}); + +export const UpdateAwsIamAccountSchema = BaseUpdatePamAccountSchema.extend({ + credentials: AwsIamAccountCredentialsSchema.optional() +}); + +export const SanitizedAwsIamAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({ + credentials: AwsIamAccountCredentialsSchema.pick({ + targetRoleArn: true, + defaultSessionDuration: true + }) +}); diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-types.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-types.ts new file mode 100644 index 000000000..732355371 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-types.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { + AwsIamAccountCredentialsSchema, + AwsIamAccountSchema, + AwsIamResourceConnectionDetailsSchema, + AwsIamResourceSchema +} from "./aws-iam-resource-schemas"; + +// Resources +export type TAwsIamResource = z.infer; +export type TAwsIamResourceConnectionDetails = z.infer; + +// Accounts +export type TAwsIamAccount = z.infer; +export type TAwsIamAccountCredentials = z.infer; diff --git a/backend/src/ee/services/pam-resource/aws-iam/index.ts b/backend/src/ee/services/pam-resource/aws-iam/index.ts new file mode 100644 index 000000000..8e41fa48a --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/index.ts @@ -0,0 +1,5 @@ +export * from "./aws-iam-federation"; +export * from "./aws-iam-resource-factory"; +export * from "./aws-iam-resource-fns"; +export * from "./aws-iam-resource-schemas"; +export * from "./aws-iam-resource-types"; diff --git a/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts b/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts index 8d3589a8a..cb12a4c8c 100644 --- a/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import { PamResource } from "../pam-resource-enums"; import { + BaseCreateGatewayPamResourceSchema, BaseCreatePamAccountSchema, - BaseCreatePamResourceSchema, BasePamAccountSchema, BasePamAccountSchemaWithResource, BasePamResourceSchema, - BaseUpdatePamAccountSchema, - BaseUpdatePamResourceSchema + BaseUpdateGatewayPamResourceSchema, + BaseUpdatePamAccountSchema } from "../pam-resource-schemas"; import { BaseSqlAccountCredentialsSchema, @@ -43,12 +43,12 @@ export const MySQLResourceListItemSchema = z.object({ resource: z.literal(PamResource.MySQL) }); -export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({ +export const CreateMySQLResourceSchema = BaseCreateGatewayPamResourceSchema.extend({ connectionDetails: MySQLResourceConnectionDetailsSchema, rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() }); -export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({ +export const UpdateMySQLResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({ connectionDetails: MySQLResourceConnectionDetailsSchema.optional(), rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() }); diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 9e5cbc985..e5b76a882 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -14,7 +14,7 @@ export const pamResourceDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { const doc = await (tx || db.replicaNode())(TableName.PamResource) - .join(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`) + .leftJoin(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`) .select(selectAllTableCols(TableName.PamResource)) .select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName")) .select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId")) diff --git a/backend/src/ee/services/pam-resource/pam-resource-enums.ts b/backend/src/ee/services/pam-resource/pam-resource-enums.ts index e4ec043e1..bea1667fb 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-enums.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-enums.ts @@ -1,7 +1,8 @@ export enum PamResource { Postgres = "postgres", MySQL = "mysql", - SSH = "ssh" + SSH = "ssh", + AwsIam = "aws-iam" } export enum PamResourceOrderBy { diff --git a/backend/src/ee/services/pam-resource/pam-resource-factory.ts b/backend/src/ee/services/pam-resource/pam-resource-factory.ts index e2d0a50f8..1d1a84f33 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-factory.ts @@ -1,3 +1,4 @@ +import { awsIamResourceFactory } from "./aws-iam/aws-iam-resource-factory"; import { PamResource } from "./pam-resource-enums"; import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types"; import { sqlResourceFactory } from "./shared/sql/sql-resource-factory"; @@ -8,5 +9,6 @@ type TPamResourceFactoryImplementation = TPamResourceFactory = { [PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation, [PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation, - [PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation + [PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation, + [PamResource.AwsIam]: awsIamResourceFactory as TPamResourceFactoryImplementation }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-fns.ts b/backend/src/ee/services/pam-resource/pam-resource-fns.ts index cad087d2f..a90743a12 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-fns.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-fns.ts @@ -3,12 +3,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { decryptAccountCredentials } from "../pam-account/pam-account-fns"; +import { getAwsIamResourceListItem } from "./aws-iam/aws-iam-resource-fns"; import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns"; import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types"; import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns"; export const listResourceOptions = () => { - return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name)); + return [getPostgresResourceListItem(), getMySQLResourceListItem(), getAwsIamResourceListItem()].sort((a, b) => + a.name.localeCompare(b.name) + ); }; // Resource diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 17ed1ccd1..a3db6b446 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -3,6 +3,18 @@ import { z } from "zod"; import { PamAccountsSchema, PamResourcesSchema } from "@app/db/schemas"; import { slugSchema } from "@app/server/lib/schemas"; +export const GatewayAccessResponseSchema = z.object({ + sessionId: z.string(), + relayClientCertificate: z.string(), + relayClientPrivateKey: z.string(), + relayServerCertificateChain: z.string(), + gatewayClientCertificate: z.string(), + gatewayClientPrivateKey: z.string(), + gatewayServerCertificateChain: z.string(), + relayHost: z.string(), + metadata: z.record(z.string(), z.string().optional()).optional() +}); + // Resources export const BasePamResourceSchema = PamResourcesSchema.omit({ encryptedConnectionDetails: true, @@ -10,17 +22,27 @@ export const BasePamResourceSchema = PamResourcesSchema.omit({ resourceType: true }); -export const BaseCreatePamResourceSchema = z.object({ +const CoreCreatePamResourceSchema = z.object({ projectId: z.string().uuid(), - gatewayId: z.string().uuid(), name: slugSchema({ field: "name" }) }); -export const BaseUpdatePamResourceSchema = z.object({ - gatewayId: z.string().uuid().optional(), +export const BaseCreateGatewayPamResourceSchema = CoreCreatePamResourceSchema.extend({ + gatewayId: z.string().uuid() +}); + +export const BaseCreatePamResourceSchema = CoreCreatePamResourceSchema; + +const CoreUpdatePamResourceSchema = z.object({ name: slugSchema({ field: "name" }).optional() }); +export const BaseUpdateGatewayPamResourceSchema = CoreUpdatePamResourceSchema.extend({ + gatewayId: z.string().uuid().optional() +}); + +export const BaseUpdatePamResourceSchema = CoreUpdatePamResourceSchema; + // Accounts export const BasePamAccountSchema = PamAccountsSchema.omit({ encryptedCredentials: true diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 0ebca02b5..abbbf651b 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -92,7 +92,8 @@ export const pamResourceServiceFactory = ({ resourceType, connectionDetails, gatewayId, - gatewayV2Service + gatewayV2Service, + projectId ); const validatedConnectionDetails = await factory.validateConnection(); @@ -162,7 +163,8 @@ export const pamResourceServiceFactory = ({ resource.resourceType as PamResource, connectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + resource.projectId ); const validatedConnectionDetails = await factory.validateConnection(); const encryptedConnectionDetails = await encryptResourceConnectionDetails({ @@ -189,7 +191,8 @@ export const pamResourceServiceFactory = ({ resource.resourceType as PamResource, decryptedConnectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + resource.projectId ); let finalCredentials = { ...rotationAccountCredentials }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index 9da094801..2a27fb76e 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -1,6 +1,12 @@ import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; +import { + TAwsIamAccount, + TAwsIamAccountCredentials, + TAwsIamResource, + TAwsIamResourceConnectionDetails +} from "./aws-iam/aws-iam-resource-types"; import { TMySQLAccount, TMySQLAccountCredentials, @@ -22,22 +28,28 @@ import { } from "./ssh/ssh-resource-types"; // Resource types -export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource; +export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TAwsIamResource; export type TPamResourceConnectionDetails = | TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails - | TSSHResourceConnectionDetails; + | TSSHResourceConnectionDetails + | TAwsIamResourceConnectionDetails; // Account types -export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount; -// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents -export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials | TSSHAccountCredentials; +export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TAwsIamAccount; + +export type TPamAccountCredentials = + | TPostgresAccountCredentials + // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents + | TMySQLAccountCredentials + | TSSHAccountCredentials + | TAwsIamAccountCredentials; // Resource DTOs -export type TCreateResourceDTO = Pick< - TPamResource, - "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" | "rotationAccountCredentials" ->; +export type TCreateResourceDTO = Pick & { + gatewayId?: string | null; + rotationAccountCredentials?: TPamAccountCredentials | null; +}; export type TUpdateResourceDTO = Partial> & { resourceId: string; @@ -65,8 +77,9 @@ export type TPamResourceFactoryRotateAccountCredentials = ( resourceType: PamResource, connectionDetails: T, - gatewayId: string, - gatewayV2Service: Pick + gatewayId: string | null | undefined, + gatewayV2Service: Pick, + projectId: string | null | undefined ) => { validateConnection: TPamResourceFactoryValidateConnection; validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials; diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts index bbe83a3a4..fd58484f7 100644 --- a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import { PamResource } from "../pam-resource-enums"; import { + BaseCreateGatewayPamResourceSchema, BaseCreatePamAccountSchema, - BaseCreatePamResourceSchema, BasePamAccountSchema, BasePamAccountSchemaWithResource, BasePamResourceSchema, - BaseUpdatePamAccountSchema, - BaseUpdatePamResourceSchema + BaseUpdateGatewayPamResourceSchema, + BaseUpdatePamAccountSchema } from "../pam-resource-schemas"; import { BaseSqlAccountCredentialsSchema, @@ -40,12 +40,12 @@ export const PostgresResourceListItemSchema = z.object({ resource: z.literal(PamResource.Postgres) }); -export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({ +export const CreatePostgresResourceSchema = BaseCreateGatewayPamResourceSchema.extend({ connectionDetails: PostgresResourceConnectionDetailsSchema, rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); -export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({ +export const UpdatePostgresResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({ connectionDetails: PostgresResourceConnectionDetailsSchema.optional(), rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index b3128c422..26fa7ff39 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -233,6 +233,10 @@ export const sqlResourceFactory: TPamResourceFactory { const validateConnection = async () => { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + try { await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => { await client.validate(true); @@ -255,6 +259,10 @@ export const sqlResourceFactory: TPamResourceFactory { try { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + await executeWithGateway( { connectionDetails, @@ -296,6 +304,10 @@ export const sqlResourceFactory: TPamResourceFactory { const newPassword = alphaNumericNanoId(32); + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + try { return await executeWithGateway( { diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts index b90aa00c6..dfbb071e2 100644 --- a/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts @@ -60,6 +60,10 @@ export const sshResourceFactory: TPamResourceFactory { const validateConnection = async () => { try { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => { return new Promise((resolve, reject) => { const client = new Client(); @@ -131,6 +135,10 @@ export const sshResourceFactory: TPamResourceFactory { try { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => { return new Promise((resolve, reject) => { const client = new Client(); diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts index 97d462369..01b8ef2c0 100644 --- a/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import { PamResource } from "../pam-resource-enums"; import { + BaseCreateGatewayPamResourceSchema, BaseCreatePamAccountSchema, - BaseCreatePamResourceSchema, BasePamAccountSchema, BasePamAccountSchemaWithResource, BasePamResourceSchema, - BaseUpdatePamAccountSchema, - BaseUpdatePamResourceSchema + BaseUpdateGatewayPamResourceSchema, + BaseUpdatePamAccountSchema } from "../pam-resource-schemas"; import { SSHAuthMethod } from "./ssh-resource-enums"; @@ -73,12 +73,12 @@ export const SanitizedSSHResourceSchema = BaseSSHResourceSchema.extend({ .optional() }); -export const CreateSSHResourceSchema = BaseCreatePamResourceSchema.extend({ +export const CreateSSHResourceSchema = BaseCreateGatewayPamResourceSchema.extend({ connectionDetails: SSHResourceConnectionDetailsSchema, rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() }); -export const UpdateSSHResourceSchema = BaseUpdatePamResourceSchema.extend({ +export const UpdateSSHResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({ connectionDetails: SSHResourceConnectionDetailsSchema.optional(), rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() }); diff --git a/backend/src/ee/services/pam-session/pam-session-dal.ts b/backend/src/ee/services/pam-session/pam-session-dal.ts index f8b3a3393..094614859 100644 --- a/backend/src/ee/services/pam-session/pam-session-dal.ts +++ b/backend/src/ee/services/pam-session/pam-session-dal.ts @@ -4,6 +4,8 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { PamSessionStatus } from "./pam-session-enums"; + export type TPamSessionDALFactory = ReturnType; export const pamSessionDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamSession); @@ -22,5 +24,19 @@ export const pamSessionDALFactory = (db: TDbClient) => { return session; }; - return { ...orm, findById }; + const expireSessionById = async (sessionId: string, tx?: Knex) => { + const now = new Date(); + + const updatedCount = await (tx || db)(TableName.PamSession) + .where("id", sessionId) + .whereIn("status", [PamSessionStatus.Active, PamSessionStatus.Starting]) + .update({ + status: PamSessionStatus.Ended, + endedAt: now + }); + + return updatedCount; + }; + + return { ...orm, findById, expireSessionById }; }; diff --git a/backend/src/ee/services/pam-session/pam-session-enums.ts b/backend/src/ee/services/pam-session/pam-session-enums.ts index 87731f577..33afe95e4 100644 --- a/backend/src/ee/services/pam-session/pam-session-enums.ts +++ b/backend/src/ee/services/pam-session/pam-session-enums.ts @@ -1,6 +1,6 @@ export enum PamSessionStatus { Starting = "starting", // Starting, user connecting to resource Active = "active", // Active, user is connected to resource - Ended = "ended", // Ended by user + Ended = "ended", // Ended by user or automatically expired after expiresAt timestamp Terminated = "terminated" // Terminated by an admin } diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts index 18c185cac..bdb82e650 100644 --- a/backend/src/ee/services/pam-session/pam-session-service.ts +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -34,9 +34,40 @@ export const pamSessionServiceFactory = ({ licenseService, kmsService }: TPamSessionServiceFactoryDep) => { + // Helper to check and update expired sessions when viewing session details (redundancy for scheduled job) + // Only applies to non-gateway sessions (e.g., AWS IAM) - gateway sessions are managed by the gateway + // This is intentionally only called in getById (session details view), not in list + const checkAndExpireSessionIfNeeded = async < + T extends { id: string; status: string; expiresAt: Date | null; gatewayIdentityId?: string | null } + >( + session: T + ): Promise => { + // Skip gateway-based sessions - they have their own lifecycle managed by the gateway + if (session.gatewayIdentityId) { + return session; + } + + const isActive = session.status === PamSessionStatus.Active || session.status === PamSessionStatus.Starting; + const isExpired = session.expiresAt && new Date(session.expiresAt) <= new Date(); + + if (isActive && isExpired) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const updatedSession = await pamSessionDAL.updateById(session.id, { + status: PamSessionStatus.Ended, + endedAt: new Date() + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return { ...session, ...updatedSession }; + } + + return session; + }; + const getById = async (sessionId: string, actor: OrgServiceActor) => { - const session = await pamSessionDAL.findById(sessionId); - if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + const sessionFromDb = await pamSessionDAL.findById(sessionId); + if (!sessionFromDb) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + + const session = await checkAndExpireSessionIfNeeded(sessionFromDb); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -116,7 +147,7 @@ export const pamSessionServiceFactory = ({ OrgPermissionSubjects.Gateway ); - if (session.gatewayIdentityId !== actor.id) { + if (session.gatewayIdentityId && session.gatewayIdentityId !== actor.id) { throw new ForbiddenRequestError({ message: "Identity does not have access to update logs for this session" }); } @@ -158,7 +189,7 @@ export const pamSessionServiceFactory = ({ OrgPermissionSubjects.Gateway ); - if (session.gatewayIdentityId !== actor.id) { + if (session.gatewayIdentityId && session.gatewayIdentityId !== actor.id) { throw new ForbiddenRequestError({ message: "Identity does not have access to end this session" }); } } else if (actor.type === ActorType.USER) { diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 7b2e0ae00..561cb3bb5 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -3,6 +3,8 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability" import { ProjectPermissionActions, ProjectPermissionAppConnectionActions, + ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, ProjectPermissionCertificateAuthorityActions, @@ -339,6 +341,16 @@ const buildAdminPermissionRules = () => { can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions); + can( + [ProjectPermissionApprovalRequestActions.Read, ProjectPermissionApprovalRequestActions.Create], + ProjectPermissionSub.ApprovalRequests + ); + + can( + [ProjectPermissionApprovalRequestGrantActions.Read, ProjectPermissionApprovalRequestGrantActions.Revoke], + ProjectPermissionSub.ApprovalRequestGrants + ); + return rules; }; @@ -586,6 +598,8 @@ const buildMemberPermissionRules = () => { ProjectPermissionSub.PamAccounts ); + can([ProjectPermissionApprovalRequestActions.Create], ProjectPermissionSub.ApprovalRequests); + return rules; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 20f9c1f09..85efd19aa 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -224,6 +224,16 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export enum ProjectPermissionApprovalRequestActions { + Read = "read", + Create = "create" +} + +export enum ProjectPermissionApprovalRequestGrantActions { + Read = "read", + Revoke = "revoke" +} + export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -274,7 +284,9 @@ export enum ProjectPermissionSub { PamResources = "pam-resources", PamAccounts = "pam-accounts", PamSessions = "pam-sessions", - CertificateProfiles = "certificate-profiles" + CertificateProfiles = "certificate-profiles", + ApprovalRequests = "approval-requests", + ApprovalRequestGrants = "approval-request-grants" } export type SecretSubjectFields = { @@ -500,7 +512,9 @@ export type ProjectPermissionSet = | ProjectPermissionSub.CertificateProfiles | (ForcedSubject & CertificateProfileSubjectFields) ) - ]; + ] + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests] + | [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ @@ -1105,6 +1119,18 @@ const GeneralPermissionSchema = [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.ApprovalRequests).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestActions).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.ApprovalRequestGrants).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestGrantActions).describe( + "Describe what action an entity can take." + ) }) ]; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/index.ts new file mode 100644 index 000000000..876ab836d --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/index.ts @@ -0,0 +1,4 @@ +export * from "./mongodb-credentials-rotation-constants"; +export * from "./mongodb-credentials-rotation-fns"; +export * from "./mongodb-credentials-rotation-schemas"; +export * from "./mongodb-credentials-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-constants.ts new file mode 100644 index 000000000..82b43f45a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-constants.ts @@ -0,0 +1,27 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const MONGODB_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "MongoDB Credentials", + type: SecretRotation.MongoDBCredentials, + connection: AppConnection.MongoDB, + template: { + createUserStatement: `use [DATABASE_NAME] +db.createUser({ + user: "infisical_user_1", + pwd: "temporary_password", + roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }] +}) + +db.createUser({ + user: "infisical_user_2", + pwd: "temporary_password", + roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }] +})`, + secretsMapping: { + username: "MONGODB_DB_USERNAME", + password: "MONGODB_DB_PASSWORD" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-fns.ts new file mode 100644 index 000000000..aad286ecc --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-fns.ts @@ -0,0 +1,191 @@ +/* eslint-disable no-await-in-loop */ +import { MongoClient } from "mongodb"; + +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { createMongoClient } from "@app/services/app-connection/mongodb/mongodb-connection-fns"; + +import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils"; +import { + TMongoDBCredentialsRotationGeneratedCredentials, + TMongoDBCredentialsRotationWithConnection +} from "./mongodb-credentials-rotation-types"; + +const redactPasswords = (e: unknown, credentials: TMongoDBCredentialsRotationGeneratedCredentials) => { + const error = e as Error; + + if (!error?.message) return "Unknown error"; + + let redactedMessage = error.message; + + credentials.forEach(({ password }) => { + redactedMessage = redactedMessage.replaceAll(password, "*******************"); + }); + + return redactedMessage; +}; + +export const mongodbCredentialsRotationFactory: TRotationFactory< + TMongoDBCredentialsRotationWithConnection, + TMongoDBCredentialsRotationGeneratedCredentials +> = (secretRotation) => { + const { + connection, + parameters: { username1, username2 }, + activeIndex, + secretsMapping + } = secretRotation; + + const passwordRequirement = DEFAULT_PASSWORD_REQUIREMENTS; + + const $getClient = async () => { + let client: MongoClient | null = null; + try { + client = await createMongoClient(connection.credentials, { validateConnection: true }); + return client; + } catch (err) { + if (client) await client.close(); + throw err; + } + }; + + const $validateCredentials = async (credentials: TMongoDBCredentialsRotationGeneratedCredentials[number]) => { + let client: MongoClient | null = null; + try { + client = await createMongoClient(connection.credentials, { + authCredentials: { + username: credentials.username, + password: credentials.password + }, + validateConnection: true + }); + } catch (error) { + throw new Error(redactPasswords(error, [credentials])); + } finally { + if (client) await client.close(); + } + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + // For MongoDB, since we get existing users, we change both their passwords + // on issue to invalidate their existing passwords + const credentialsSet = [ + { username: username1, password: generatePassword(passwordRequirement) }, + { username: username2, password: generatePassword(passwordRequirement) } + ]; + + let client: MongoClient | null = null; + try { + client = await $getClient(); + const db = client.db(connection.credentials.database); + + for (const credentials of credentialsSet) { + await db.command({ + updateUser: credentials.username, + pwd: credentials.password + }); + } + } catch (error) { + throw new Error(redactPasswords(error, credentialsSet)); + } finally { + if (client) await client.close(); + } + + for (const credentials of credentialsSet) { + await $validateCredentials(credentials); + } + + return callback(credentialsSet[0]); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentialsToRevoke, + callback + ) => { + const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ + username, + password: generatePassword(passwordRequirement) + })); + + let client: MongoClient | null = null; + try { + client = await $getClient(); + const db = client.db(connection.credentials.database); + + for (const credentials of revokedCredentials) { + await db.command({ + updateUser: credentials.username, + pwd: credentials.password + }); + } + } catch (error) { + throw new Error(redactPasswords(error, revokedCredentials)); + } finally { + if (client) await client.close(); + } + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = { + username: activeIndex === 0 ? username2 : username1, + password: generatePassword(passwordRequirement) + }; + + let client: MongoClient | null = null; + try { + client = await $getClient(); + const db = client.db(connection.credentials.database); + + await db.command({ + updateUser: credentials.username, + pwd: credentials.password + }); + } catch (error) { + throw new Error(redactPasswords(error, [credentials])); + } finally { + if (client) await client.close(); + } + + await $validateCredentials(credentials); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const { username, password } = secretsMapping; + + const secrets = [ + { + key: username, + value: generatedCredentials.username + }, + { + key: password, + value: generatedCredentials.password + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts new file mode 100644 index 000000000..9a5335f5f --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { + SqlCredentialsRotationGeneratedCredentialsSchema, + SqlCredentialsRotationParametersSchema, + SqlCredentialsRotationTemplateSchema +} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const MongoDBCredentialsRotationGeneratedCredentialsSchema = SqlCredentialsRotationGeneratedCredentialsSchema; +export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema; +export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema; + +const MongoDBCredentialsRotationSecretsMappingSchema = z.object({ + username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.username), + password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.password) +}); + +export const MongoDBCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MongoDBCredentials).extend({ + type: z.literal(SecretRotation.MongoDBCredentials), + parameters: MongoDBCredentialsRotationParametersSchema, + secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema +}); + +export const CreateMongoDBCredentialsRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.MongoDBCredentials +).extend({ + parameters: MongoDBCredentialsRotationParametersSchema, + secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema +}); + +export const UpdateMongoDBCredentialsRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.MongoDBCredentials +).extend({ + parameters: MongoDBCredentialsRotationParametersSchema.optional(), + secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema.optional() +}); + +export const MongoDBCredentialsRotationListItemSchema = z.object({ + name: z.literal("MongoDB Credentials"), + connection: z.literal(AppConnection.MongoDB), + type: z.literal(SecretRotation.MongoDBCredentials), + template: MongoDBCredentialsRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-types.ts new file mode 100644 index 000000000..3a53a8cc5 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-types.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { TMongoDBConnection } from "@app/services/app-connection/mongodb"; + +import { + CreateMongoDBCredentialsRotationSchema, + MongoDBCredentialsRotationGeneratedCredentialsSchema, + MongoDBCredentialsRotationListItemSchema, + MongoDBCredentialsRotationSchema +} from "./mongodb-credentials-rotation-schemas"; + +export type TMongoDBCredentialsRotation = z.infer; + +export type TMongoDBCredentialsRotationInput = z.infer; + +export type TMongoDBCredentialsRotationListItem = z.infer; + +export type TMongoDBCredentialsRotationWithConnection = TMongoDBCredentialsRotation & { + connection: TMongoDBConnection; +}; + +export type TMongoDBCredentialsRotationGeneratedCredentials = z.infer< + typeof MongoDBCredentialsRotationGeneratedCredentialsSchema +>; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 661a2399a..470a63849 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -8,7 +8,8 @@ export enum SecretRotation { AwsIamUserSecret = "aws-iam-user-secret", LdapPassword = "ldap-password", OktaClientSecret = "okta-client-secret", - RedisCredentials = "redis-credentials" + RedisCredentials = "redis-credentials", + MongoDBCredentials = "mongodb-credentials" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index e4e6a8531..bb774c4be 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -9,6 +9,7 @@ import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret" import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret"; import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"; import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password"; +import { MONGODB_CREDENTIALS_ROTATION_LIST_OPTION } from "./mongodb-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials"; import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret"; @@ -37,7 +38,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 2087fa195..c2b0714ab 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -11,7 +11,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", [SecretRotation.LdapPassword]: "LDAP Password", [SecretRotation.OktaClientSecret]: "Okta Client Secret", - [SecretRotation.RedisCredentials]: "Redis Credentials" + [SecretRotation.RedisCredentials]: "Redis Credentials", + [SecretRotation.MongoDBCredentials]: "MongoDB Credentials" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { @@ -24,5 +25,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = async ( callback ) => { - // For SQL, since we get existing users, we change both their passwords - // on issue to invalidate their existing passwords // For SQL, since we get existing users, we change both their passwords // on issue to invalidate their existing passwords const credentialsSet = [ diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 4cf6160c7..81b0c0de2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2874,6 +2874,12 @@ export const SecretRotations = { }, REDIS_CREDENTIALS: { permissionScope: "The ACL permission scope to assign to the issued Redis users." + }, + MONGODB_CREDENTIALS: { + username1: + "The username of the first MongoDB user to rotate passwords for. This user must already exist in your database.", + username2: + "The username of the second MongoDB user to rotate passwords for. This user must already exist in your database." } }, SECRETS_MAPPING: { @@ -2904,6 +2910,10 @@ export const SecretRotations = { OKTA_CLIENT_SECRET: { clientId: "The name of the secret that the client ID will be mapped to.", clientSecret: "The name of the secret that the rotated client secret will be mapped to." + }, + MONGODB_CREDENTIALS: { + username: "The name of the secret that the active username will be mapped to.", + password: "The name of the secret that the generated password will be mapped to." } } }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 21e83c2b7..14eb60192 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -286,6 +286,10 @@ const envSchema = z DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default( process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY ), + + // PAM AWS credentials (for AWS IAM PAM resource type) + PAM_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()), + PAM_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()), /* ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */ diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index dab9d3278..b5497ca21 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -183,3 +183,23 @@ export class CryptographyError extends Error { this.error = error; } } + +export class PolicyViolationError extends Error { + name: string; + + error: unknown; + + details?: unknown; + + constructor({ + name, + error, + message, + details + }: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) { + super(message || "A policy is in place for this resource"); + this.name = name || "PolicyViolationError"; + this.error = error; + this.details = details; + } +} diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index c46e9c023..0243c81f2 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -83,6 +83,7 @@ export enum QueueName { HealthAlert = "health-alert", CertificateV3AutoRenewal = "certificate-v3-auto-renewal", PamAccountRotation = "pam-account-rotation", + PamSessionExpiration = "pam-session-expiration", PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } @@ -138,6 +139,7 @@ export enum QueueJobs { HealthAlert = "health-alert", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", PamAccountRotation = "pam-account-rotation", + PamSessionExpiration = "pam-session-expiration", PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } @@ -404,6 +406,10 @@ export type TQueueJobTypes = { name: QueueJobs.PamAccountRotation; payload: undefined; }; + [QueueName.PamSessionExpiration]: { + name: QueueJobs.PamSessionExpiration; + payload: { sessionId: string }; + }; [QueueName.PkiAcmeChallengeValidation]: { name: QueueJobs.PkiAcmeChallengeValidation; payload: { challengeId: string }; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index e703df5ef..df1988ce3 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -17,6 +17,7 @@ import { NotFoundError, OidcAuthError, PermissionBoundaryError, + PolicyViolationError, RateLimitError, ScimRequestError, UnauthorizedError @@ -255,6 +256,14 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider detail: error.message // TODO: add subproblems if they exist }); + } else if (error instanceof PolicyViolationError) { + void res.status(HttpStatusCodes.Forbidden).send({ + reqId: req.id, + statusCode: HttpStatusCodes.Forbidden, + error: "PolicyViolationError", + message: error.message, + details: error.details + }); } else { void res.status(HttpStatusCodes.InternalServerError).send({ reqId: req.id, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 914491d3c..6611f4a59 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -159,6 +159,19 @@ import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { + approvalPolicyDALFactory, + approvalPolicyStepApproversDALFactory, + approvalPolicyStepsDALFactory +} from "@app/services/approval-policy/approval-policy-dal"; +import { approvalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service"; +import { + approvalRequestApprovalsDALFactory, + approvalRequestDALFactory, + approvalRequestGrantsDALFactory, + approvalRequestStepEligibleApproversDALFactory, + approvalRequestStepsDALFactory +} from "@app/services/approval-policy/approval-request-dal"; import { authDALFactory } from "@app/services/auth/auth-dal"; import { authLoginServiceFactory } from "@app/services/auth/auth-login-service"; import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service"; @@ -279,6 +292,7 @@ import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue"; +import { pamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue"; import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; @@ -1916,6 +1930,9 @@ export const registerRoutes = async ( identityDAL }); + const approvalRequestDAL = approvalRequestDALFactory(db); + const approvalRequestGrantsDAL = approvalRequestGrantsDALFactory(db); + // DAILY const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ scimService, @@ -1931,7 +1948,9 @@ export const registerRoutes = async ( serviceTokenService, orgService, userNotificationDAL, - keyValueStoreDAL + keyValueStoreDAL, + approvalRequestDAL, + approvalRequestGrantsDAL }); const healthAlert = healthAlertServiceFactory({ @@ -2412,6 +2431,12 @@ export const registerRoutes = async ( gatewayV2Service }); + const approvalPolicyDAL = approvalPolicyDALFactory(db); + const pamSessionExpirationService = pamSessionExpirationServiceFactory({ + queueService, + pamSessionDAL + }); + const pamAccountService = pamAccountServiceFactory({ pamAccountDAL, gatewayV2Service, @@ -2423,7 +2448,10 @@ export const registerRoutes = async ( permissionService, projectDAL, userDAL, - auditLogService + auditLogService, + approvalRequestGrantsDAL, + approvalPolicyDAL, + pamSessionExpirationService }); const pamAccountRotation = pamAccountRotationServiceFactory({ @@ -2451,6 +2479,27 @@ export const registerRoutes = async ( auditLogService }); + const approvalPolicyStepsDAL = approvalPolicyStepsDALFactory(db); + const approvalPolicyStepApproversDAL = approvalPolicyStepApproversDALFactory(db); + const approvalRequestStepsDAL = approvalRequestStepsDALFactory(db); + const approvalRequestStepEligibleApproversDAL = approvalRequestStepEligibleApproversDALFactory(db); + const approvalRequestApprovalsDAL = approvalRequestApprovalsDALFactory(db); + + const approvalPolicyService = approvalPolicyServiceFactory({ + approvalPolicyDAL, + approvalPolicyStepsDAL, + approvalPolicyStepApproversDAL, + permissionService, + projectMembershipDAL, + approvalRequestDAL, + approvalRequestStepsDAL, + approvalRequestStepEligibleApproversDAL, + approvalRequestApprovalsDAL, + userGroupMembershipDAL, + notificationService, + approvalRequestGrantsDAL + }); + // setup the communication with license key server await licenseService.init(); @@ -2490,6 +2539,7 @@ export const registerRoutes = async ( await healthAlert.init(); await pkiSyncCleanup.init(); await pamAccountRotation.init(); + await pamSessionExpirationService.init(); await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyExpiringPkiItemAlert.startSendingAlerts(); @@ -2630,7 +2680,8 @@ export const registerRoutes = async ( additionalPrivilege: additionalPrivilegeService, identityProject: identityProjectService, convertor: convertorService, - pkiAlertV2: pkiAlertV2Service + pkiAlertV2: pkiAlertV2Service, + approvalPolicy: approvalPolicyService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts index e44b9af4e..0feb1ba55 100644 --- a/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts +++ b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts @@ -2,6 +2,8 @@ import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; import { UnpackedPermissionSchema } from "./permission"; -export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({ +export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.omit({ + projectMembershipId: true +}).extend({ permissions: UnpackedPermissionSchema.array() }); diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 48fdc7c38..072abbadb 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -87,6 +87,10 @@ import { SanitizedLaravelForgeConnectionSchema } from "@app/services/app-connection/laravel-forge"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; +import { + MongoDBConnectionListItemSchema, + SanitizedMongoDBConnectionSchema +} from "@app/services/app-connection/mongodb"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql"; import { @@ -173,6 +177,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedOktaConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options, ...SanitizedRedisConnectionSchema.options, + ...SanitizedMongoDBConnectionSchema.options, ...SanitizedLaravelForgeConnectionSchema.options, ...SanitizedChefConnectionSchema.options, ...SanitizedDNSMadeEasyConnectionSchema.options @@ -219,6 +224,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ OktaConnectionListItemSchema, AzureADCSConnectionListItemSchema, RedisConnectionListItemSchema, + MongoDBConnectionListItemSchema, LaravelForgeConnectionListItemSchema, ChefConnectionListItemSchema, DNSMadeEasyConnectionListItemSchema diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index d7a4065fd..0738f0407 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -16,8 +16,8 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; -import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; +import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -28,6 +28,7 @@ import { registerHerokuConnectionRouter } from "./heroku-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLaravelForgeConnectionRouter } from "./laravel-forge-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; +import { registerMongoDBConnectionRouter } from "./mongodb-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerNetlifyConnectionRouter } from "./netlify-connection-router"; @@ -90,5 +91,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.MongoDB, + server, + sanitizedResponseSchema: SanitizedMongoDBConnectionSchema, + createSchema: CreateMongoDBConnectionSchema, + updateSchema: UpdateMongoDBConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts b/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts new file mode 100644 index 000000000..91e042542 --- /dev/null +++ b/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts @@ -0,0 +1,625 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { BadRequestError } from "@app/lib/errors"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; +import { + TApprovalPolicy, + TCreatePolicyDTO, + TCreateRequestDTO, + TUpdatePolicyDTO +} from "@app/services/approval-policy/approval-policy-types"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerApprovalPolicyEndpoints =

({ + server, + policyType, + createPolicySchema, + updatePolicySchema, + policyResponseSchema, + createRequestSchema, + requestResponseSchema, + grantResponseSchema +}: { + server: FastifyZodProvider; + policyType: ApprovalPolicyType; + createPolicySchema: z.ZodType< + TCreatePolicyDTO & { + conditions: P["conditions"]["conditions"]; + constraints: P["constraints"]["constraints"]; + } + >; + updatePolicySchema: z.ZodType< + TUpdatePolicyDTO & { + conditions?: P["conditions"]["conditions"]; + constraints?: P["constraints"]["constraints"]; + } + >; + policyResponseSchema: z.ZodTypeAny; + createRequestSchema: z.ZodType; + requestResponseSchema: z.ZodTypeAny; + grantResponseSchema: z.ZodTypeAny; +}) => { + // Policies + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create approval policy", + body: createPolicySchema, + response: { + 200: z.object({ + policy: policyResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policy } = await server.services.approvalPolicy.create(policyType, req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.body.projectId, + event: { + type: EventType.APPROVAL_POLICY_CREATE, + metadata: { + policyType, + name: req.body.name + } + } + }); + + return { policy }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval policies", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + policies: z.array(policyResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policies } = await server.services.approvalPolicy.list(policyType, req.query.projectId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.APPROVAL_POLICY_LIST, + metadata: { + policyType, + count: policies.length + } + } + }); + + return { policies }; + } + }); + + server.route({ + method: "GET", + url: "/:policyId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval policy", + params: z.object({ + policyId: z.string().uuid() + }), + response: { + 200: z.object({ + policy: policyResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policy } = await server.services.approvalPolicy.getById(req.params.policyId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: policy.projectId, + event: { + type: EventType.APPROVAL_POLICY_GET, + metadata: { + policyType, + policyId: policy.id, + name: policy.name + } + } + }); + + return { policy }; + } + }); + + server.route({ + method: "PATCH", + url: "/:policyId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update approval policy", + params: z.object({ + policyId: z.string().uuid() + }), + body: updatePolicySchema, + response: { + 200: z.object({ + policy: policyResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policy } = await server.services.approvalPolicy.updateById(req.params.policyId, req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: policy.projectId, + event: { + type: EventType.APPROVAL_POLICY_UPDATE, + metadata: { + policyType, + policyId: policy.id, + name: policy.name + } + } + }); + + return { policy }; + } + }); + + server.route({ + method: "DELETE", + url: "/:policyId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete approval policy", + params: z.object({ + policyId: z.string().uuid() + }), + response: { + 200: z.object({ + policyId: z.string().uuid() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policyId, projectId } = await server.services.approvalPolicy.deleteById( + req.params.policyId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event: { + type: EventType.APPROVAL_POLICY_DELETE, + metadata: { + policyType, + policyId + } + } + }); + + return { policyId }; + } + }); + + // Requests + server.route({ + method: "GET", + url: "/requests", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval requests", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + requests: z.array(requestResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { requests } = await server.services.approvalPolicy.listRequests( + policyType, + req.query.projectId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.APPROVAL_REQUEST_LIST, + metadata: { + policyType, + count: requests.length + } + } + }); + + return { requests }; + } + }); + + server.route({ + method: "POST", + url: "/requests", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create approval request", + body: createRequestSchema, + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + // To prevent type errors when accessing req.auth.user + if (req.auth.authMode !== AuthMode.JWT) { + throw new BadRequestError({ message: "You can only request access using JWT auth tokens." }); + } + + const { request } = await server.services.approvalPolicy.createRequest( + policyType, + { + requesterName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(), + requesterEmail: req.auth.user.email ?? "", + ...req.body + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_CREATE, + metadata: { + policyType, + justification: req.body.justification || undefined, + requestDuration: req.body.requestDuration || "infinite" + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "GET", + url: "/requests/:requestId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval request", + params: z.object({ + requestId: z.string().uuid() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.getRequestById(req.params.requestId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GET, + metadata: { + policyType, + requestId: request.id, + status: request.status + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "POST", + url: "/requests/:requestId/approve", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Approve approval request", + params: z.object({ + requestId: z.string().uuid() + }), + body: z.object({ + comment: z.string().optional() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.approveRequest( + req.params.requestId, + req.body, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_APPROVE, + metadata: { + policyType, + requestId: req.params.requestId, + comment: req.body.comment + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "POST", + url: "/requests/:requestId/reject", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Reject approval request", + params: z.object({ + requestId: z.string().uuid() + }), + body: z.object({ + comment: z.string().optional() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.rejectRequest( + req.params.requestId, + req.body, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_REJECT, + metadata: { + policyType, + requestId: req.params.requestId, + comment: req.body.comment + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "POST", + url: "/requests/:requestId/cancel", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Cancel approval request", + params: z.object({ + requestId: z.string().uuid() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.cancelRequest(req.params.requestId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_CANCEL, + metadata: { + policyType, + requestId: req.params.requestId + } + } + }); + + return { request }; + } + }); + + // Grants + server.route({ + method: "GET", + url: "/grants", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval grants", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + grants: z.array(grantResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grants } = await server.services.approvalPolicy.listGrants( + policyType, + req.query.projectId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GRANT_LIST, + metadata: { + policyType, + count: grants.length + } + } + }); + + return { grants }; + } + }); + + server.route({ + method: "GET", + url: "/grants/:grantId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval grant", + params: z.object({ + grantId: z.string().uuid() + }), + response: { + 200: z.object({ + grant: grantResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grant } = await server.services.approvalPolicy.getGrantById(req.params.grantId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: grant.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GRANT_GET, + metadata: { + policyType, + grantId: grant.id, + status: grant.status + } + } + }); + + return { grant }; + } + }); + + server.route({ + method: "POST", + url: "/grants/:grantId/revoke", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Revoke approval grant", + params: z.object({ + grantId: z.string().uuid() + }), + body: z.object({ + revocationReason: z.string().optional() + }), + response: { + 200: z.object({ + grant: grantResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grant } = await server.services.approvalPolicy.revokeGrant(req.params.grantId, req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: grant.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GRANT_REVOKE, + metadata: { + policyType, + grantId: grant.id, + revocationReason: req.body.revocationReason + } + } + }); + + return { grant }; + } + }); +}; diff --git a/backend/src/server/routes/v1/approval-policy-routers/index.ts b/backend/src/server/routes/v1/approval-policy-routers/index.ts new file mode 100644 index 000000000..c848e2e47 --- /dev/null +++ b/backend/src/server/routes/v1/approval-policy-routers/index.ts @@ -0,0 +1,29 @@ +import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; +import { + CreatePamAccessPolicySchema, + CreatePamAccessRequestSchema, + PamAccessPolicySchema, + PamAccessRequestGrantSchema, + PamAccessRequestSchema, + UpdatePamAccessPolicySchema +} from "@app/services/approval-policy/pam-access/pam-access-policy-schemas"; + +import { registerApprovalPolicyEndpoints } from "./approval-policy-endpoints"; + +export const APPROVAL_POLICY_REGISTER_ROUTER_MAP: Record< + ApprovalPolicyType, + (server: FastifyZodProvider) => Promise +> = { + [ApprovalPolicyType.PamAccess]: async (server: FastifyZodProvider) => { + registerApprovalPolicyEndpoints({ + server, + policyType: ApprovalPolicyType.PamAccess, + createPolicySchema: CreatePamAccessPolicySchema, + updatePolicySchema: UpdatePamAccessPolicySchema, + policyResponseSchema: PamAccessPolicySchema, + createRequestSchema: CreatePamAccessRequestSchema, + requestResponseSchema: PamAccessRequestSchema, + grantResponseSchema: PamAccessRequestGrantSchema + }); + } +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index c27399453..3b4b10b88 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -7,6 +7,7 @@ import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router" import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers"; import { registerAdminRouter } from "./admin-router"; +import { APPROVAL_POLICY_REGISTER_ROUTER_MAP } from "./approval-policy-routers"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerCaRouter } from "./certificate-authority-router"; @@ -275,4 +276,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerEventRouter, { prefix: "/events" }); await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" }); + + await server.register( + async (approvalPolicyRouter) => { + // Register policy type-specific endpoints + for await (const [type, router] of Object.entries(APPROVAL_POLICY_REGISTER_ROUTER_MAP)) { + await approvalPolicyRouter.register(router, { prefix: `/${type}` }); + } + }, + { prefix: "/approval-policies" } + ); }; diff --git a/backend/src/services/additional-privilege/additional-privilege-service.ts b/backend/src/services/additional-privilege/additional-privilege-service.ts index 2af9e6419..69f103c85 100644 --- a/backend/src/services/additional-privilege/additional-privilege-service.ts +++ b/backend/src/services/additional-privilege/additional-privilege-service.ts @@ -79,7 +79,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; } @@ -103,7 +106,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -136,7 +142,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; } @@ -158,7 +167,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -179,7 +191,10 @@ export const additionalPrivilegeServiceFactory = ({ const additionalPrivilege = await additionalPrivilegeDAL.deleteById(existingPrivilege.id); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -199,7 +214,10 @@ export const additionalPrivilegeServiceFactory = ({ throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -219,7 +237,10 @@ export const additionalPrivilegeServiceFactory = ({ throw new NotFoundError({ message: `Additional privilege with name ${selector.name} doesn't exist` }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 8e0260c01..e7e2bca76 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -39,6 +39,7 @@ export enum AppConnection { Netlify = "netlify", Okta = "okta", Redis = "redis", + MongoDB = "mongodb", LaravelForge = "laravel-forge", Chef = "chef", Northflank = "northflank" diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index d8af3773b..f28508efb 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -119,6 +119,7 @@ import { validateLaravelForgeConnectionCredentials } from "./laravel-forge"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; +import { getMongoDBConnectionListItem, MongoDBConnectionMethod, validateMongoDBConnectionCredentials } from "./mongodb"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; @@ -224,6 +225,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getNorthflankConnectionListItem(), getOktaConnectionListItem(), getRedisConnectionListItem(), + getMongoDBConnectionListItem(), getChefConnectionListItem() ] .filter((option) => { @@ -357,7 +359,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); @@ -411,6 +414,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case OracleDBConnectionMethod.UsernameAndPassword: case AzureADCSConnectionMethod.UsernamePassword: case RedisConnectionMethod.UsernameAndPassword: + case MongoDBConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken: @@ -504,6 +508,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Northflank]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Redis]: platformManagedCredentialsNotSupported, + [AppConnection.MongoDB]: platformManagedCredentialsNotSupported, [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported, [AppConnection.Chef]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 27d6a27a8..a41589d12 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -42,6 +42,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Netlify]: "Netlify", [AppConnection.Okta]: "Okta", [AppConnection.Redis]: "Redis", + [AppConnection.MongoDB]: "MongoDB", [AppConnection.Chef]: "Chef", [AppConnection.Northflank]: "Northflank" }; @@ -88,6 +89,7 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "MongoDB" as const, + app: AppConnection.MongoDB as const, + methods: Object.values(MongoDBConnectionMethod) as [MongoDBConnectionMethod.UsernameAndPassword], + supportsPlatformManagement: false as const + }; +}; + +export type TMongoDBConnectionCredentials = { + host: string; + port?: number; + database: string; + username: string; + password: string; + tlsEnabled?: boolean; + tlsRejectUnauthorized?: boolean; + tlsCertificate?: string; +}; + +export type TCreateMongoClientOptions = { + authCredentials?: { username: string; password: string }; + validateConnection?: boolean; +}; + +const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000; + +export const createMongoClient = async ( + credentials: TMongoDBConnectionCredentials, + options?: TCreateMongoClientOptions +): Promise => { + const srvRegex = new RE2("^mongodb\\+srv:\\/\\/"); + const protocolRegex = new RE2("^mongodb:\\/\\/"); + + let normalizedHost = credentials.host.trim(); + const isSrvFromHost = srvRegex.test(normalizedHost); + if (isSrvFromHost) { + normalizedHost = srvRegex.replace(normalizedHost, ""); + } else if (protocolRegex.test(normalizedHost)) { + normalizedHost = protocolRegex.replace(normalizedHost, ""); + } + + const [hostIp] = await verifyHostInputValidity(normalizedHost); + + const isSrv = !credentials.port || isSrvFromHost; + const uri = isSrv ? `mongodb+srv://${hostIp}` : `mongodb://${hostIp}:${credentials.port}`; + + const authCredentials = options?.authCredentials ?? { + username: credentials.username, + password: credentials.password + }; + + const clientOptions: { + auth?: { username: string; password?: string }; + authSource?: string; + tls?: boolean; + tlsInsecure?: boolean; + ca?: string; + directConnection?: boolean; + connectTimeoutMS?: number; + serverSelectionTimeoutMS?: number; + socketTimeoutMS?: number; + } = { + auth: { + username: authCredentials.username, + password: authCredentials.password + }, + authSource: isSrv ? undefined : credentials.database, + directConnection: !isSrv, + connectTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS, + serverSelectionTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS, + socketTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS + }; + + if (credentials.tlsEnabled) { + clientOptions.tls = true; + clientOptions.tlsInsecure = !credentials.tlsRejectUnauthorized; + if (credentials.tlsCertificate) { + clientOptions.ca = credentials.tlsCertificate; + } + } + + const client = new MongoClient(uri, clientOptions); + + if (options?.validateConnection) { + await client + .db(credentials.database) + .command({ ping: 1 }) + .then(() => true); + } + + return client; +}; + +export const validateMongoDBConnectionCredentials = async (config: TMongoDBConnectionConfig) => { + let client: MongoClient | null = null; + try { + client = await createMongoClient(config.credentials, { validateConnection: true }); + + if (client) await client.close(); + + return config.credentials; + } catch (err) { + if (err instanceof BadRequestError) { + throw err; + } + throw new BadRequestError({ + message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}` + }); + } finally { + if (client) await client.close(); + } +}; diff --git a/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts b/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts new file mode 100644 index 000000000..c934a0741 --- /dev/null +++ b/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts @@ -0,0 +1,89 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AppConnection } from "../app-connection-enums"; +import { MongoDBConnectionMethod } from "./mongodb-connection-enums"; + +export const BaseMongoDBUsernameAndPasswordConnectionSchema = z.object({ + host: z.string().toLowerCase().min(1), + port: z.coerce.number(), + username: z.string().min(1), + password: z.string().min(1), + database: z.string().min(1).trim(), + + tlsRejectUnauthorized: z.boolean(), + tlsEnabled: z.boolean(), + tlsCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() +}); + +export const MongoDBConnectionAccessTokenCredentialsSchema = BaseMongoDBUsernameAndPasswordConnectionSchema; + +const BaseMongoDBConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.MongoDB) }); + +export const MongoDBConnectionSchema = BaseMongoDBConnectionSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: MongoDBConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedMongoDBConnectionSchema = z.discriminatedUnion("method", [ + BaseMongoDBConnectionSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: MongoDBConnectionAccessTokenCredentialsSchema.pick({ + host: true, + port: true, + username: true, + database: true, + tlsEnabled: true, + tlsRejectUnauthorized: true, + tlsCertificate: true + }) + }) +]); + +export const ValidateMongoDBConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(MongoDBConnectionMethod.UsernameAndPassword) + .describe(AppConnections.CREATE(AppConnection.MongoDB).method), + credentials: MongoDBConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.MongoDB).credentials + ) + }) +]); + +export const CreateMongoDBConnectionSchema = ValidateMongoDBConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.MongoDB, { + supportsPlatformManagedCredentials: false, + supportsGateways: false + }) +); + +export const UpdateMongoDBConnectionSchema = z + .object({ + credentials: MongoDBConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.MongoDB).credentials + ) + }) + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MongoDB, { + supportsPlatformManagedCredentials: false, + supportsGateways: false + }) + ); + +export const MongoDBConnectionListItemSchema = z.object({ + name: z.literal("MongoDB"), + app: z.literal(AppConnection.MongoDB), + methods: z.nativeEnum(MongoDBConnectionMethod).array(), + supportsPlatformManagement: z.literal(false) +}); diff --git a/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts b/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts new file mode 100644 index 000000000..52212545a --- /dev/null +++ b/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts @@ -0,0 +1,22 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateMongoDBConnectionSchema, + MongoDBConnectionSchema, + ValidateMongoDBConnectionCredentialsSchema +} from "./mongodb-connection-schemas"; + +export type TMongoDBConnection = z.infer; + +export type TMongoDBConnectionInput = z.infer & { + app: AppConnection.MongoDB; +}; + +export type TValidateMongoDBConnectionCredentialsSchema = typeof ValidateMongoDBConnectionCredentialsSchema; + +export type TMongoDBConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/approval-policy/approval-policy-dal.ts b/backend/src/services/approval-policy/approval-policy-dal.ts new file mode 100644 index 000000000..67dd7a521 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-dal.ts @@ -0,0 +1,150 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums"; +import { ApprovalPolicyStep } from "./approval-policy-types"; + +// Approval Policy +export type TApprovalPolicyDALFactory = ReturnType; +export const approvalPolicyDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalPolicies); + + const findStepsByPolicyId = async (policyId: string) => { + try { + const dbInstance = db.replicaNode(); + const steps = await dbInstance(TableName.ApprovalPolicySteps).where({ policyId }).orderBy("stepNumber", "asc"); + + if (!steps.length) { + return []; + } + + const stepIds = steps.map((step) => step.id); + + const approvers = await dbInstance(TableName.ApprovalPolicyStepApprovers) + .whereIn("policyStepId", stepIds) + .select("policyStepId", "userId", "groupId"); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.policyStepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.policyStepId] = stepApprovers; + return acc; + }, + {} + ); + + return steps.map((step) => { + const stepApprovers = approversByStepId[step.id] || []; + + const formattedStep: ApprovalPolicyStep = { + requiredApprovals: step.requiredApprovals, + approvers: stepApprovers + }; + + if (step.name) { + formattedStep.name = step.name; + } + if (typeof step.notifyApprovers === "boolean") { + formattedStep.notifyApprovers = step.notifyApprovers; + } + + return formattedStep; + }); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval policy steps" }); + } + }; + + const findByProjectId = async (policyType: ApprovalPolicyType, projectId: string) => { + try { + const dbInstance = db.replicaNode(); + const policies = await dbInstance(TableName.ApprovalPolicies).where({ type: policyType, projectId }); + + if (!policies.length) { + return []; + } + + const policyIds = policies.map((p) => p.id); + + const steps = await dbInstance(TableName.ApprovalPolicySteps) + .whereIn("policyId", policyIds) + .orderBy("stepNumber", "asc"); + + const stepsByPolicyId: Record = {}; + + if (steps.length) { + const stepIds = steps.map((step) => step.id); + + const approvers = await dbInstance(TableName.ApprovalPolicyStepApprovers) + .whereIn("policyStepId", stepIds) + .select("policyStepId", "userId", "groupId"); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.policyStepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.policyStepId] = stepApprovers; + return acc; + }, + {} + ); + + steps.forEach((step) => { + const stepApprovers = approversByStepId[step.id] || []; + const formattedStep: ApprovalPolicyStep = { + requiredApprovals: step.requiredApprovals, + approvers: stepApprovers + }; + + if (step.name) { + formattedStep.name = step.name; + } + if (typeof step.notifyApprovers === "boolean") { + formattedStep.notifyApprovers = step.notifyApprovers; + } + + if (!stepsByPolicyId[step.policyId]) { + stepsByPolicyId[step.policyId] = []; + } + stepsByPolicyId[step.policyId].push(formattedStep); + }); + } + + return policies.map((policy) => ({ + ...policy, + steps: stepsByPolicyId[policy.id] || [] + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval policies by project id" }); + } + }; + + return { + ...orm, + findStepsByPolicyId, + findByProjectId + }; +}; + +// Approval Policy Steps +export type TApprovalPolicyStepsDALFactory = ReturnType; +export const approvalPolicyStepsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalPolicySteps); + return orm; +}; + +// Approval Policy Step Approvers +export type TApprovalPolicyStepApproversDALFactory = ReturnType; +export const approvalPolicyStepApproversDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalPolicyStepApprovers); + return orm; +}; diff --git a/backend/src/services/approval-policy/approval-policy-enums.ts b/backend/src/services/approval-policy/approval-policy-enums.ts new file mode 100644 index 000000000..bc57801f3 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-enums.ts @@ -0,0 +1,33 @@ +export enum ApprovalPolicyType { + PamAccess = "pam-access" +} + +export enum ApproverType { + Group = "group", + User = "user" +} + +export enum ApprovalRequestStatus { + Pending = "pending", + Approved = "approved", + Rejected = "rejected", + Expired = "expired", + Cancelled = "cancelled" +} + +export enum ApprovalRequestStepStatus { + Pending = "pending", + InProgress = "in-progress", + Completed = "completed" +} + +export enum ApprovalRequestApprovalDecision { + Approved = "approved", + Rejected = "rejected" +} + +export enum ApprovalRequestGrantStatus { + Active = "active", + Expired = "expired", + Revoked = "revoked" +} diff --git a/backend/src/services/approval-policy/approval-policy-factory.ts b/backend/src/services/approval-policy/approval-policy-factory.ts new file mode 100644 index 000000000..42ef11ac7 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-factory.ts @@ -0,0 +1,18 @@ +import { ApprovalPolicyType } from "./approval-policy-enums"; +import { + TApprovalPolicy, + TApprovalPolicyInputs, + TApprovalRequestData, + TApprovalResourceFactory +} from "./approval-policy-types"; +import { pamAccessPolicyFactory } from "./pam-access/pam-access-policy-factory"; + +type TApprovalPolicyFactoryImplementation = TApprovalResourceFactory< + TApprovalPolicyInputs, + TApprovalPolicy, + TApprovalRequestData +>; + +export const APPROVAL_POLICY_FACTORY_MAP: Record = { + [ApprovalPolicyType.PamAccess]: pamAccessPolicyFactory as TApprovalPolicyFactoryImplementation +}; diff --git a/backend/src/services/approval-policy/approval-policy-schemas.ts b/backend/src/services/approval-policy/approval-policy-schemas.ts new file mode 100644 index 000000000..d912e3245 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-schemas.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; + +import { + ApprovalPoliciesSchema, + ApprovalRequestApprovalsSchema, + ApprovalRequestGrantsSchema, + ApprovalRequestsSchema, + ApprovalRequestStepsSchema +} from "@app/db/schemas"; +import { ms } from "@app/lib/ms"; + +import { ApproverType } from "./approval-policy-enums"; + +const ApprovalPolicyStepSchema = z.object({ + name: z.string().min(1).max(128).nullable().optional(), + requiredApprovals: z.number().min(1).max(100), + notifyApprovers: z.boolean().nullable().optional(), + approvers: z + .object({ + type: z.nativeEnum(ApproverType), + id: z.string().uuid() + }) + .array() +}); + +const MaxRequestTtlSchema = z.string().refine( + (val) => { + const duration = ms(val) / 1000; + + // 1 hour to 30 days + return duration >= 3600 && duration <= 2592000; + }, + { message: "Duration must be between 1 hour and 30 days" } +); + +// Policy +export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({ + steps: ApprovalPolicyStepSchema.array() +}); + +export const BaseCreateApprovalPolicySchema = z.object({ + projectId: z.string().uuid(), + name: z.string().min(1).max(128), + maxRequestTtl: MaxRequestTtlSchema.nullable().optional(), + steps: ApprovalPolicyStepSchema.array() +}); + +export const BaseUpdateApprovalPolicySchema = z.object({ + name: z.string().min(1).max(128).optional(), + maxRequestTtl: MaxRequestTtlSchema.nullable().optional(), + steps: ApprovalPolicyStepSchema.array().optional() +}); + +// Request +const ApprovalRequestStepSchema = ApprovalRequestStepsSchema.extend({ + name: z.string().min(1).max(128).nullable().optional(), + requiredApprovals: z.number().min(1).max(100), + notifyApprovers: z.boolean().nullable().optional(), + stepNumber: z.number(), + status: z.string(), + startedAt: z.date().nullable().optional(), + completedAt: z.date().nullable().optional(), + approvers: z + .object({ + type: z.nativeEnum(ApproverType), + id: z.string().uuid() + }) + .array(), + approvals: ApprovalRequestApprovalsSchema.array() +}); + +export const BaseApprovalRequestSchema = ApprovalRequestsSchema.extend({ + steps: ApprovalRequestStepSchema.array() +}); + +export const BaseCreateApprovalRequestSchema = z.object({ + projectId: z.string().uuid(), + justification: z.string().max(256).nullable().optional(), + requestDuration: z + .string() + .refine( + (val) => { + const duration = ms(val) / 1000; + + // 1 minute to 30 days + return duration >= 60 && duration <= 2592000; + }, + { message: "Duration must be between 1 minute and 30 days" } + ) + .nullable() + .optional() +}); + +// Grants +export const BaseApprovalRequestGrantSchema = ApprovalRequestGrantsSchema; diff --git a/backend/src/services/approval-policy/approval-policy-service.ts b/backend/src/services/approval-policy/approval-policy-service.ts new file mode 100644 index 000000000..80fa820ac --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-service.ts @@ -0,0 +1,902 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType, ProjectMembershipRole, TApprovalPolicies, TApprovalRequests } from "@app/db/schemas"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { OrgServiceActor } from "@app/lib/types"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; + +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { + TApprovalPolicyDALFactory, + TApprovalPolicyStepApproversDALFactory, + TApprovalPolicyStepsDALFactory +} from "./approval-policy-dal"; +import { + ApprovalPolicyType, + ApprovalRequestApprovalDecision, + ApprovalRequestGrantStatus, + ApprovalRequestStatus, + ApprovalRequestStepStatus, + ApproverType +} from "./approval-policy-enums"; +import { APPROVAL_POLICY_FACTORY_MAP } from "./approval-policy-factory"; +import { + ApprovalPolicyStep, + TApprovalRequest, + TCreatePolicyDTO, + TCreateRequestDTO, + TUpdatePolicyDTO +} from "./approval-policy-types"; +import { + TApprovalRequestApprovalsDALFactory, + TApprovalRequestDALFactory, + TApprovalRequestGrantsDALFactory, + TApprovalRequestStepEligibleApproversDALFactory, + TApprovalRequestStepsDALFactory +} from "./approval-request-dal"; + +type TApprovalPolicyServiceFactoryDep = { + approvalPolicyDAL: TApprovalPolicyDALFactory; + approvalPolicyStepsDAL: TApprovalPolicyStepsDALFactory; + approvalPolicyStepApproversDAL: TApprovalPolicyStepApproversDALFactory; + approvalRequestApprovalsDAL: TApprovalRequestApprovalsDALFactory; + approvalRequestDAL: TApprovalRequestDALFactory; + approvalRequestStepsDAL: TApprovalRequestStepsDALFactory; + approvalRequestStepEligibleApproversDAL: TApprovalRequestStepEligibleApproversDALFactory; + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + notificationService: TNotificationServiceFactory; + permissionService: Pick; + projectMembershipDAL: Pick; +}; +export type TApprovalPolicyServiceFactory = ReturnType; + +export const approvalPolicyServiceFactory = ({ + approvalPolicyDAL, + approvalPolicyStepsDAL, + approvalPolicyStepApproversDAL, + approvalRequestApprovalsDAL, + approvalRequestDAL, + approvalRequestStepsDAL, + approvalRequestStepEligibleApproversDAL, + approvalRequestGrantsDAL, + userGroupMembershipDAL, + notificationService, + permissionService, + projectMembershipDAL +}: TApprovalPolicyServiceFactoryDep) => { + const $notifyApproversForStep = async (step: ApprovalPolicyStep, request: TApprovalRequests) => { + if (!step.notifyApprovers) return; + + const userIdsToNotify = new Set(); + + for await (const approver of step.approvers) { + if (approver.type === ApproverType.User) { + userIdsToNotify.add(approver.id); + } else if (approver.type === ApproverType.Group) { + const members = await userGroupMembershipDAL.find({ groupId: approver.id }); + members.forEach((member) => userIdsToNotify.add(member.userId)); + } + } + + if (userIdsToNotify.size === 0) return; + + // TODO: Potentially link to requests in the future to support click redirects + await notificationService.createUserNotifications( + Array.from(userIdsToNotify).map((userId) => ({ + userId, + orgId: request.organizationId, + type: NotificationType.APPROVAL_REQUIRED, + title: "Approval Required", + body: `You have a new approval request for ${request.type} from ${request.requesterName}.` + })) + ); + }; + + const $verifyProjectUserMembership = async (userIds: string[], orgId: string, projectId: string) => { + const uniqueUserIds = [...new Set(userIds)]; + if (uniqueUserIds.length === 0) return; + + const allMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, uniqueUserIds); + const projectMemberships = allMemberships.filter((membership) => membership.projectId === projectId); + + if (projectMemberships.length !== uniqueUserIds.length) { + const projectMemberUserIds = new Set(projectMemberships.map((membership) => membership.userId)); + const userIdsNotInProject = uniqueUserIds.filter((id) => !projectMemberUserIds.has(id)); + throw new BadRequestError({ + message: `Some users are not members of the project: ${userIdsNotInProject.join(", ")}` + }); + } + }; + + const create = async ( + policyType: ApprovalPolicyType, + { projectId, name, maxRequestTtl, conditions, constraints, steps }: TCreatePolicyDTO, + actor: OrgServiceActor + ) => { + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + // Verify all users are part of project + const approverUserIds = steps + .flatMap((step) => step.approvers ?? []) + .filter((approver) => approver.type === ApproverType.User) + .map((approver) => approver.id); + await $verifyProjectUserMembership(approverUserIds, actor.orgId, projectId); + + const policy = await approvalPolicyDAL.transaction(async (tx) => { + const newPolicy = await approvalPolicyDAL.create( + { + projectId, + organizationId: actor.orgId, + name, + maxRequestTtl, + conditions: { version: 1, conditions }, + constraints: { version: 1, constraints }, + type: policyType + }, + tx + ); + + // Create policy steps and their approvers + await Promise.all( + steps.map(async (step, i) => { + const newStep = await approvalPolicyStepsDAL.create( + { + policyId: newPolicy.id, + requiredApprovals: step.requiredApprovals, + stepNumber: i + 1, + name: step.name, + notifyApprovers: step.notifyApprovers + }, + tx + ); + + if (step.approvers?.length) { + await Promise.all( + step.approvers.map((approver) => + approvalPolicyStepApproversDAL.create( + { + policyStepId: newStep.id, + userId: approver.type === ApproverType.User ? approver.id : null, + groupId: approver.type === ApproverType.Group ? approver.id : null + }, + tx + ) + ) + ); + } + }) + ); + + return newPolicy; + }); + + return { + policy: { ...policy, steps } + }; + }; + + const list = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + const policies = await approvalPolicyDAL.findByProjectId(policyType, projectId); + + return { policies }; + }; + + const getById = async (policyId: string, actor: OrgServiceActor) => { + const policy = await approvalPolicyDAL.findById(policyId); + if (!policy) { + throw new ForbiddenRequestError({ message: "Policy not found" }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: policy.projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + const steps = await approvalPolicyDAL.findStepsByPolicyId(policyId); + + return { policy: { ...policy, steps } }; + }; + + const updateById = async ( + policyId: string, + { name, maxRequestTtl, conditions, constraints, steps }: TUpdatePolicyDTO, + actor: OrgServiceActor + ) => { + const policy = await approvalPolicyDAL.findById(policyId); + if (!policy) { + throw new ForbiddenRequestError({ message: "Policy not found" }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: policy.projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + if (steps !== undefined) { + // Verify all users are part of project + const approverUserIds = steps + .flatMap((step) => step.approvers ?? []) + .filter((approver) => approver.type === ApproverType.User) + .map((approver) => approver.id); + await $verifyProjectUserMembership(approverUserIds, actor.orgId, policy.projectId); + } + + const updatedPolicy = await approvalPolicyDAL.transaction(async (tx) => { + const updateDoc: Partial = {}; + + if (name !== undefined) { + updateDoc.name = name; + } + + if (maxRequestTtl !== undefined) { + updateDoc.maxRequestTtl = maxRequestTtl; + } + + if (conditions !== undefined) { + updateDoc.conditions = { version: 1, conditions }; + } + + if (constraints !== undefined) { + updateDoc.constraints = { version: 1, constraints }; + } + + const updated = await approvalPolicyDAL.updateById(policyId, updateDoc, tx); + + if (steps !== undefined) { + await approvalPolicyStepsDAL.delete({ policyId }, tx); + + await Promise.all( + steps.map(async (step, i) => { + const newStep = await approvalPolicyStepsDAL.create( + { + policyId, + requiredApprovals: step.requiredApprovals, + stepNumber: i + 1, + name: step.name, + notifyApprovers: step.notifyApprovers + }, + tx + ); + + if (step.approvers?.length) { + await Promise.all( + step.approvers.map((approver) => + approvalPolicyStepApproversDAL.create( + { + policyStepId: newStep.id, + userId: approver.type === ApproverType.User ? approver.id : null, + groupId: approver.type === ApproverType.Group ? approver.id : null + }, + tx + ) + ) + ); + } + }) + ); + } + return updated; + }); + + const fetchedSteps = await approvalPolicyDAL.findStepsByPolicyId(policyId); + + return { + policy: { ...updatedPolicy, steps: fetchedSteps } + }; + }; + + const deleteById = async (policyId: string, actor: OrgServiceActor) => { + const policy = await approvalPolicyDAL.findById(policyId); + if (!policy) { + throw new ForbiddenRequestError({ message: "Policy not found" }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: policy.projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + await approvalPolicyDAL.deleteById(policyId); + + return { + policyId, + projectId: policy.projectId + }; + }; + + const createRequest = async ( + policyType: ApprovalPolicyType, + { + projectId, + requestData, + requestDuration, + justification, + requesterName, + requesterEmail + }: TCreateRequestDTO & { + requesterName: string; + requesterEmail: string; + }, + actor: OrgServiceActor + ) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestActions.Create, + ProjectPermissionSub.ApprovalRequests + ); + + const fac = APPROVAL_POLICY_FACTORY_MAP[policyType](policyType); + + const policy = await fac.matchPolicy(approvalPolicyDAL, projectId, requestData); + + if (!policy) { + throw new ForbiddenRequestError({ + message: "No policies match the requested resource, you can access it without a request" + }); + } + + const constraintValidation = fac.validateConstraints(policy, requestData); + if (!constraintValidation.valid) { + const errorMessage = constraintValidation.errors + ? `Policy constraints not met: ${constraintValidation.errors.join("; ")}` + : "Policy constraints not met"; + throw new ForbiddenRequestError({ message: errorMessage }); + } + + let expiresAt: Date | undefined; + + if (requestDuration) { + const ttlMs = ms(requestDuration); + + expiresAt = new Date(Date.now() + ttlMs); + + if (policy.maxRequestTtl) { + const maxTtlMs = ms(policy.maxRequestTtl); + if (ttlMs > maxTtlMs) { + throw new BadRequestError({ + message: `Expiration time exceeds the maximum allowed TTL of ${policy.maxRequestTtl}` + }); + } + } + } + + const { request, steps } = await approvalRequestDAL.transaction(async (tx) => { + const newRequest = await approvalRequestDAL.create( + { + projectId, + organizationId: actor.orgId, + policyId: policy.id, + requesterId: actor.id, + requesterName, + requesterEmail, + type: policyType, + status: ApprovalRequestStatus.Pending, + justification, + currentStep: 1, + requestData: { version: 1, requestData }, + expiresAt + }, + tx + ); + + const newSteps = await Promise.all( + policy.steps.map(async (step, i) => { + const stepNum = i + 1; + const newStep = await approvalRequestStepsDAL.create( + { + requestId: newRequest.id, + stepNumber: stepNum, + name: step.name, + status: stepNum === 1 ? ApprovalRequestStepStatus.InProgress : ApprovalRequestStepStatus.Pending, + requiredApprovals: step.requiredApprovals, + notifyApprovers: step.notifyApprovers, + startedAt: stepNum === 1 ? new Date() : null + }, + tx + ); + + await Promise.all( + step.approvers.map((approver) => + approvalRequestStepEligibleApproversDAL.create( + { + stepId: newStep.id, + userId: approver.type === ApproverType.User ? approver.id : null, + groupId: approver.type === ApproverType.Group ? approver.id : null + }, + tx + ) + ) + ); + + return { + ...newStep, + approvers: step.approvers, + approvals: [] + }; + }) + ); + + return { request: newRequest, steps: newSteps }; + }); + + if (steps.length > 0) { + await $notifyApproversForStep(steps[0], request); + } + + return { + request: { ...request, steps } + }; + }; + + const getRequestById = async (requestId: string, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + + const isRequester = request.requesterId === actor.id; + + // Check if user is an eligible approver for any step + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isApprover = steps.some((step) => + step.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ) + ); + + // If user is requester or approver, allow access regardless of role permission + if (!isRequester && !isApprover) { + // Otherwise, check role permission + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: request.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestActions.Read, + ProjectPermissionSub.ApprovalRequests + ); + } + + return { + request: { ...request, steps } + }; + }; + + const approveRequest = async (requestId: string, { comment }: { comment?: string }, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + if (request.status !== ApprovalRequestStatus.Pending) { + throw new BadRequestError({ message: "Request is not pending" }); + } + + if (request.expiresAt && new Date(request.expiresAt) < new Date()) { + await approvalRequestDAL.updateById(requestId, { status: ApprovalRequestStatus.Expired }); + throw new BadRequestError({ message: "Request has expired" }); + } + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + const currentStepIndex = steps.findIndex((s) => s.stepNumber === request.currentStep); + if (currentStepIndex === -1) { + throw new BadRequestError({ message: "Current step not found" }); + } + + const currentStep = steps[currentStepIndex]; + + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isEligible = currentStep.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ); + + if (!isEligible) { + throw new ForbiddenRequestError({ message: "You are not an eligible approver for this step" }); + } + + const hasApproved = currentStep.approvals.some((a) => a.approverUserId === actor.id); + if (hasApproved) { + throw new BadRequestError({ message: "You have already approved this request" }); + } + + const { updatedRequest, nextStepToNotify } = await approvalRequestDAL.transaction(async (tx) => { + let nextStepToNotifyInner = null; + + // Create approval + await approvalRequestApprovalsDAL.create( + { + stepId: currentStep.id, + approverUserId: actor.id, + decision: ApprovalRequestApprovalDecision.Approved, + comment + }, + tx + ); + + const newApprovalCount = currentStep.approvals.length + 1; + if (newApprovalCount >= currentStep.requiredApprovals) { + // Step completed + await approvalRequestStepsDAL.updateById( + currentStep.id, + { + status: ApprovalRequestStepStatus.Completed, + completedAt: new Date() + }, + tx + ); + + const nextStep = steps[currentStepIndex + 1]; + if (nextStep) { + // Move to next step + await approvalRequestDAL.updateById( + requestId, + { + currentStep: request.currentStep + 1 + }, + tx + ); + + await approvalRequestStepsDAL.updateById( + nextStep.id, + { + status: ApprovalRequestStepStatus.InProgress, + startedAt: new Date() + }, + tx + ); + + if (nextStep.notifyApprovers) { + nextStepToNotifyInner = nextStep; + } + } else { + // All steps completed + const completedReq = await approvalRequestDAL.updateById( + requestId, + { + status: ApprovalRequestStatus.Approved + }, + tx + ); + + return { updatedRequest: completedReq, nextStepToNotify: null }; + } + } + + return { updatedRequest: request, nextStepToNotify: nextStepToNotifyInner }; + }); + + if (nextStepToNotify) { + await $notifyApproversForStep(nextStepToNotify, updatedRequest); + } + + // Fetch fresh state + const finalSteps = await approvalRequestDAL.findStepsByRequestId(requestId); + const finalRequest = await approvalRequestDAL.findById(requestId); + + const newRequest = { ...finalRequest, steps: finalSteps }; + + if (updatedRequest.status === ApprovalRequestStatus.Approved) { + const fac = APPROVAL_POLICY_FACTORY_MAP[updatedRequest.type as ApprovalPolicyType]( + updatedRequest.type as ApprovalPolicyType + ); + await fac.postApprovalRoutine(approvalRequestGrantsDAL, newRequest as TApprovalRequest); + } + + return { request: newRequest }; + }; + + const rejectRequest = async (requestId: string, { comment }: { comment?: string }, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + if (request.status !== ApprovalRequestStatus.Pending) { + throw new BadRequestError({ message: "Request is not pending" }); + } + + if (request.expiresAt && new Date(request.expiresAt) < new Date()) { + await approvalRequestDAL.updateById(requestId, { status: ApprovalRequestStatus.Expired }); + throw new BadRequestError({ message: "Request has expired" }); + } + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + const currentStep = steps.find((s) => s.stepNumber === request.currentStep); + + if (!currentStep) { + throw new BadRequestError({ message: "Current step not found" }); + } + + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isEligible = currentStep.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ); + + if (!isEligible) { + throw new ForbiddenRequestError({ message: "You are not an eligible approver for this step" }); + } + + await approvalRequestDAL.transaction(async (tx) => { + await approvalRequestApprovalsDAL.create( + { + stepId: currentStep.id, + approverUserId: actor.id, + decision: ApprovalRequestApprovalDecision.Rejected, + comment + }, + tx + ); + + await approvalRequestDAL.updateById( + requestId, + { + status: ApprovalRequestStatus.Rejected + }, + tx + ); + }); + + const finalSteps = await approvalRequestDAL.findStepsByRequestId(requestId); + const finalRequest = await approvalRequestDAL.findById(requestId); + + return { request: { ...finalRequest, steps: finalSteps } }; + }; + + const listRequests = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + const hasReadPermission = permission.can( + ProjectPermissionApprovalRequestActions.Read, + ProjectPermissionSub.ApprovalRequests + ); + + const requests = await approvalRequestDAL.findByProjectId(policyType, projectId); + + // If user has read permission, return all requests + if (hasReadPermission) { + return { requests }; + } + + // Otherwise, filter to only requests where user is requester or approver + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const filteredRequests = []; + for (const request of requests) { + const isRequester = request.requesterId === actor.id; + + if (isRequester) { + filteredRequests.push(request); + // eslint-disable-next-line no-continue + continue; + } + + // Check if user is an eligible approver for any step + const isApprover = request.steps.some((step) => + step.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ) + ); + + if (isApprover) { + filteredRequests.push(request); + } + } + + return { requests: filteredRequests }; + }; + + const cancelRequest = async (requestId: string, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + if (request.status !== ApprovalRequestStatus.Pending) { + throw new BadRequestError({ message: "Request is not pending" }); + } + + if (request.requesterId !== actor.id) { + throw new ForbiddenRequestError({ message: "You are not the requester of this request" }); + } + + const updatedRequest = await approvalRequestDAL.updateById(requestId, { + status: ApprovalRequestStatus.Cancelled + }); + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + + return { request: { ...updatedRequest, steps } }; + }; + + const listGrants = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Read, + ProjectPermissionSub.ApprovalRequestGrants + ); + + const grants = await approvalRequestGrantsDAL.find({ projectId, type: policyType }); + return { grants }; + }; + + const getGrantById = async (grantId: string, actor: OrgServiceActor) => { + const grant = await approvalRequestGrantsDAL.findById(grantId); + if (!grant) { + throw new NotFoundError({ message: "Grant not found" }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: grant.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Read, + ProjectPermissionSub.ApprovalRequestGrants + ); + + return { grant }; + }; + + const revokeGrant = async ( + grantId: string, + { revocationReason }: { revocationReason?: string }, + actor: OrgServiceActor + ) => { + const grant = await approvalRequestGrantsDAL.findById(grantId); + if (!grant) { + throw new NotFoundError({ message: "Grant not found" }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: grant.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Revoke, + ProjectPermissionSub.ApprovalRequestGrants + ); + + if (grant.status !== ApprovalRequestGrantStatus.Active) { + throw new BadRequestError({ message: "Grant is not active" }); + } + + const updatedGrant = await approvalRequestGrantsDAL.updateById(grantId, { + status: ApprovalRequestGrantStatus.Revoked, + revokedAt: new Date(), + revokedByUserId: actor.id, + revocationReason + }); + + return { grant: updatedGrant }; + }; + + return { + create, + list, + getById, + updateById, + deleteById, + createRequest, + listRequests, + getRequestById, + approveRequest, + rejectRequest, + cancelRequest, + listGrants, + getGrantById, + revokeGrant + }; +}; diff --git a/backend/src/services/approval-policy/approval-policy-types.ts b/backend/src/services/approval-policy/approval-policy-types.ts new file mode 100644 index 000000000..8dccc1453 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-types.ts @@ -0,0 +1,88 @@ +import { TApprovalPolicyDALFactory } from "@app/services/approval-policy/approval-policy-dal"; +import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal"; + +import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums"; +import { + TPamAccessPolicy, + TPamAccessPolicyConditions, + TPamAccessPolicyConstraints, + TPamAccessPolicyInputs, + TPamAccessRequest, + TPamAccessRequestData +} from "./pam-access/pam-access-policy-types"; + +export type TApprovalPolicy = TPamAccessPolicy; +export type TApprovalPolicyInputs = TPamAccessPolicyInputs; +export type TApprovalPolicyConditions = TPamAccessPolicyConditions; +export type TApprovalPolicyConstraints = TPamAccessPolicyConstraints; + +export type TApprovalRequest = TPamAccessRequest; +export type TApprovalRequestData = TPamAccessRequestData; + +export interface ApprovalPolicyStep { + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean | null; + approvers: { + type: ApproverType; + id: string; + }[]; +} + +// Policy DTOs +export interface TCreatePolicyDTO { + projectId: TApprovalPolicy["projectId"]; + name: TApprovalPolicy["name"]; + maxRequestTtl?: TApprovalPolicy["maxRequestTtl"]; + conditions: TApprovalPolicy["conditions"]["conditions"]; + constraints: TApprovalPolicy["constraints"]["constraints"]; + steps: ApprovalPolicyStep[]; +} + +export interface TUpdatePolicyDTO { + name?: TApprovalPolicy["name"]; + maxRequestTtl?: TApprovalPolicy["maxRequestTtl"]; + conditions?: TApprovalPolicy["conditions"]["conditions"]; + constraints?: TApprovalPolicy["constraints"]["constraints"]; + steps?: ApprovalPolicyStep[]; +} + +// Request DTOs +export interface TCreateRequestDTO { + projectId: TApprovalRequest["projectId"]; + requestData: TApprovalRequest["requestData"]["requestData"]; + justification?: TApprovalRequest["justification"]; + requestDuration?: string | null; +} + +// Factory +export type TApprovalRequestFactoryMatchPolicy = ( + approvalPolicyDAL: TApprovalPolicyDALFactory, + projectId: string, + inputs: I +) => Promise

; +export type TApprovalRequestFactoryCanAccess = ( + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory, + projectId: string, + userId: string, + inputs: I +) => Promise; +export type TApprovalRequestFactoryValidateConstraints

= ( + policy: P, + inputs: R +) => { valid: boolean; errors?: string[] }; +export type TApprovalRequestFactoryPostApprovalRoutine = ( + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory, + request: TApprovalRequest +) => Promise; + +export type TApprovalResourceFactory< + I extends TApprovalPolicyInputs, + P extends TApprovalPolicy, + R extends TApprovalRequestData +> = (policyType: ApprovalPolicyType) => { + matchPolicy: TApprovalRequestFactoryMatchPolicy; + canAccess: TApprovalRequestFactoryCanAccess; + validateConstraints: TApprovalRequestFactoryValidateConstraints; + postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine; +}; diff --git a/backend/src/services/approval-policy/approval-request-dal.ts b/backend/src/services/approval-policy/approval-request-dal.ts new file mode 100644 index 000000000..ced8bea41 --- /dev/null +++ b/backend/src/services/approval-policy/approval-request-dal.ts @@ -0,0 +1,199 @@ +import { TDbClient } from "@app/db"; +import { TableName, TApprovalRequestApprovals } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +import { + ApprovalPolicyType, + ApprovalRequestGrantStatus, + ApprovalRequestStatus, + ApproverType +} from "./approval-policy-enums"; +import { ApprovalPolicyStep } from "./approval-policy-types"; + +// Approval Request +export type TApprovalRequestDALFactory = ReturnType; +export const approvalRequestDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequests); + + const findStepsByRequestId = async (requestId: string) => { + try { + const dbInstance = db.replicaNode(); + const steps = await dbInstance(TableName.ApprovalRequestSteps).where({ requestId }).orderBy("stepNumber", "asc"); + + if (!steps.length) { + return []; + } + + const stepIds = steps.map((step) => step.id); + + const [approvers, approvals] = await Promise.all([ + dbInstance(TableName.ApprovalRequestStepEligibleApprovers) + .whereIn("stepId", stepIds) + .select("stepId", "userId", "groupId"), + dbInstance(TableName.ApprovalRequestApprovals).whereIn("stepId", stepIds) + ]); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.stepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.stepId] = stepApprovers; + return acc; + }, + {} + ); + + const approvalsByStepId = approvals.reduce>((acc, approval) => { + const stepApprovals = acc[approval.stepId] || []; + stepApprovals.push(approval); + acc[approval.stepId] = stepApprovals; + return acc; + }, {}); + + return steps.map((step) => { + return { + ...step, + approvers: approversByStepId[step.id] || [], + approvals: approvalsByStepId[step.id] || [] + }; + }); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval request steps" }); + } + }; + + const findByProjectId = async (policyType: ApprovalPolicyType, projectId: string) => { + try { + const dbInstance = db.replicaNode(); + const requests = await dbInstance(TableName.ApprovalRequests).where({ type: policyType, projectId }); + + if (!requests.length) { + return []; + } + + const requestIds = requests.map((req) => req.id); + + const steps = await dbInstance(TableName.ApprovalRequestSteps) + .whereIn("requestId", requestIds) + .orderBy("stepNumber", "asc"); + + const stepsByRequestId: Record = {}; + + if (steps.length) { + const stepIds = steps.map((step) => step.id); + + const [approvers, approvals] = await Promise.all([ + dbInstance(TableName.ApprovalRequestStepEligibleApprovers) + .whereIn("stepId", stepIds) + .select("stepId", "userId", "groupId"), + dbInstance(TableName.ApprovalRequestApprovals).whereIn("stepId", stepIds) + ]); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.stepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.stepId] = stepApprovers; + return acc; + }, + {} + ); + + const approvalsByStepId = approvals.reduce>((acc, approval) => { + const stepApprovals = acc[approval.stepId] || []; + stepApprovals.push(approval); + acc[approval.stepId] = stepApprovals; + return acc; + }, {}); + + steps.forEach((step) => { + const formattedStep = { + ...step, + approvers: approversByStepId[step.id] || [], + approvals: approvalsByStepId[step.id] || [] + }; + + if (!stepsByRequestId[step.requestId]) { + stepsByRequestId[step.requestId] = []; + } + stepsByRequestId[step.requestId].push(formattedStep); + }); + } + + return requests.map((req) => ({ + ...req, + steps: stepsByRequestId[req.id] || [] + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval requests by project id" }); + } + }; + + const markExpiredRequests = async () => { + try { + const result = await db(TableName.ApprovalRequests) + .where("status", ApprovalRequestStatus.Pending) + .whereNotNull("expiresAt") + .where("expiresAt", "<", new Date()) + .update({ status: ApprovalRequestStatus.Expired }); + + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Mark expired approval requests" }); + } + }; + + return { ...orm, findStepsByRequestId, findByProjectId, markExpiredRequests }; +}; + +// Approval Request Steps +export type TApprovalRequestStepsDALFactory = ReturnType; +export const approvalRequestStepsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestSteps); + return orm; +}; + +// Approval Request Step Eligible Approvers +export type TApprovalRequestStepEligibleApproversDALFactory = ReturnType< + typeof approvalRequestStepEligibleApproversDALFactory +>; +export const approvalRequestStepEligibleApproversDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestStepEligibleApprovers); + return orm; +}; + +// Approval Request Grants +export type TApprovalRequestGrantsDALFactory = ReturnType; +export const approvalRequestGrantsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestGrants); + + const markExpiredGrants = async () => { + try { + const result = await db(TableName.ApprovalRequestGrants) + .where("status", ApprovalRequestGrantStatus.Active) + .whereNotNull("expiresAt") + .where("expiresAt", "<", new Date()) + .update({ status: ApprovalRequestGrantStatus.Expired }); + + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Mark expired approval grants" }); + } + }; + + return { ...orm, markExpiredGrants }; +}; + +// Approval Request Approvals +export type TApprovalRequestApprovalsDALFactory = ReturnType; +export const approvalRequestApprovalsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestApprovals); + return orm; +}; diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-factory.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-factory.ts new file mode 100644 index 000000000..21b97e0d1 --- /dev/null +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-factory.ts @@ -0,0 +1,127 @@ +import picomatch from "picomatch"; + +import { ms } from "@app/lib/ms"; + +import { ApprovalRequestGrantStatus } from "../approval-policy-enums"; +import { + TApprovalRequestFactoryCanAccess, + TApprovalRequestFactoryMatchPolicy, + TApprovalRequestFactoryPostApprovalRoutine, + TApprovalRequestFactoryValidateConstraints, + TApprovalResourceFactory +} from "../approval-policy-types"; +import { TPamAccessPolicy, TPamAccessPolicyInputs, TPamAccessRequestData } from "./pam-access-policy-types"; + +export const pamAccessPolicyFactory: TApprovalResourceFactory< + TPamAccessPolicyInputs, + TPamAccessPolicy, + TPamAccessRequestData +> = (policyType) => { + const matchPolicy: TApprovalRequestFactoryMatchPolicy = async ( + approvalPolicyDAL, + projectId, + inputs + ) => { + const policies = await approvalPolicyDAL.findByProjectId(policyType, projectId); + + let bestMatch: { policy: TPamAccessPolicy; wildcardCount: number; pathLength: number } | null = null; + + for (const policy of policies) { + const p = policy as TPamAccessPolicy; + for (const c of p.conditions.conditions) { + // Find the most specific path pattern + // TODO(andrey): Make matching logic more advanced by accounting for wildcard positions + for (const pathPattern of c.accountPaths) { + if (picomatch(pathPattern)(inputs.accountPath)) { + const wildcardCount = (pathPattern.match(/\*/g) || []).length; + const pathLength = pathPattern.length; + + if ( + !bestMatch || + wildcardCount < bestMatch.wildcardCount || + (wildcardCount === bestMatch.wildcardCount && pathLength > bestMatch.pathLength) + ) { + bestMatch = { policy: p, wildcardCount, pathLength }; + } + } + } + } + } + + return bestMatch?.policy || null; + }; + + const canAccess: TApprovalRequestFactoryCanAccess = async ( + approvalRequestGrantsDAL, + projectId, + userId, + inputs + ) => { + const grants = await approvalRequestGrantsDAL.find({ + granteeUserId: userId, + type: policyType, + status: ApprovalRequestGrantStatus.Active, + projectId, + revokedAt: null + }); + + // TODO(andrey): Move some of this check to be part of SQL query + return grants.some((grant) => { + const grantAttributes = grant.attributes as TPamAccessPolicyInputs; + const isMatch = picomatch(grantAttributes.accountPath); + return isMatch(inputs.accountPath) && (!grant.expiresAt || grant.expiresAt > new Date()); + }); + }; + + const validateConstraints: TApprovalRequestFactoryValidateConstraints = ( + policy, + inputs + ) => { + const reqDuration = ms(inputs.accessDuration); + const durationConstraint = policy.constraints.constraints.accessDuration; + const minDuration = ms(durationConstraint.min); + const maxDuration = ms(durationConstraint.max); + + const errors: string[] = []; + + if (reqDuration < minDuration) { + errors.push( + `Access duration ${inputs.accessDuration} is below the minimum allowed duration of ${durationConstraint.min}` + ); + } + + if (reqDuration > maxDuration) { + errors.push( + `Access duration ${inputs.accessDuration} exceeds the maximum allowed duration of ${durationConstraint.max}` + ); + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined + }; + }; + + const postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine = async (approvalRequestGrantsDAL, request) => { + const inputs = request.requestData.requestData; + const durationMs = ms(inputs.accessDuration); + const expiresAt = new Date(Date.now() + durationMs); + + await approvalRequestGrantsDAL.create({ + projectId: request.projectId, + requestId: request.id, + granteeUserId: request.requesterId, + status: ApprovalRequestGrantStatus.Active, + type: request.type, + attributes: inputs, + expiresAt + }); + }; + + return { + matchPolicy, + canAccess, + validateConstraints, + postApprovalRoutine + }; +}; diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts new file mode 100644 index 000000000..caa2cfa67 --- /dev/null +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts @@ -0,0 +1,101 @@ +import picomatch from "picomatch"; +import { z } from "zod"; + +import { ms } from "@app/lib/ms"; + +import { + BaseApprovalPolicySchema, + BaseApprovalRequestGrantSchema, + BaseApprovalRequestSchema, + BaseCreateApprovalPolicySchema, + BaseCreateApprovalRequestSchema, + BaseUpdateApprovalPolicySchema +} from "../approval-policy-schemas"; + +// Inputs +export const PamAccessPolicyInputsSchema = z.object({ + accountPath: z.string() +}); + +// Conditions +export const PamAccessPolicyConditionsSchema = z + .object({ + accountPaths: z + .string() + .refine( + (el) => { + try { + picomatch.parse([el]); + return true; + } catch { + return false; + } + }, + { message: "Invalid glob pattern" } + ) + .array() + }) + .array(); + +const DurationSchema = z.string().refine( + (val) => { + const duration = ms(val) / 1000; + + // 30 seconds to 7 days + return duration >= 30 && duration <= 604800; + }, + { message: "Duration must be between 30 seconds and 7 days" } +); + +// Constraints +export const PamAccessPolicyConstraintsSchema = z.object({ + accessDuration: z.object({ + min: DurationSchema, + max: DurationSchema + }) +}); + +// Request Data +export const PamAccessPolicyRequestDataSchema = z.object({ + accountPath: z.string(), + accessDuration: DurationSchema +}); + +// Policy +export const PamAccessPolicySchema = BaseApprovalPolicySchema.extend({ + conditions: z.object({ + version: z.literal(1), + conditions: PamAccessPolicyConditionsSchema + }), + constraints: z.object({ + version: z.literal(1), + constraints: PamAccessPolicyConstraintsSchema + }) +}); + +export const CreatePamAccessPolicySchema = BaseCreateApprovalPolicySchema.extend({ + conditions: PamAccessPolicyConditionsSchema, + constraints: PamAccessPolicyConstraintsSchema +}); + +export const UpdatePamAccessPolicySchema = BaseUpdateApprovalPolicySchema.extend({ + conditions: PamAccessPolicyConditionsSchema.optional(), + constraints: PamAccessPolicyConstraintsSchema.optional() +}); + +// Request +export const PamAccessRequestSchema = BaseApprovalRequestSchema.extend({ + requestData: z.object({ + version: z.literal(1), + requestData: PamAccessPolicyRequestDataSchema + }) +}); + +export const CreatePamAccessRequestSchema = BaseCreateApprovalRequestSchema.extend({ + requestData: PamAccessPolicyRequestDataSchema +}); + +// Grants +export const PamAccessRequestGrantSchema = BaseApprovalRequestGrantSchema.extend({ + attributes: PamAccessPolicyRequestDataSchema +}); diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-types.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-types.ts new file mode 100644 index 000000000..78118fcb0 --- /dev/null +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-types.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +import { + PamAccessPolicyConditionsSchema, + PamAccessPolicyConstraintsSchema, + PamAccessPolicyInputsSchema, + PamAccessPolicyRequestDataSchema, + PamAccessPolicySchema, + PamAccessRequestSchema +} from "./pam-access-policy-schemas"; + +// Policy +export type TPamAccessPolicy = z.infer; +export type TPamAccessPolicyInputs = z.infer; +export type TPamAccessPolicyConditions = z.infer; +export type TPamAccessPolicyConstraints = z.infer; + +// Request +export type TPamAccessRequest = z.infer; +export type TPamAccessRequestData = z.infer; diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index 9d8d1aebb..3cb1746a4 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -19,8 +19,8 @@ import { CertExtendedKeyUsageType, CertIncludeType, CertKeyUsageType, - CertSubjectAttributeType, - CertSubjectAlternativeNameType + CertSubjectAlternativeNameType, + CertSubjectAttributeType } from "@app/services/certificate-common/certificate-constants"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types"; diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index c20412537..fd46f2c5f 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -19,8 +19,10 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { + CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyType, + CertKeyUsage, CertSignatureAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; @@ -46,7 +48,9 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns import { CertExtendedKeyUsageType, CertKeyUsageType, - CertSubjectAlternativeNameType + CertSubjectAlternativeNameType, + mapLegacyExtendedKeyUsageToStandard, + mapLegacyKeyUsageToStandard } from "../certificate-common/certificate-constants"; import { extractAlgorithmsFromCSR, @@ -309,47 +313,54 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s const parseKeyUsages = (keyUsages: unknown): CertKeyUsageType[] => { if (!keyUsages) return []; - const validKeyUsages = Object.values(CertKeyUsageType); + const validKeyUsages = [...Object.values(CertKeyUsageType), ...Object.values(CertKeyUsage)] as string[]; + + const normalize = (usage: string): CertKeyUsageType | null => { + if (validKeyUsages.includes(usage)) { + return mapLegacyKeyUsageToStandard(usage as CertKeyUsageType); + } + return null; + }; + + let raw: string[]; if (Array.isArray(keyUsages)) { - return keyUsages.filter( - (usage): usage is CertKeyUsageType => - typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsageType) - ); + raw = keyUsages.filter((u): u is string => typeof u === "string"); + } else if (typeof keyUsages === "string") { + raw = keyUsages.split(",").map((u) => u.trim()); + } else { + return []; } - if (typeof keyUsages === "string") { - return keyUsages - .split(",") - .map((usage) => usage.trim()) - .filter((usage): usage is CertKeyUsageType => validKeyUsages.includes(usage as CertKeyUsageType)); - } - - return []; + return raw.map((u) => normalize(u)).filter((u): u is CertKeyUsageType => u !== null); }; const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsageType[] => { if (!extendedKeyUsages) return []; - const validExtendedKeyUsages = Object.values(CertExtendedKeyUsageType); + const validExtendedKeyUsages = [ + ...Object.values(CertExtendedKeyUsageType), + ...Object.values(CertExtendedKeyUsage) + ] as string[]; + + const normalize = (usage: string): CertExtendedKeyUsageType | null => { + if (validExtendedKeyUsages.includes(usage)) { + return mapLegacyExtendedKeyUsageToStandard(usage as CertExtendedKeyUsageType); + } + return null; + }; + + let raw: string[]; if (Array.isArray(extendedKeyUsages)) { - return extendedKeyUsages.filter( - (usage): usage is CertExtendedKeyUsageType => - typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) - ); + raw = extendedKeyUsages.filter((u): u is string => typeof u === "string"); + } else if (typeof extendedKeyUsages === "string") { + raw = extendedKeyUsages.split(",").map((u) => u.trim()); + } else { + return []; } - if (typeof extendedKeyUsages === "string") { - return extendedKeyUsages - .split(",") - .map((usage) => usage.trim()) - .filter((usage): usage is CertExtendedKeyUsageType => - validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) - ); - } - - return []; + return raw.map((u) => normalize(u)).filter((u): u is CertExtendedKeyUsageType => u !== null); }; const convertEnumsToStringArray = (enumArray: T[]): string[] => { diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 8f868978d..a63f0d41d 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -253,7 +253,7 @@ export const kmsServiceFactory = ({ } if (!org.kmsDefaultKeyId) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } return org.kmsDefaultKeyId; @@ -292,7 +292,7 @@ export const kmsServiceFactory = ({ let externalKms: TExternalKmsProviderFns; if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } // The idea is external kms connection info is encrypted by an org default KMS @@ -338,7 +338,7 @@ export const kmsServiceFactory = ({ break; } default: - throw new Error("Invalid KMS provider."); + throw new BadRequestError({ message: "Invalid KMS provider." }); } return async ({ cipherTextBlob }: Pick) => { @@ -509,7 +509,7 @@ export const kmsServiceFactory = ({ if (kmsDoc.externalKms) { let externalKms: TExternalKmsProviderFns; if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } const orgKmsDecryptor = await decryptWithKmsKey({ @@ -550,7 +550,7 @@ export const kmsServiceFactory = ({ break; } default: - throw new Error("Invalid KMS provider."); + throw new BadRequestError({ message: "Invalid KMS provider." }); } return async ({ plainText }: Pick) => { @@ -651,7 +651,7 @@ export const kmsServiceFactory = ({ } if (!org.kmsEncryptedDataKey) { - throw new Error("Invalid organization KMS"); + throw new BadRequestError({ message: "Invalid organization KMS" }); } const kmsDecryptor = await decryptWithKmsKey({ @@ -723,7 +723,7 @@ export const kmsServiceFactory = ({ } if (!project.kmsSecretManagerKeyId) { - throw new Error("Missing project KMS key ID"); + throw new BadRequestError({ message: "Missing project KMS key ID" }); } return project.kmsSecretManagerKeyId; @@ -832,9 +832,10 @@ export const kmsServiceFactory = ({ const isBase64 = !envConfig.ENCRYPTION_KEY; if (!encryptionKey) - throw new Error( - "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" - ); + throw new BadRequestError({ + message: + "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" + }); const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); @@ -846,7 +847,9 @@ export const kmsServiceFactory = ({ if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) { const hsmIsActive = await hsmService.isActive(); if (!hsmIsActive) { - throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + throw new BadRequestError({ + message: "Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?" + }); } const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey); @@ -861,14 +864,16 @@ export const kmsServiceFactory = ({ return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); } - throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`); + throw new BadRequestError({ message: `Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}` }); }; const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { if (strategy === RootKeyEncryptionStrategy.HSM) { const hsmIsActive = await hsmService.isActive(); if (!hsmIsActive) { - throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + throw new BadRequestError({ + message: "Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?" + }); } const encrypted = await hsmService.encrypt(plainKeyBuffer); return encrypted; @@ -882,7 +887,7 @@ export const kmsServiceFactory = ({ } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Invalid root key encryption strategy: ${strategy}`); + throw new BadRequestError({ message: `Invalid root key encryption strategy: ${strategy}` }); }; // by keeping the decrypted data key in inner scope @@ -1130,7 +1135,7 @@ export const kmsServiceFactory = ({ if (!encryptedRootKey) { logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy"); - throw new Error("Failed to re-encrypt ROOT Key with selected strategy"); + throw new BadRequestError({ message: "Failed to re-encrypt ROOT Key with selected strategy" }); } await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 84cf35a50..56045174a 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -17,7 +17,8 @@ export enum NotificationType { PROJECT_INVITATION = "project-invitation", SECRET_SYNC_FAILED = "secret-sync-failed", GATEWAY_HEALTH_ALERT = "gateway-health-alert", - RELAY_HEALTH_ALERT = "relay-health-alert" + RELAY_HEALTH_ALERT = "relay-health-alert", + APPROVAL_REQUIRED = "approval-required" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/pam-session-expiration/pam-session-expiration-queue.ts b/backend/src/services/pam-session-expiration/pam-session-expiration-queue.ts new file mode 100644 index 000000000..f14da58f0 --- /dev/null +++ b/backend/src/services/pam-session-expiration/pam-session-expiration-queue.ts @@ -0,0 +1,81 @@ +import { TPamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +type TPamSessionExpirationServiceFactoryDep = { + queueService: TQueueServiceFactory; + pamSessionDAL: Pick; +}; + +export type TPamSessionExpirationServiceFactory = ReturnType; + +export const pamSessionExpirationServiceFactory = ({ + queueService, + pamSessionDAL +}: TPamSessionExpirationServiceFactoryDep) => { + const appCfg = getConfig(); + + const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + + await queueService.startPg( + QueueJobs.PamSessionExpiration, + async (jobs) => { + await Promise.all( + jobs.map(async (job) => { + const { sessionId } = job.data; + try { + logger.info({ sessionId }, `${QueueName.PamSessionExpiration}: expiring session`); + const updated = await pamSessionDAL.expireSessionById(sessionId); + if (updated > 0) { + logger.info({ sessionId }, `${QueueName.PamSessionExpiration}: session expired successfully`); + } else { + logger.info( + { sessionId }, + `${QueueName.PamSessionExpiration}: session not expired (already ended or not found)` + ); + } + } catch (error) { + logger.error(error, `${QueueName.PamSessionExpiration}: failed to expire session ${sessionId}`); + throw error; + } + }) + ); + }, + { + batchSize: 1, + workerCount: 1, + pollingIntervalSeconds: 30 + } + ); + }; + + // Schedule a session expiration job to run at the session's expiresAt time + const scheduleSessionExpiration = async (sessionId: string, expiresAt: Date) => { + const now = new Date(); + const delayMs = Math.max(0, expiresAt.getTime() - now.getTime()); + const startAfter = new Date(now.getTime() + delayMs); + + await queueService.queuePg( + QueueJobs.PamSessionExpiration, + { sessionId }, + { + startAfter, + singletonKey: `pam-session-expiration-${sessionId}` + } + ); + + logger.info( + { sessionId, expiresAt: expiresAt.toISOString(), scheduledFor: startAfter.toISOString() }, + `${QueueName.PamSessionExpiration}: scheduled session expiration` + ); + }; + + return { + init, + scheduleSessionExpiration + }; +}; diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 60310765b..68d261193 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -7,6 +7,7 @@ import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TUserNotificationDALFactory } from "@app/services/notification/user-notification-dal"; +import { TApprovalRequestDALFactory, TApprovalRequestGrantsDALFactory } from "../approval-policy/approval-request-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; import { TOrgServiceFactory } from "../org/org-service"; @@ -31,6 +32,8 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { userNotificationDAL: Pick; keyValueStoreDAL: Pick; scimService: Pick; + approvalRequestDAL: Pick; + approvalRequestGrantsDAL: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -49,7 +52,9 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ scimService, orgService, userNotificationDAL, - keyValueStoreDAL + keyValueStoreDAL, + approvalRequestDAL, + approvalRequestGrantsDAL }: TDailyResourceCleanUpQueueServiceFactoryDep) => { const appCfg = getConfig(); @@ -94,6 +99,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await auditLogDAL.pruneAuditLog(); await userNotificationDAL.pruneNotifications(); await keyValueStoreDAL.pruneExpiredKeys(); + await approvalRequestDAL.markExpiredRequests(); + await approvalRequestGrantsDAL.markExpiredGrants(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); } catch (error) { logger.error(error, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`); diff --git a/docs/docs.json b/docs/docs.json index 5a3d965fd..39ebf31a9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -130,6 +130,7 @@ "integrations/app-connections/laravel-forge", "integrations/app-connections/ldap", "integrations/app-connections/mssql", + "integrations/app-connections/mongodb", "integrations/app-connections/mysql", "integrations/app-connections/netlify", "integrations/app-connections/northflank", @@ -444,6 +445,7 @@ "documentation/platform/secret-rotation/aws-iam-user-secret", "documentation/platform/secret-rotation/azure-client-secret", "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mongodb-credentials", "documentation/platform/secret-rotation/mssql-credentials", "documentation/platform/secret-rotation/mysql-credentials", "documentation/platform/secret-rotation/okta-client-secret", @@ -1393,6 +1395,18 @@ "api-reference/endpoints/app-connections/mssql/delete" ] }, + { + "group": "MongoDB", + "pages": [ + "api-reference/endpoints/app-connections/mongodb/list", + "api-reference/endpoints/app-connections/mongodb/available", + "api-reference/endpoints/app-connections/mongodb/get-by-id", + "api-reference/endpoints/app-connections/mongodb/get-by-name", + "api-reference/endpoints/app-connections/mongodb/create", + "api-reference/endpoints/app-connections/mongodb/update", + "api-reference/endpoints/app-connections/mongodb/delete" + ] + }, { "group": "MySQL", "pages": [ @@ -1929,6 +1943,19 @@ "api-reference/endpoints/secret-rotations/mssql-credentials/update" ] }, + { + "group": "MongoDB Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mongodb-credentials/create", + "api-reference/endpoints/secret-rotations/mongodb-credentials/delete", + "api-reference/endpoints/secret-rotations/mongodb-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mongodb-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mongodb-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mongodb-credentials/list", + "api-reference/endpoints/secret-rotations/mongodb-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mongodb-credentials/update" + ] + }, { "group": "MySQL Credentials", "pages": [ diff --git a/docs/documentation/platform/secret-rotation/mongodb-credentials.mdx b/docs/documentation/platform/secret-rotation/mongodb-credentials.mdx new file mode 100644 index 000000000..5b3808126 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mongodb-credentials.mdx @@ -0,0 +1,177 @@ +--- +title: "MongoDB Credentials Rotation" +description: "Learn how to automatically rotate MongoDB credentials." +--- + +## Prerequisites + +1. Create a [MongoDB Connection](/integrations/app-connections/mongodb) with the required **Secret Rotation** permissions +2. Create two designated database users for Infisical to rotate the credentials for. Be sure to grant each user login permissions for the desired database with the necessary privileges their use case will require. + + An example creation statement might look like: + ```bash + // Switch to the target database + use my_database + + // Create first user + db.createUser({ + user: "infisical_user_1", + pwd: "temporary_password", + roles: [] + }) + + // Create second user + db.createUser({ + user: "infisical_user_2", + pwd: "temporary_password", + roles: [] + }) + + // Grant necessary permissions to both users + db.grantRolesToUser("infisical_user_1", [ + { role: "readWrite", db: "my_database" } + ]) + + db.grantRolesToUser("infisical_user_2", [ + { role: "readWrite", db: "my_database" } + ]) + ``` + + + To learn more about MongoDB's permission system, please visit their [documentation](https://www.mongodb.com/docs/manual/core/security-built-in-roles/). + + +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. + +## Create a MongoDB Credentials Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **MongoDB Credentials** option. + ![Select MongoDB Credentials](/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png) + + 3. Select the **MongoDB Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png) + + - **MongoDB Connection** - the connection that will perform the rotation of the configured database user credentials. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Input the usernames of the database users created above that will be used for rotation. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png) + + - **Database Username 1** - the username of the first user that will be used for rotation. + - **Database Username 2** - the username of the second user that will be used for rotation. + + 5. Specify the secret names that the active credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png) + + - **Username** - the name of the secret that the active username will be mapped to. + - **Password** - the name of the secret that the active password will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png) + + 8. Your **MongoDB Credentials** are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png) + + + To create a MongoDB Credentials Rotation, make an API request to the [Create MongoDB + Credentials Rotation](/api-reference/endpoints/secret-rotations/mongodb-credentials/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/mongodb-credentials \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-mongodb-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my database credentials rotation", + "connectionId": "11c76f38-cd13-4137-b1a3-ecd6a429952c", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + }, + "secretsMapping": { + "username": "MONGODB_DB_USERNAME", + "password": "MONGODB_DB_PASSWORD" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-mongodb-rotation", + "description": "my database credentials rotation", + "secretsMapping": { + "username": "MONGODB_DB_USERNAME", + "password": "MONGODB_DB_PASSWORD" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "b3257e1f-8d32-4e86-8bfd-b1f1bc1bf2c3", + "connectionId": "11c76f38-cd13-4137-b1a3-ecd6a429952c", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": null, + "nextRotationAt": "2023-11-07T05:31:56Z", + "isLastRotationManual": true, + "connection": { + "app": "mongodb", + "name": "my-mongodb-connection", + "id": "11c76f38-cd13-4137-b1a3-ecd6a429952c" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "170a40f1-1b48-4cc7-addf-e563aa9fbe37" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "b3257e1f-8d32-4e86-8bfd-b1f1bc1bf2c3", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "mongodb-credentials", + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + } + } + } + ``` + + + diff --git a/docs/images/app-connections/general/add-connection.png b/docs/images/app-connections/general/add-connection.png index b6dce69ac..5ebadae09 100644 Binary files a/docs/images/app-connections/general/add-connection.png and b/docs/images/app-connections/general/add-connection.png differ diff --git a/docs/images/app-connections/mongodb/mongodb-app-connection-form.png b/docs/images/app-connections/mongodb/mongodb-app-connection-form.png new file mode 100644 index 000000000..f57ca1aa5 Binary files /dev/null and b/docs/images/app-connections/mongodb/mongodb-app-connection-form.png differ diff --git a/docs/images/app-connections/mongodb/mongodb-app-connection-generated.png b/docs/images/app-connections/mongodb/mongodb-app-connection-generated.png new file mode 100644 index 000000000..eed7d01af Binary files /dev/null and b/docs/images/app-connections/mongodb/mongodb-app-connection-generated.png differ diff --git a/docs/images/app-connections/mongodb/mongodb-app-connection-option.png b/docs/images/app-connections/mongodb/mongodb-app-connection-option.png new file mode 100644 index 000000000..0999decb3 Binary files /dev/null and b/docs/images/app-connections/mongodb/mongodb-app-connection-option.png differ diff --git a/docs/images/secret-rotations-v2/generic/add-secret-rotation.png b/docs/images/secret-rotations-v2/generic/add-secret-rotation.png index 86b84001b..4b1b626ae 100644 Binary files a/docs/images/secret-rotations-v2/generic/add-secret-rotation.png and b/docs/images/secret-rotations-v2/generic/add-secret-rotation.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png new file mode 100644 index 000000000..8d0db7d03 Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png new file mode 100644 index 000000000..d568e2d5d Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png new file mode 100644 index 000000000..e18ef7b0f Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png new file mode 100644 index 000000000..949ff40be Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png new file mode 100644 index 000000000..14723c0a6 Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png new file mode 100644 index 000000000..aceb38a8d Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png b/docs/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png new file mode 100644 index 000000000..4de950114 Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png differ diff --git a/docs/integrations/app-connections/mongodb.mdx b/docs/integrations/app-connections/mongodb.mdx new file mode 100644 index 000000000..e677eaa4e --- /dev/null +++ b/docs/integrations/app-connections/mongodb.mdx @@ -0,0 +1,141 @@ +--- +title: "MongoDB Connection" +description: "Learn how to configure a MongoDB Connection for Infisical." +--- + +Infisical supports the use of Username & Password authentication to connect with MongoDB databases. + +## Configure a MongoDB user for Infisical + + + + Infisical recommends creating a designated user in your MongoDB database for your connection. + + ```bash + use [TARGET-DATABASE] + db.createUser({ + user: "infisical_manager", + pwd: "[ENTER-YOUR-USER-PASSWORD]", + roles: [] + }) + ``` + + + + Depending on how you intend to use your MongoDB connection, you'll need to grant one or more of the following permissions. + + + To learn more about MongoDB's permission system, please visit their [documentation](https://www.mongodb.com/docs/manual/core/security-built-in-roles/). + + + + + For Secret Rotations, your Infisical user will require the ability to create, update, and delete users in the target database: + + ```bash + use [TARGET-DATABASE] + db.grantRolesToUser("infisical_manager", [ + { role: "userAdmin", db: "[TARGET-DATABASE]" } + ]) + ``` + + + The `userAdmin` role allows managing users (create, update passwords, delete) within the specified database. + + + + + + + +## Create MongoDB Connection in Infisical + + + + + + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **MongoDB Connection** option from the available integrations. + + ![Select MongoDB Connection](/images/app-connections/mongodb/mongodb-app-connection-option.png) + + + Complete the MongoDB Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - The MongoDB host URL for your database + - The MongoDB port for your database + - The MongoDB username for your database + - The MongoDB password for your database + - The MongoDB database name to connect to + + You can optionally configure SSL/TLS for your MongoDB connection in the **SSL** section. + + ![MongoDB Connection Modal](/images/app-connections/mongodb/mongodb-app-connection-form.png) + + + After clicking Create, your **MongoDB Connection** is established and ready to use with your Infisical project. + + ![MongoDB Connection Created](/images/app-connections/mongodb/mongodb-app-connection-generated.png) + + + + + To create a MongoDB Connection, make an API request to the [Create MongoDB Connection](/api-reference/endpoints/app-connections/mongodb/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/mongodb \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-mongodb-connection", + "method": "username-and-password", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "credentials": { + "host": "[MONGODB HOST]", + "port": 27017, + "username": "[MONGODB USERNAME]", + "password": "[MONGODB PASSWORD]", + "database": "[MONGODB DATABASE]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-mongodb-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e", + "app": "mongodb", + "method": "username-and-password", + "credentials": { + "host": "[MONGODB HOST]", + "port": 27017, + "username": "[MONGODB USERNAME]", + "database": "[MONGODB DATABASE]", + "sslEnabled": false, + "sslRejectUnauthorized": false, + "sslCertificate": "" + } + } + } + ``` + + + diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx index d7001d6eb..c79ef366b 100644 --- a/docs/snippets/AppConnectionsBrowser.jsx +++ b/docs/snippets/AppConnectionsBrowser.jsx @@ -362,6 +362,13 @@ export const AppConnectionsBrowser = () => { "Learn how to connect your Northflank projects to pull secrets from Infisical.", category: "Hosting", }, + { + name: "MongoDB", + slug: "mongodb", + path: "/integrations/app-connections/mongodb", + description: "Learn how to connect your MongoDB to pull secrets from Infisical.", + category: "Databases" + } ].sort(function (a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); diff --git a/docs/snippets/RotationsBrowser.jsx b/docs/snippets/RotationsBrowser.jsx index 3dbede698..0666237b3 100644 --- a/docs/snippets/RotationsBrowser.jsx +++ b/docs/snippets/RotationsBrowser.jsx @@ -16,7 +16,8 @@ export const RotationsBrowser = () => { {"name": "PostgreSQL", "slug": "postgres-credentials", "path": "/documentation/platform/secret-rotation/postgres-credentials", "description": "Learn how to automatically rotate PostgreSQL database credentials.", "category": "Databases"}, {"name": "Redis", "slug": "redis-credentials", "path": "/documentation/platform/secret-rotation/redis-credentials", "description": "Learn how to automatically rotate Redis database credentials.", "category": "Databases"}, {"name": "Microsoft SQL Server", "slug": "mssql-credentials", "path": "/documentation/platform/secret-rotation/mssql-credentials", "description": "Learn how to automatically rotate Microsoft SQL Server credentials.", "category": "Databases"}, - {"name": "Oracle Database", "slug": "oracledb-credentials", "path": "/documentation/platform/secret-rotation/oracledb-credentials", "description": "Learn how to automatically rotate Oracle Database credentials.", "category": "Databases"} + {"name": "Oracle Database", "slug": "oracledb-credentials", "path": "/documentation/platform/secret-rotation/oracledb-credentials", "description": "Learn how to automatically rotate Oracle Database credentials.", "category": "Databases"}, + {"name": "MongoDB Credentials", "slug": "mongodb-credentials", "path": "/documentation/platform/secret-rotation/mongodb-credentials", "description": "Learn how to automatically rotate MongoDB credentials.", "category": "Databases"} ].sort(function(a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index e8553f6d9..01904b852 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -67,6 +67,7 @@ const Content = ({ secretRotation }: ContentProps) => { case SecretRotation.MySqlCredentials: case SecretRotation.MsSqlCredentials: case SecretRotation.OracleDBCredentials: + case SecretRotation.MongoDBCredentials: Component = ( = { [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields, [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields, - [SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields + [SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields, + [SecretRotation.MongoDBCredentials]: SqlCredentialsRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index e484a64b1..d48c69cca 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -24,7 +24,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields, [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields, - [SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields + [SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields, + [SecretRotation.MongoDBCredentials]: SqlCredentialsRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index e05fd31f5..a211abff8 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -21,7 +21,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields, [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields, - [SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields + [SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields, + [SecretRotation.MongoDBCredentials]: SqlCredentialsRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 199036a8f..3ea0f75ed 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -4,6 +4,7 @@ import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotation import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema"; import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema"; import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema"; +import { MongoDBCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema"; import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema"; import { MySqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mysql-credentials-rotation-schema"; import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema"; @@ -27,7 +28,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) => LdapPasswordRotationSchema, AwsIamUserSecretRotationSchema, OktaClientSecretRotationSchema, - RedisCredentialsRotationSchema + RedisCredentialsRotationSchema, + MongoDBCredentialsRotationSchema ]), z.object({ id: z.string().optional() }) ) diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema.ts new file mode 100644 index 000000000..24c22cfed --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const MongoDBCredentialsRotationSchema = z + .object({ + type: z.literal(SecretRotation.MongoDBCredentials) + }) + .merge(SqlCredentialsRotationSchema) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx index 30cd4ce8e..9227d57b3 100644 --- a/frontend/src/components/v2/Input/Input.tsx +++ b/frontend/src/components/v2/Input/Input.tsx @@ -65,6 +65,12 @@ const inputParentContainerVariants = cva("inline-flex font-inter items-center bo } }); +const data1pIgnore = (autoComplete?: string) => { + if (!autoComplete) return true; + + return !autoComplete.match(/(email|password|username)/i); +}; + export type InputProps = Omit, "size"> & VariantProps & Props; @@ -86,6 +92,7 @@ export const Input = forwardRef( isReadOnly, autoCapitalization, warning, + autoComplete, ...props }, ref @@ -116,6 +123,8 @@ export const Input = forwardRef( readOnly={isReadOnly} disabled={isDisabled} onInput={handleInput} + autoComplete={autoComplete} + data-1p-ignore={data1pIgnore(autoComplete)} className={twMerge( leftIcon ? "pl-10" : "pl-2.5", rightIcon || warning ? "pr-10" : "pr-2.5", diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 6046c8fc8..ee922f9e6 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -359,6 +359,10 @@ export const ROUTE_PATHS = Object.freeze({ "/organizations/$orgId/projects/pam/$projectId/sessions", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/" ), + ApprovalRequestDetailPage: setRoute( + "/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/approval-requests/$approvalRequestId" + ), PamSessionByIDPage: setRoute( "/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId" diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 83eb2fecd..0838b0c36 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -228,6 +228,16 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export enum ProjectPermissionApprovalRequestActions { + Read = "read", + Create = "create" +} + +export enum ProjectPermissionApprovalRequestGrantActions { + Read = "read", + Revoke = "revoke" +} + export type IdentityManagementSubjectFields = { identityId: string; }; @@ -339,7 +349,9 @@ export enum ProjectPermissionSub { PamFolders = "pam-folders", PamResources = "pam-resources", PamAccounts = "pam-accounts", - PamSessions = "pam-sessions" + PamSessions = "pam-sessions", + ApprovalRequests = "approval-requests", + ApprovalRequestGrants = "approval-request-grants" } export type SecretSubjectFields = { @@ -577,6 +589,8 @@ export type ProjectPermissionSet = | (ForcedSubject & PamAccountSubjectFields) ) ] - | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; + | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions] + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests] + | [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 7f02bd64e..e8857e022 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -31,6 +31,7 @@ import { HCVaultConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, + MongoDBConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, OktaConnectionMethod, @@ -129,6 +130,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Northflank]: { name: "Northflank", image: "Northflank.png" }, [AppConnection.Okta]: { name: "Okta", image: "Okta.png" }, [AppConnection.Redis]: { name: "Redis", image: "Redis.png" }, + [AppConnection.MongoDB]: { name: "MongoDB", image: "MongoDB.png" }, [AppConnection.LaravelForge]: { name: "Laravel Forge", image: "Laravel Forge.png", @@ -181,6 +183,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case OracleDBConnectionMethod.UsernameAndPassword: case AzureADCSConnectionMethod.UsernamePassword: case RedisConnectionMethod.UsernameAndPassword: + case MongoDBConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index d3bb83f19..187177c68 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -54,6 +54,11 @@ export const SECRET_ROTATION_MAP: Record< name: "Redis Credentials", image: "Redis.png", size: 50 + }, + [SecretRotation.MongoDBCredentials]: { + name: "MongoDB Credentials", + image: "MongoDB.png", + size: 50 } }; @@ -67,7 +72,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.LdapPassword]: false, [SecretRotation.AwsIamUserSecret]: true, [SecretRotation.OktaClientSecret]: true, - [SecretRotation.RedisCredentials]: true + [SecretRotation.RedisCredentials]: true, + [SecretRotation.MongoDBCredentials]: true }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 9535e8348..dbe6c7367 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -40,6 +40,7 @@ export enum AppConnection { Northflank = "northflank", Okta = "okta", Redis = "redis", + MongoDB = "mongodb", LaravelForge = "laravel-forge", Chef = "chef" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index d1c991f34..9c0f78a0c 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Redis; }; +export type TMongoDBConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.MongoDB; +}; + export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & { app: AppConnection.DNSMadeEasy; }; @@ -229,6 +233,8 @@ export type TAppConnectionOption = | TOktaConnectionOption | TAzureAdCsConnectionOption | TLaravelForgeConnectionOption + | TRedisConnectionOption + | TMongoDBConnectionOption | TChefConnectionOption | TDNSMadeEasyConnectionOption; @@ -274,6 +280,7 @@ export type TAppConnectionOptionMap = { [AppConnection.Okta]: TOktaConnectionOption; [AppConnection.AzureADCS]: TAzureAdCsConnectionOption; [AppConnection.Redis]: TRedisConnectionOption; + [AppConnection.MongoDB]: TMongoDBConnectionOption; [AppConnection.LaravelForge]: TLaravelForgeConnectionOption; [AppConnection.Chef]: TChefConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index a272b48cd..c78d2aed2 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -26,6 +26,7 @@ import { THerokuConnection } from "./heroku-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TLaravelForgeConnection } from "./laravel-forge-connection"; import { TLdapConnection } from "./ldap-connection"; +import { TMongoDBConnection } from "./mongodb-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TMySqlConnection } from "./mysql-connection"; import { TNetlifyConnection } from "./netlify-connection"; @@ -69,6 +70,7 @@ export * from "./heroku-connection"; export * from "./humanitec-connection"; export * from "./laravel-forge-connection"; export * from "./ldap-connection"; +export * from "./mongodb-connection"; export * from "./mssql-connection"; export * from "./mysql-connection"; export * from "./netlify-connection"; @@ -129,6 +131,7 @@ export type TAppConnection = | TNorthflankConnection | TOktaConnection | TRedisConnection + | TMongoDBConnection | TChefConnection | TDNSMadeEasyConnection; diff --git a/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts b/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts new file mode 100644 index 000000000..5c8e72cb5 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts @@ -0,0 +1,22 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum MongoDBConnectionMethod { + UsernameAndPassword = "username-and-password" +} + +export type TMongoDBConnectionCredentials = { + host: string; + port: number; + username: string; + password: string; + database: string; + tlsEnabled: boolean; + tlsRejectUnauthorized: boolean; + tlsCertificate?: string; +}; + +export type TMongoDBConnection = TRootAppConnection & { app: AppConnection.MongoDB } & { + method: MongoDBConnectionMethod.UsernameAndPassword; + credentials: TMongoDBConnectionCredentials; +}; diff --git a/frontend/src/hooks/api/approvalGrants/index.tsx b/frontend/src/hooks/api/approvalGrants/index.tsx new file mode 100644 index 000000000..140fb4bac --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/index.tsx @@ -0,0 +1,10 @@ +export { useRevokeApprovalGrant } from "./mutations"; +export { approvalGrantQuery } from "./queries"; +export { + ApprovalGrantStatus, + type PamAccessGrantAttributes, + type TApprovalGrant, + type TGetApprovalGrantByIdDTO, + type TListApprovalGrantsDTO, + type TRevokeApprovalGrantDTO +} from "./types"; diff --git a/frontend/src/hooks/api/approvalGrants/mutations.tsx b/frontend/src/hooks/api/approvalGrants/mutations.tsx new file mode 100644 index 000000000..b28ff45f9 --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/mutations.tsx @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { approvalGrantQuery } from "./queries"; +import { TApprovalGrant, TRevokeApprovalGrantDTO } from "./types"; + +export const useRevokeApprovalGrant = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ policyType, grantId, revocationReason }: TRevokeApprovalGrantDTO) => { + const { data } = await apiRequest.post<{ grant: TApprovalGrant }>( + `/api/v1/approval-policies/${policyType}/grants/${grantId}/revoke`, + { revocationReason } + ); + return data.grant; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalGrantQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/approvalGrants/queries.tsx b/frontend/src/hooks/api/approvalGrants/queries.tsx new file mode 100644 index 000000000..ca9334ca7 --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/queries.tsx @@ -0,0 +1,37 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TApprovalGrant, TGetApprovalGrantByIdDTO, TListApprovalGrantsDTO } from "./types"; + +export const approvalGrantQuery = { + allKey: () => ["approval-grants"] as const, + getByIdKey: (params: TGetApprovalGrantByIdDTO) => + [...approvalGrantQuery.allKey(), "by-id", params] as const, + listKey: (params: TListApprovalGrantsDTO) => + [...approvalGrantQuery.allKey(), "list", params] as const, + getById: (params: TGetApprovalGrantByIdDTO) => + queryOptions({ + queryKey: approvalGrantQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ grant: TApprovalGrant }>( + `/api/v1/approval-policies/${params.policyType}/grants/${params.grantId}` + ); + return data.grant; + } + }), + list: (params: TListApprovalGrantsDTO) => + queryOptions({ + queryKey: approvalGrantQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + grants: TApprovalGrant[]; + }>(`/api/v1/approval-policies/${params.policyType}/grants`, { + params: { + projectId: params.projectId + } + }); + return data.grants; + } + }) +}; diff --git a/frontend/src/hooks/api/approvalGrants/types.ts b/frontend/src/hooks/api/approvalGrants/types.ts new file mode 100644 index 000000000..852529259 --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/types.ts @@ -0,0 +1,46 @@ +import { ApprovalPolicyType } from "../approvalPolicies"; + +export enum ApprovalGrantStatus { + Active = "active", + Expired = "expired", + Revoked = "revoked" +} + +// PAM Access Grant Attributes +export type PamAccessGrantAttributes = { + accountPath: string; + accessDuration: string; +}; + +// Base Grant Type +export type TApprovalGrant = { + id: string; + projectId: string; + requestId: string | null; + granteeUserId: string | null; + revokedByUserId: string | null; + revocationReason: string | null; + status: ApprovalGrantStatus; + type: ApprovalPolicyType; + attributes: PamAccessGrantAttributes; + createdAt: string; + expiresAt: string | null; + revokedAt: string | null; +}; + +// DTOs +export type TListApprovalGrantsDTO = { + policyType: ApprovalPolicyType; + projectId: string; +}; + +export type TGetApprovalGrantByIdDTO = { + policyType: ApprovalPolicyType; + grantId: string; +}; + +export type TRevokeApprovalGrantDTO = { + policyType: ApprovalPolicyType; + grantId: string; + revocationReason?: string; +}; diff --git a/frontend/src/hooks/api/approvalPolicies/index.tsx b/frontend/src/hooks/api/approvalPolicies/index.tsx new file mode 100644 index 000000000..303a09844 --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/index.tsx @@ -0,0 +1,19 @@ +export { + useCreateApprovalPolicy, + useDeleteApprovalPolicy, + useUpdateApprovalPolicy +} from "./mutations"; +export { approvalPolicyQuery } from "./queries"; +export { + type ApprovalPolicyStep, + ApprovalPolicyType, + ApproverType, + type PamAccessPolicyConditions, + type PamAccessPolicyConstraints, + type TApprovalPolicy, + type TCreateApprovalPolicyDTO, + type TDeleteApprovalPolicyDTO, + type TGetApprovalPolicyByIdDTO, + type TListApprovalPoliciesDTO, + type TUpdateApprovalPolicyDTO +} from "./types"; diff --git a/frontend/src/hooks/api/approvalPolicies/mutations.tsx b/frontend/src/hooks/api/approvalPolicies/mutations.tsx new file mode 100644 index 000000000..ca9aae0fc --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/mutations.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { approvalPolicyQuery } from "./queries"; +import { + TApprovalPolicy, + TCreateApprovalPolicyDTO, + TDeleteApprovalPolicyDTO, + TUpdateApprovalPolicyDTO +} from "./types"; + +export const useCreateApprovalPolicy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, ...dto }: TCreateApprovalPolicyDTO) => { + const { data } = await apiRequest.post<{ policy: TApprovalPolicy }>( + `/api/v1/approval-policies/${policyType}`, + dto + ); + return data.policy; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() }); + } + }); +}; + +export const useUpdateApprovalPolicy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, policyId, ...updates }: TUpdateApprovalPolicyDTO) => { + const { data } = await apiRequest.patch<{ policy: TApprovalPolicy }>( + `/api/v1/approval-policies/${policyType}/${policyId}`, + updates + ); + return data.policy; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() }); + } + }); +}; + +export const useDeleteApprovalPolicy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, policyId }: TDeleteApprovalPolicyDTO) => { + const { data } = await apiRequest.delete<{ policyId: string }>( + `/api/v1/approval-policies/${policyType}/${policyId}` + ); + return data.policyId; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/approvalPolicies/queries.tsx b/frontend/src/hooks/api/approvalPolicies/queries.tsx new file mode 100644 index 000000000..1c524ddac --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/queries.tsx @@ -0,0 +1,37 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TApprovalPolicy, TGetApprovalPolicyByIdDTO, TListApprovalPoliciesDTO } from "./types"; + +export const approvalPolicyQuery = { + allKey: () => ["approval-policies"] as const, + getByIdKey: (params: TGetApprovalPolicyByIdDTO) => + [...approvalPolicyQuery.allKey(), "by-id", params] as const, + listKey: (params: TListApprovalPoliciesDTO) => + [...approvalPolicyQuery.allKey(), "list", params] as const, + getById: (params: TGetApprovalPolicyByIdDTO) => + queryOptions({ + queryKey: approvalPolicyQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ policy: TApprovalPolicy }>( + `/api/v1/approval-policies/${params.policyType}/${params.policyId}` + ); + return data.policy; + } + }), + list: (params: TListApprovalPoliciesDTO) => + queryOptions({ + queryKey: approvalPolicyQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + policies: TApprovalPolicy[]; + }>(`/api/v1/approval-policies/${params.policyType}`, { + params: { + projectId: params.projectId + } + }); + return data.policies; + } + }) +}; diff --git a/frontend/src/hooks/api/approvalPolicies/types.ts b/frontend/src/hooks/api/approvalPolicies/types.ts new file mode 100644 index 000000000..8fb461814 --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/types.ts @@ -0,0 +1,83 @@ +export enum ApprovalPolicyType { + PamAccess = "pam-access" +} + +export enum ApproverType { + Group = "group", + User = "user" +} + +export type ApprovalPolicyStep = { + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean; + approvers: { + type: ApproverType; + id: string; + }[]; +}; + +export type PamAccessPolicyConditions = { + accountPaths: string[]; +}[]; + +export type PamAccessPolicyConstraints = { + accessDuration: { + min: string; + max: string; + }; +}; + +export type TApprovalPolicy = { + id: string; + projectId: string; + name: string; + maxRequestTtl?: string | null; + type: ApprovalPolicyType; + conditions: { + version: number; + conditions: PamAccessPolicyConditions; + }; + constraints: { + version: number; + constraints: PamAccessPolicyConstraints; + }; + steps: ApprovalPolicyStep[]; + createdAt: string; + updatedAt: string; +}; + +export type TCreateApprovalPolicyDTO = { + policyType: ApprovalPolicyType; + projectId: string; + name: string; + maxRequestTtl?: string | null; + conditions: PamAccessPolicyConditions; + constraints: PamAccessPolicyConstraints; + steps: ApprovalPolicyStep[]; +}; + +export type TUpdateApprovalPolicyDTO = { + policyType: ApprovalPolicyType; + policyId: string; + name?: string; + maxRequestTtl?: string | null; + conditions?: PamAccessPolicyConditions; + constraints?: PamAccessPolicyConstraints; + steps?: ApprovalPolicyStep[]; +}; + +export type TGetApprovalPolicyByIdDTO = { + policyType: ApprovalPolicyType; + policyId: string; +}; + +export type TListApprovalPoliciesDTO = { + policyType: ApprovalPolicyType; + projectId: string; +}; + +export type TDeleteApprovalPolicyDTO = { + policyType: ApprovalPolicyType; + policyId: string; +}; diff --git a/frontend/src/hooks/api/approvalRequests/index.tsx b/frontend/src/hooks/api/approvalRequests/index.tsx new file mode 100644 index 000000000..2b146836a --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/index.tsx @@ -0,0 +1,20 @@ +export { + useApproveApprovalRequest, + useCancelApprovalRequest, + useCreateApprovalRequest, + useRejectApprovalRequest +} from "./mutations"; +export { approvalRequestQuery } from "./queries"; +export { + type ApprovalRequestApproval, + ApprovalRequestStatus, + type ApprovalRequestStep, + ApprovalRequestStepStatus, + type PamAccessRequestData, + type TApprovalRequest, + type TApproveApprovalRequestDTO, + type TCreateApprovalRequestDTO, + type TGetApprovalRequestByIdDTO, + type TListApprovalRequestsDTO, + type TRejectApprovalRequestDTO +} from "./types"; diff --git a/frontend/src/hooks/api/approvalRequests/mutations.tsx b/frontend/src/hooks/api/approvalRequests/mutations.tsx new file mode 100644 index 000000000..81e14b311 --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/mutations.tsx @@ -0,0 +1,75 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { approvalRequestQuery } from "./queries"; +import { + TApprovalRequest, + TApproveApprovalRequestDTO, + TCancelApprovalRequestDTO, + TCreateApprovalRequestDTO, + TRejectApprovalRequestDTO +} from "./types"; + +export const useCreateApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, ...dto }: TCreateApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests`, + dto + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; + +export const useApproveApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, requestId, comment }: TApproveApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests/${requestId}/approve`, + { comment } + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; + +export const useRejectApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, requestId, comment }: TRejectApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests/${requestId}/reject`, + { comment } + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; + +export const useCancelApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, requestId }: TCancelApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests/${requestId}/cancel` + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/approvalRequests/queries.tsx b/frontend/src/hooks/api/approvalRequests/queries.tsx new file mode 100644 index 000000000..3b4abddcc --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/queries.tsx @@ -0,0 +1,37 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TApprovalRequest, TGetApprovalRequestByIdDTO, TListApprovalRequestsDTO } from "./types"; + +export const approvalRequestQuery = { + allKey: () => ["approval-requests"] as const, + getByIdKey: (params: TGetApprovalRequestByIdDTO) => + [...approvalRequestQuery.allKey(), "by-id", params] as const, + listKey: (params: TListApprovalRequestsDTO) => + [...approvalRequestQuery.allKey(), "list", params] as const, + getById: (params: TGetApprovalRequestByIdDTO) => + queryOptions({ + queryKey: approvalRequestQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${params.policyType}/requests/${params.requestId}` + ); + return data.request; + } + }), + list: (params: TListApprovalRequestsDTO) => + queryOptions({ + queryKey: approvalRequestQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + requests: TApprovalRequest[]; + }>(`/api/v1/approval-policies/${params.policyType}/requests`, { + params: { + projectId: params.projectId + } + }); + return data.requests; + } + }) +}; diff --git a/frontend/src/hooks/api/approvalRequests/types.ts b/frontend/src/hooks/api/approvalRequests/types.ts new file mode 100644 index 000000000..2270d4988 --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/types.ts @@ -0,0 +1,110 @@ +import { ApprovalPolicyType, ApproverType } from "../approvalPolicies"; + +export enum ApprovalRequestStatus { + Pending = "pending", + Approved = "approved", + Rejected = "rejected", + Expired = "expired", + Cancelled = "cancelled" +} + +export enum ApprovalRequestStepStatus { + Pending = "pending", + InProgress = "in-progress", + Approved = "approved", + Rejected = "rejected" +} + +export enum ApprovalRequestApprovalDecision { + Approved = "approved", + Rejected = "rejected" +} + +export type ApprovalRequestApproval = { + id: string; + stepId: string; + approverUserId: string; + decision: ApprovalRequestApprovalDecision.Approved; + comment?: string | null; + createdAt: string; + updatedAt: string; +}; + +export type ApprovalRequestStep = { + id: string; + requestId: string; + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean | null; + stepNumber: number; + status: ApprovalRequestStepStatus; + startedAt?: string | null; + completedAt?: string | null; + approvers: { + type: ApproverType; + id: string; + }[]; + approvals: ApprovalRequestApproval[]; + createdAt: string; + updatedAt: string; +}; + +export type PamAccessRequestData = { + accountPath: string; + accessDuration: string; +}; + +export type TApprovalRequest = { + id: string; + projectId: string; + policyId: string; + type: ApprovalPolicyType; + status: ApprovalRequestStatus; + requesterId: string; + requesterName: string; + requesterEmail: string; + justification?: string | null; + expiresAt?: string | null; + requestData: { + version: number; + requestData: PamAccessRequestData; + }; + steps: ApprovalRequestStep[]; + createdAt: string; + updatedAt: string; +}; + +export type TCreateApprovalRequestDTO = { + policyType: ApprovalPolicyType; + projectId: string; + justification?: string | null; + requestDuration?: string | null; + requestData: PamAccessRequestData; +}; + +export type TGetApprovalRequestByIdDTO = { + policyType: ApprovalPolicyType; + requestId: string; +}; + +export type TListApprovalRequestsDTO = { + policyType: ApprovalPolicyType; + projectId: string; +}; + +export type TApproveApprovalRequestDTO = { + policyType: ApprovalPolicyType; + requestId: string; + comment?: string; +}; + +export type TRejectApprovalRequestDTO = { + policyType: ApprovalPolicyType; + requestId: string; + comment?: string; +}; + +export type TCancelApprovalRequestDTO = { + policyType: ApprovalPolicyType; + requestId: string; +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index a11504193..3ace3f841 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -291,7 +291,22 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.SIGN_CERTIFICATE_FROM_PROFILE]: "Sign Certificate From Profile", [EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile", [EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]: - "Get Certificate Profile Latest Active Bundle" + "Get Certificate Profile Latest Active Bundle", + + [EventType.APPROVAL_POLICY_CREATE]: "Create Approval Policy", + [EventType.APPROVAL_POLICY_UPDATE]: "Update Approval Policy", + [EventType.APPROVAL_POLICY_DELETE]: "Delete Approval Policy", + [EventType.APPROVAL_POLICY_LIST]: "List Approval Policies", + [EventType.APPROVAL_POLICY_GET]: "Get Approval Policy", + [EventType.APPROVAL_REQUEST_GET]: "Get Approval Request", + [EventType.APPROVAL_REQUEST_LIST]: "List Approval Requests", + [EventType.APPROVAL_REQUEST_CREATE]: "Create Approval Request", + [EventType.APPROVAL_REQUEST_APPROVE]: "Approve Approval Request", + [EventType.APPROVAL_REQUEST_REJECT]: "Reject Approval Request", + [EventType.APPROVAL_REQUEST_CANCEL]: "Cancel Approval Request", + [EventType.APPROVAL_REQUEST_GRANT_LIST]: "List Approval Request Grants", + [EventType.APPROVAL_REQUEST_GRANT_GET]: "Get Approval Request Grant", + [EventType.APPROVAL_REQUEST_GRANT_REVOKE]: "Revoke Approval Request Grant" }; export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { @@ -309,7 +324,21 @@ const sharedProjectEvents = [ EventType.REMOVE_PROJECT_MEMBER, EventType.CREATE_PROJECT_ROLE, EventType.UPDATE_PROJECT_ROLE, - EventType.DELETE_PROJECT_ROLE + EventType.DELETE_PROJECT_ROLE, + EventType.APPROVAL_POLICY_CREATE, + EventType.APPROVAL_POLICY_UPDATE, + EventType.APPROVAL_POLICY_DELETE, + EventType.APPROVAL_POLICY_LIST, + EventType.APPROVAL_POLICY_GET, + EventType.APPROVAL_REQUEST_GET, + EventType.APPROVAL_REQUEST_LIST, + EventType.APPROVAL_REQUEST_CREATE, + EventType.APPROVAL_REQUEST_APPROVE, + EventType.APPROVAL_REQUEST_REJECT, + EventType.APPROVAL_REQUEST_CANCEL, + EventType.APPROVAL_REQUEST_GRANT_LIST, + EventType.APPROVAL_REQUEST_GRANT_GET, + EventType.APPROVAL_REQUEST_GRANT_REVOKE ]; export const projectToEventsMap: Partial> = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index bde306450..b3be5a6fd 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -282,5 +282,20 @@ export enum EventType { ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile", SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile", ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile", - GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle" + GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle", + + APPROVAL_POLICY_CREATE = "approval-policy-create", + APPROVAL_POLICY_UPDATE = "approval-policy-update", + APPROVAL_POLICY_DELETE = "approval-policy-delete", + APPROVAL_POLICY_LIST = "approval-policy-list", + APPROVAL_POLICY_GET = "approval-policy-get", + APPROVAL_REQUEST_GET = "approval-request-get", + APPROVAL_REQUEST_LIST = "approval-request-list", + APPROVAL_REQUEST_CREATE = "approval-request-create", + APPROVAL_REQUEST_APPROVE = "approval-request-approve", + APPROVAL_REQUEST_REJECT = "approval-request-reject", + APPROVAL_REQUEST_CANCEL = "approval-request-cancel", + APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list", + APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get", + APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke" } diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx index 1e070e2bb..dbdbe7c3b 100644 --- a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx @@ -5,7 +5,6 @@ export enum IdentityProjectAdditionalPrivilegeTemporaryMode { } export type TIdentityProjectPrivilege = { - projectMembershipId: string; slug: string; id: string; createdAt: Date; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 33eacd18d..acbf1d9f5 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,6 +1,9 @@ export * from "./accessApproval"; export * from "./admin"; export * from "./apiKeys"; +export * from "./approvalGrants"; +export * from "./approvalPolicies"; +export * from "./approvalRequests"; export * from "./assumePrivileges"; export * from "./auditLogs"; export * from "./auditLogStreams"; diff --git a/frontend/src/hooks/api/kms/mutations.tsx b/frontend/src/hooks/api/kms/mutations.tsx index 4fb0a5af5..534e9fd5d 100644 --- a/frontend/src/hooks/api/kms/mutations.tsx +++ b/frontend/src/hooks/api/kms/mutations.tsx @@ -6,6 +6,7 @@ import { kmsKeys } from "./queries"; import { AddExternalKmsType, ExternalKmsGcpSchemaType, + ExternalKmsProvider, KmsGcpKeyFetchAuthType, KmsType, UpdateExternalKmsType @@ -14,11 +15,12 @@ import { export const useAddExternalKms = (orgId: string) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ name, description, provider }: AddExternalKmsType) => { - const { data } = await apiRequest.post("/api/v1/external-kms", { + mutationFn: async ({ name, description, configuration }: AddExternalKmsType) => { + const providerPath = configuration.type === ExternalKmsProvider.Aws ? "aws" : "gcp"; + const { data } = await apiRequest.post(`/api/v1/external-kms/${providerPath}`, { name, description, - provider + configuration: configuration.inputs }); return data; @@ -29,21 +31,21 @@ export const useAddExternalKms = (orgId: string) => { }); }; -export const useUpdateExternalKms = (orgId: string) => { +export const useUpdateExternalKms = (orgId: string, provider: ExternalKmsProvider) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ kmsId, name, description, - provider + configuration }: { kmsId: string; } & UpdateExternalKmsType) => { - const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, { + const { data } = await apiRequest.patch(`/api/v1/external-kms/${provider}/${kmsId}`, { name, description, - provider + configuration: configuration?.inputs }); return data; @@ -58,8 +60,8 @@ export const useUpdateExternalKms = (orgId: string) => { export const useRemoveExternalKms = (orgId: string) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (kmsId: string) => { - const { data } = await apiRequest.delete(`/api/v1/external-kms/${kmsId}`); + mutationFn: async ({ kmsId, provider }: { kmsId: string; provider: ExternalKmsProvider }) => { + const { data } = await apiRequest.delete(`/api/v1/external-kms/${provider}/${kmsId}`); return data; }, @@ -130,11 +132,19 @@ export const useExternalKmsFetchGcpKeys = (orgId: string) => { ); } - const { data } = await apiRequest.post("/api/v1/external-kms/gcp/keys", { - authMethod: credential ? KmsGcpKeyFetchAuthType.Credential : KmsGcpKeyFetchAuthType.Kms, - region: gcpRegion, - ...rest - }); + const requestBody = credential + ? { + authMethod: KmsGcpKeyFetchAuthType.Credential, + region: gcpRegion, + credential + } + : { + authMethod: KmsGcpKeyFetchAuthType.Kms, + region: gcpRegion, + kmsId + }; + + const { data } = await apiRequest.post("/api/v1/external-kms/gcp/keys", requestBody); return data; }, diff --git a/frontend/src/hooks/api/kms/queries.tsx b/frontend/src/hooks/api/kms/queries.tsx index 97d25376c..4342c1881 100644 --- a/frontend/src/hooks/api/kms/queries.tsx +++ b/frontend/src/hooks/api/kms/queries.tsx @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { Kms, KmsListEntry } from "./types"; +import { ExternalKmsProvider, Kms, KmsListEntry } from "./types"; export const kmsKeys = { getExternalKmsList: (orgId: string) => ["get-all-external-kms", { orgId }], @@ -23,15 +23,19 @@ export const useGetExternalKmsList = (orgId: string, { enabled }: { enabled?: bo }); }; -export const useGetExternalKmsById = (kmsId: string) => { +export const useGetExternalKmsById = ({ + kmsId, + provider +}: { + kmsId: string; + provider: ExternalKmsProvider; +}) => { return useQuery({ queryKey: kmsKeys.getExternalKmsById(kmsId), enabled: Boolean(kmsId), queryFn: async () => { - const { - data: { externalKms } - } = await apiRequest.get<{ externalKms: Kms }>(`/api/v1/external-kms/${kmsId}`); - return externalKms; + const { data } = await apiRequest.get(`/api/v1/external-kms/${provider}/${kmsId}`); + return data; } }); }; diff --git a/frontend/src/hooks/api/kms/types.ts b/frontend/src/hooks/api/kms/types.ts index 73b821b1a..32c2fa32a 100644 --- a/frontend/src/hooks/api/kms/types.ts +++ b/frontend/src/hooks/api/kms/types.ts @@ -8,12 +8,13 @@ export type Kms = { description: string; orgId: string; name: string; - external: { + externalKms: { id: string; status: string; statusDetails: string; provider: string; - providerInput: Record; + configuration: Record; + credentialsHash?: string; }; }; @@ -123,14 +124,14 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [ export const AddExternalKmsSchema = z.object({ name: slugSchema({ min: 1, field: "Alias" }), description: z.string().trim().optional(), - provider: ExternalKmsInputSchema + configuration: ExternalKmsInputSchema }); export type AddExternalKmsType = z.infer; // we need separate schema for update because the credential field is not required on GCP export const ExternalKmsUpdateInputSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema }), + z.object({ type: z.literal(ExternalKmsProvider.Aws), inputs: ExternalKmsAwsSchema.partial() }), z.object({ type: z.literal(ExternalKmsProvider.Gcp), inputs: ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true }) @@ -144,9 +145,10 @@ export const UpdateExternalKmsSchema = z.object({ .min(1) .refine((v) => slugify(v) === v, { message: "Alias must be a valid slug" - }), + }) + .optional(), description: z.string().trim().optional(), - provider: ExternalKmsUpdateInputSchema + configuration: ExternalKmsUpdateInputSchema.optional() }); export type UpdateExternalKmsType = z.infer; diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index 2c86d9921..c6dfcd70c 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -16,7 +16,8 @@ export enum PamResourceType { CockroachDB = "cockroachdb", Elasticsearch = "elasticsearch", Snowflake = "snowflake", - DynamoDB = "dynamodb" + DynamoDB = "dynamodb", + AwsIam = "aws-iam" } export enum PamResourceOrderBy { diff --git a/frontend/src/hooks/api/pam/maps.ts b/frontend/src/hooks/api/pam/maps.ts index 90286a05d..e42eb7748 100644 --- a/frontend/src/hooks/api/pam/maps.ts +++ b/frontend/src/hooks/api/pam/maps.ts @@ -20,5 +20,6 @@ export const PAM_RESOURCE_TYPE_MAP: Record< [PamResourceType.CockroachDB]: { name: "CockroachDB", image: "CockroachDB.png" }, [PamResourceType.Elasticsearch]: { name: "Elasticsearch", image: "Elastic.png" }, [PamResourceType.Snowflake]: { name: "Snowflake", image: "Snowflake.png" }, - [PamResourceType.DynamoDB]: { name: "DynamoDB", image: "DynamoDB.png", size: 55 } + [PamResourceType.DynamoDB]: { name: "DynamoDB", image: "DynamoDB.png", size: 55 }, + [PamResourceType.AwsIam]: { name: "AWS IAM", image: "Amazon Web Services.png" } }; diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx index c5d6ff05b..ce92de0fe 100644 --- a/frontend/src/hooks/api/pam/mutations.tsx +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -120,6 +120,45 @@ export const useDeletePamAccount = () => { }); }; +export type TAccessPamAccountDTO = { + accountId: string; + accountPath: string; + projectId: string; + duration: string; +}; + +export type TAccessPamAccountResponse = { + sessionId: string; + resourceType: string; + consoleUrl?: string; + metadata?: Record; + relayClientCertificate?: string; + relayClientPrivateKey?: string; + relayServerCertificateChain?: string; + gatewayClientCertificate?: string; + gatewayClientPrivateKey?: string; + gatewayServerCertificateChain?: string; + relayHost?: string; +}; + +export const useAccessPamAccount = () => { + return useMutation({ + mutationFn: async ({ accountId, accountPath, projectId, duration }: TAccessPamAccountDTO) => { + const { data } = await apiRequest.post( + "/api/v1/pam/accounts/access", + { + accountId, + accountPath, + projectId, + duration + } + ); + + return data; + } + }); +}; + // Folders export const useCreatePamFolder = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/pam/types/aws-iam-resource.ts b/frontend/src/hooks/api/pam/types/aws-iam-resource.ts new file mode 100644 index 000000000..8cb51a0ec --- /dev/null +++ b/frontend/src/hooks/api/pam/types/aws-iam-resource.ts @@ -0,0 +1,25 @@ +import { PamResourceType } from "../enums"; +import { TBasePamAccount } from "./base-account"; +import { TBasePamResource } from "./base-resource"; + +export type TAwsIamConnectionDetails = { + roleArn: string; +}; + +export type TAwsIamCredentials = { + targetRoleArn: string; + defaultSessionDuration: number; +}; + +export type TAwsIamResource = Omit & { + resourceType: PamResourceType.AwsIam; + gatewayId?: string | null; + connectionDetails: TAwsIamConnectionDetails; +}; + +export type TAwsIamAccount = Omit< + TBasePamAccount, + "rotationEnabled" | "rotationIntervalSeconds" | "lastRotatedAt" +> & { + credentials: TAwsIamCredentials; +}; diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 01b87c282..878a35aa1 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -6,17 +6,19 @@ import { PamResourceType, PamSessionStatus } from "../enums"; +import { TAwsIamAccount, TAwsIamResource } from "./aws-iam-resource"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; import { TSSHAccount, TSSHResource } from "./ssh-resource"; +export * from "./aws-iam-resource"; export * from "./mysql-resource"; export * from "./postgres-resource"; export * from "./ssh-resource"; -export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource; +export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TAwsIamResource; -export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount; +export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TAwsIamAccount; export type TPamFolder = { id: string; diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index 264a6a4a4..d52de16fc 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -8,7 +8,8 @@ export enum SecretRotation { LdapPassword = "ldap-password", AwsIamUserSecret = "aws-iam-user-secret", OktaClientSecret = "okta-client-secret", - RedisCredentials = "redis-credentials" + RedisCredentials = "redis-credentials", + MongoDBCredentials = "mongodb-credentials" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index a04b0e020..cc938ae05 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -31,6 +31,11 @@ import { TSqlCredentialsRotationOption } from "@app/hooks/api/secretRotationsV2/ import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { DiscriminativePick } from "@app/types"; +import { + TMongoDBCredentialsRotation, + TMongoDBCredentialsRotationGeneratedCredentialsResponse, + TMongoDBCredentialsRotationOption +} from "./mongodb-credentials-rotation"; import { TMySqlCredentialsRotation, TMySqlCredentialsRotationGeneratedCredentialsResponse @@ -61,6 +66,7 @@ export type TSecretRotationV2 = ( | TAwsIamUserSecretRotation | TOktaClientSecretRotation | TRedisCredentialsRotation + | TMongoDBCredentialsRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; @@ -72,7 +78,8 @@ export type TSecretRotationV2Option = | TLdapPasswordRotationOption | TAwsIamUserSecretRotationOption | TOktaClientSecretRotationOption - | TRedisCredentialsRotationOption; + | TRedisCredentialsRotationOption + | TMongoDBCredentialsRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -88,7 +95,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse = | TLdapPasswordRotationGeneratedCredentialsResponse | TAwsIamUserSecretRotationGeneratedCredentialsResponse | TOktaClientSecretRotationGeneratedCredentialsResponse - | TRedisCredentialsRotationGeneratedCredentialsResponse; + | TRedisCredentialsRotationGeneratedCredentialsResponse + | TMongoDBCredentialsRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -142,6 +150,7 @@ export type TSecretRotationOptionMap = { [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption; [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption; [SecretRotation.RedisCredentials]: TRedisCredentialsRotationOption; + [SecretRotation.MongoDBCredentials]: TMongoDBCredentialsRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { @@ -155,4 +164,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse; [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse; [SecretRotation.RedisCredentials]: TRedisCredentialsRotationGeneratedCredentialsResponse; + [SecretRotation.MongoDBCredentials]: TMongoDBCredentialsRotationGeneratedCredentialsResponse; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation.ts new file mode 100644 index 000000000..425357d54 --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation.ts @@ -0,0 +1,28 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase, + TSqlCredentialsRotationGeneratedCredentials, + TSqlCredentialsRotationProperties +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TMongoDBCredentialsRotation = TSecretRotationV2Base & { + type: SecretRotation.MongoDBCredentials; +} & TSqlCredentialsRotationProperties; + +export type TMongoDBCredentialsRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.MongoDBCredentials, + TSqlCredentialsRotationGeneratedCredentials + >; + +export type TMongoDBCredentialsRotationOption = { + name: string; + type: SecretRotation.MongoDBCredentials; + connection: AppConnection.MongoDB; + template: { + createUserStatement: string; + secretsMapping: TMongoDBCredentialsRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index b7ceb4a1b..727fafbf6 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -78,6 +78,7 @@ export type TUserMembership = { scope: string; scopeOrgId: string; actorUserId: string; + actorGroupId: string; }; export type TProjectMembership = { diff --git a/frontend/src/layouts/PamLayout/PamLayout.tsx b/frontend/src/layouts/PamLayout/PamLayout.tsx index 365d3c3eb..743c02f7b 100644 --- a/frontend/src/layouts/PamLayout/PamLayout.tsx +++ b/frontend/src/layouts/PamLayout/PamLayout.tsx @@ -69,6 +69,23 @@ export const PamLayout = () => { > {({ isActive }) => Sessions} + + {({ isActive }) => ( + + Approvals + + )} + { - const [selectedFormat, setSelectedFormat] = useState("pem"); - const [pkcs12Options, setPkcs12Options] = useState({ - password: "", - alias: "" - }); +const exportFormSchema = z + .object({ + format: z.enum(["pem", "pkcs12"]), + pkcs12Password: z.string().optional(), + pkcs12Alias: z.string().optional() + }) + .refine( + (data) => { + if (data.format === "pkcs12") { + return data.pkcs12Password && data.pkcs12Alias && data.pkcs12Alias.trim() !== ""; + } + return true; + }, + { + message: "PKCS12 password and alias are required when using PKCS12 format", + path: ["pkcs12Password"] + } + ) + .refine( + (data) => { + if (data.format === "pkcs12") { + return data.pkcs12Password && data.pkcs12Password.length >= 6; + } + return true; + }, + { + message: "PKCS12 password must be 6 characters or longer", + path: ["pkcs12Password"] + } + ) + .refine( + (data) => { + if (data.format === "pkcs12" && data.pkcs12Password) { + return data.pkcs12Password.length >= 6; + } + return true; + }, + { + message: "Password must be at least 6 characters long", + path: ["pkcs12Password"] + } + ) + .refine( + (data) => { + if (data.format === "pkcs12") { + return data.pkcs12Alias && data.pkcs12Alias.trim() !== ""; + } + return true; + }, + { + message: "Certificate alias is required", + path: ["pkcs12Alias"] + } + ); +type ExportFormData = z.infer; + +export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => { const { certificateId, serialNumber } = (popUp?.certificateExport?.data as { certificateId: string; serialNumber: string; }) || {}; + const { + control, + handleSubmit, + reset, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(exportFormSchema), + defaultValues: { + format: "pem", + pkcs12Password: "", + pkcs12Alias: "" + } + }); + + const selectedFormat = watch("format"); + // Reset form whenever the modal opens useEffect(() => { if (popUp?.certificateExport?.isOpen) { - setSelectedFormat("pem"); - setPkcs12Options({ - password: "", - alias: "" + reset({ + format: "pem", + pkcs12Password: "", + pkcs12Alias: "" }); } - }, [popUp?.certificateExport?.isOpen]); + }, [popUp?.certificateExport?.isOpen, reset]); - const isFormValid = () => { - if (selectedFormat === "pkcs12") { - return pkcs12Options.password.length >= 6 && pkcs12Options.alias.trim() !== ""; + const onFormSubmit = (data: ExportFormData) => { + if (!(certificateId || serialNumber)) return; + + const options: ExportOptions = {}; + + if (data.format === "pkcs12") { + options.pkcs12 = { + password: data.pkcs12Password!, + alias: data.pkcs12Alias! + }; } - return true; - }; - const handleExport = () => { - if ((certificateId || serialNumber) && isFormValid()) { - const options: ExportOptions = {}; - - if (selectedFormat === "pkcs12") { - options.pkcs12 = pkcs12Options; - } - - onFormatSelected( - selectedFormat, - { - certificateId, - serialNumber - }, - options - ); - handlePopUpToggle("certificateExport", false); - } + onFormatSelected( + data.format, + { + certificateId, + serialNumber + }, + options + ); + handlePopUpToggle("certificateExport", false); }; return ( @@ -100,79 +167,89 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec }} > -

-

Choose the format for exporting your certificate

+
+
+

+ Choose the format for exporting your certificate +

- - - - - {selectedFormat === "pkcs12" && ( - <> - 0 && pkcs12Options.password.length < 6 - ? undefined - : "Password to protect the PKCS12 keystore (minimum 6 characters)" - } - isError={pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6} - errorText="Password must be at least 6 characters long" - > - - setPkcs12Options((prev) => ({ ...prev, password: e.target.value })) + ( + - + isError={Boolean(error)} + errorText={error?.message} + > + + + )} + /> - + ( + + + + )} + /> + + ( + + + + )} + /> + + )} + +
+ - + Cancel + + +
-
+ ); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index af26a940d..6fa3d8854 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -35,6 +35,7 @@ import { HerokuConnectionForm } from "./HerokuAppConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { LaravelForgeConnectionForm } from "./LaravelForgeConnectionForm"; import { LdapConnectionForm } from "./LdapConnectionForm"; +import { MongoDBConnectionForm } from "./MongoDBConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { NetlifyConnectionForm } from "./NetlifyConnectionForm"; @@ -173,6 +174,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Redis: return ; + case AppConnection.MongoDB: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -331,6 +334,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Redis: return ; + case AppConnection.MongoDB: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx new file mode 100644 index 000000000..72e359e0a --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx @@ -0,0 +1,326 @@ +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tab } from "@headlessui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem, + Switch, + TextArea, + Tooltip +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { MongoDBConnectionMethod, TMongoDBConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TMongoDBConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.MongoDB) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: z.object({ + host: z.string().trim().min(1, "Host required"), + port: z.coerce.number().default(27017), + username: z.string().trim().min(1, "Username required"), + password: z.string().trim().min(1, "Password required"), + database: z.string().trim().min(1, "Database required"), + tlsEnabled: z.boolean().default(false), + tlsRejectUnauthorized: z.boolean().default(true), + tlsCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const MongoDBConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.MongoDB, + method: MongoDBConnectionMethod.UsernameAndPassword, + credentials: { + host: "", + port: 27017, + username: "", + password: "", + database: "", + tlsEnabled: false, + tlsRejectUnauthorized: true, + tlsCertificate: undefined + } + } + }); + + const { + handleSubmit, + watch, + control, + formState: { isSubmitting, isDirty } + } = form; + + const tlsEnabled = watch("credentials.tlsEnabled"); + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + + + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + Configuration + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + TLS ({tlsEnabled ? "Enabled" : "Disabled"}) + + + + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+
+ + ( + + + Enable TLS + + + )} + /> + ( + +