diff --git a/backend-pg/package-lock.json b/backend-pg/package-lock.json index c9b6fa553..f2adb35ee 100644 --- a/backend-pg/package-lock.json +++ b/backend-pg/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.485.0", "@casl/ability": "^6.5.0", "@fastify/cookie": "^9.2.0", "@fastify/cors": "^8.4.1", @@ -23,6 +24,7 @@ "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", + "aws-sdk": "^2.1532.0", "axios": "^1.6.2", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", @@ -37,6 +39,8 @@ "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", "knex": "^3.0.1", + "libsodium-wrappers": "^0.7.13", + "lodash.isequal": "^4.5.0", "mysql2": "^3.6.5", "nanoid": "^5.0.4", "nodemailer": "^6.9.7", @@ -57,6 +61,8 @@ "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "^9.0.5", "@types/jsrp": "^0.2.6", + "@types/libsodium-wrappers": "^0.7.13", + "@types/lodash.isequal": "^4.5.8", "@types/node": "^20.9.5", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", @@ -93,6 +99,582 @@ "node": ">=0.10.0" } }, + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.485.0.tgz", + "integrity": "sha512-TruRGEdTy1y/5ln1NcU5LvIZyK38O89zU9vCfNQIKwTSrpS0sDJQukjg8VfMC8gbqUUvXdiPcS61Fxr1WfWn7g==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.485.0", + "@aws-sdk/core": "3.485.0", + "@aws-sdk/credential-provider-node": "3.485.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-signing": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/core": "^1.2.2", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.485.0.tgz", + "integrity": "sha512-apN2bEn0PZs0jD4jAfvwO3dlWqw9YIQJ6TAudM1bd3S5vzWqlBBcLfQpK6taHoQaI+WqgUWXLuOf7gRFbGXKPg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.485.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/core": "^1.2.2", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.485.0.tgz", + "integrity": "sha512-PI4q36kVF0fpIPZyeQhrwwJZ6SRkOGvU3rX5Qn4b5UY5X+Ct1aLhqSX8/OB372UZIcnh6eSvERu8POHleDO7Jw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.485.0", + "@aws-sdk/credential-provider-node": "3.485.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/core": "^1.2.2", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-middleware": "^2.0.9", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.485.0.tgz", + "integrity": "sha512-Yvi80DQcbjkYCft471ClE3HuetuNVqntCs6eFOomDcrJaqdOFrXv2kJAxky84MRA/xb7bGlDGAPbTuj1ICputg==", + "dependencies": { + "@smithy/core": "^1.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/signature-v4": "^2.0.0", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.485.0.tgz", + "integrity": "sha512-3XkFgwVU1XOB33dV7t9BKJ/ptdl2iS+0dxE7ecq8aqT2/gsfKmLCae1G17P8WmdD3z0kMDTvnqM2aWgUnSOkmg==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.485.0.tgz", + "integrity": "sha512-cFYF/Bdw7EnT4viSxYpNIv3IBkri/Yb+JpQXl8uDq7bfVJfAN5qZmK07vRkg08xL6TC4F41wshhMSAucGdTwIw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.485.0", + "@aws-sdk/credential-provider-process": "3.485.0", + "@aws-sdk/credential-provider-sso": "3.485.0", + "@aws-sdk/credential-provider-web-identity": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.485.0.tgz", + "integrity": "sha512-2DwzO2azkSzngifKDT61W/DL0tSzewuaFHiLJWdfc8Et3mdAQJ9x3KAj8u7XFpjIcGNqk7FiKjN+zeGUuNiEhA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.485.0", + "@aws-sdk/credential-provider-ini": "3.485.0", + "@aws-sdk/credential-provider-process": "3.485.0", + "@aws-sdk/credential-provider-sso": "3.485.0", + "@aws-sdk/credential-provider-web-identity": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.485.0.tgz", + "integrity": "sha512-X9qS6ZO/rDKYDgWqD1YmSX7sAUUHax9HbXlgGiTTdtfhZvQh1ZmnH6wiPu5WNliafHZFtZT2W07kgrDLPld/Ug==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.485.0.tgz", + "integrity": "sha512-l0oC8GTrWh+LFQQfSmG1Jai1PX7Mhj9arb/CaS1/tmeZE0hgIXW++tvljYs/Dds4LGXUlaWG+P7BrObf6OyIXA==", + "dependencies": { + "@aws-sdk/client-sso": "3.485.0", + "@aws-sdk/token-providers": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.485.0.tgz", + "integrity": "sha512-WpBFZFE0iXtnibH5POMEKITj/hR0YV5l2n9p8BEvKjdJ63s3Xke1RN20ZdIyKDaRDwj8adnKDgNPEnAKdS4kLw==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.485.0.tgz", + "integrity": "sha512-1mAUX9dQNGo2RIKseVj7SI/D5abQJQ/Os8hQ0NyVAyyVYF+Yjx5PphKgfhM5yoBwuwZUl6q71XPYEGNx7be6SA==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.485.0.tgz", + "integrity": "sha512-O8IgJ0LHi5wTs5GlpI7nqmmSSagkVdd1shpGgQWY2h0kMSCII8CJZHBG97dlFFpGTvx5EDlhPNek7rl/6F4dRw==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.485.0.tgz", + "integrity": "sha512-ZeVNATGNFcqkWDut3luVszROTUzkU5u+rJpB/xmeMoenlDAjPRiHt/ca3WkI5wAnIJ1VSNGpD2sOFLMCH+EWag==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.485.0.tgz", + "integrity": "sha512-41xzT2p1sOibhsLkdE5rwPJkNbBtKD8Gp36/ySfu0KE415wfXKacElSVxAaBw39/j7iSWDYqqybeEYbAzk+3GQ==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/signature-v4": "^2.0.0", + "@smithy/types": "^2.8.0", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.485.0.tgz", + "integrity": "sha512-CddCVOn+OPQ0CcchketIg+WF6v+MDLAf3GOYTR2htUxxIm7HABuRd6R3kvQ5Jny9CV8gMt22G1UZITsFexSJlQ==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.485.0.tgz", + "integrity": "sha512-2FB2EQ0sIE+YgFqGtkE1lDIMIL6nYe6MkOHBwBM7bommadKIrbbr2L22bPZGs3ReTsxiJabjzxbuCAVhrpHmhg==", + "dependencies": { + "@smithy/node-config-provider": "^2.1.9", + "@smithy/types": "^2.8.0", + "@smithy/util-config-provider": "^2.1.0", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.485.0.tgz", + "integrity": "sha512-kOXA1WKIVIFNRqHL8ynVZ3hCKLsgnEmGr2iDR6agDNw5fYIlCO/6N2xR6QdGcLTvUUbwOlz4OvKLUQnWMKAnnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.485.0.tgz", + "integrity": "sha512-+QW32YQdvZRDOwrAQPo/qCyXoSjgXB6RwJwCwkd8ebJXRXw6tmGKIHaZqYHt/LtBymvnaBgBBADNa4+qFvlOFw==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.485.0.tgz", + "integrity": "sha512-dTd642F7nJisApF8YjniqQ6U59CP/DCtar11fXf1nG9YNBCBsNNVw5ZfZb5nSNzaIdy27mQioWTCV18JEj1mxg==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/util-endpoints": "^1.0.8", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.465.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.465.0.tgz", + "integrity": "sha512-f+QNcWGswredzC1ExNAB/QzODlxwaTdXkNT5cvke2RLX8SFU5pYk6h4uCtWC0vWPELzOfMfloBrJefBzlarhsw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.485.0.tgz", + "integrity": "sha512-QliWbjg0uOhGTcWgWTKPMY0SBi07g253DjwrCINT1auqDrdQPxa10xozpZExBYjAK2KuhYDNUzni127ae6MHOw==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/types": "^2.8.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.485.0.tgz", + "integrity": "sha512-QF+aQ9jnDlPUlFBxBRqOylPf86xQuD3aEPpOErR+50qJawVvKa94uiAFdvtI9jv6hnRZmuFsTj2rsyytnbAYBA==", + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, "node_modules/@casl/ability": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", @@ -1443,6 +2025,536 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@smithy/abort-controller": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.16.tgz", + "integrity": "sha512-4foO7738k8kM9flMHu3VLabqu7nPgvIj8TB909S0CnKx0YZz/dcDH3pZ/4JHdatfxlZdKF1JWOYCw9+v3HVVsw==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.23.tgz", + "integrity": "sha512-XakUqgtP2YY8Mi+Nlif5BiqJgWdvfxJafSpOSQeCOMizu+PUhE4fBQSy6xFcR+eInrwVadaABNxoJyGUMn15ew==", + "dependencies": { + "@smithy/node-config-provider": "^2.1.9", + "@smithy/types": "^2.8.0", + "@smithy/util-config-provider": "^2.1.0", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.2.2.tgz", + "integrity": "sha512-uLjrskLT+mWb0emTR5QaiAIxVEU7ndpptDaVDrTwwhD+RjvHhjIiGQ3YL5jKk1a5VSDQUA2RGkXvJ6XKRcz6Dg==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/protocol-http": "^3.0.12", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.1.5.tgz", + "integrity": "sha512-VfvE6Wg1MUWwpTZFBnUD7zxvPhLY8jlHCzu6bCjlIYoWgXCDzZAML76IlZUEf45nib3rjehnFgg0s1rgsuN/bg==", + "dependencies": { + "@smithy/node-config-provider": "^2.1.9", + "@smithy/property-provider": "^2.0.17", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.16.tgz", + "integrity": "sha512-umYh5pdCE9GHgiMAH49zu9wXWZKNHHdKPm/lK22WYISTjqu29SepmpWNmPiBLy/yUu4HFEGJHIFrDWhbDlApaw==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^2.8.0", + "@smithy/util-hex-encoding": "^2.0.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.3.2.tgz", + "integrity": "sha512-O9R/OlnAOTsnysuSDjt0v2q6DcSvCz5cCFC/CFAWWcLyBwJDeFyGTCTszgpQTb19+Fi8uRwZE5/3ziAQBFeDMQ==", + "dependencies": { + "@smithy/protocol-http": "^3.0.12", + "@smithy/querystring-builder": "^2.0.16", + "@smithy/types": "^2.8.0", + "@smithy/util-base64": "^2.0.1", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.18.tgz", + "integrity": "sha512-gN2JFvAgnZCyDN9rJgcejfpK0uPPJrSortVVVVWsru9whS7eQey6+gj2eM5ln2i6rHNntIXzal1Fm9XOPuoaKA==", + "dependencies": { + "@smithy/types": "^2.8.0", + "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.16.tgz", + "integrity": "sha512-apEHakT/kmpNo1VFHP4W/cjfeP9U0x5qvfsLJubgp7UM/gq4qYp0GbqdE7QhsjUaYvEnrftRqs7+YrtWreV0wA==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz", + "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.18.tgz", + "integrity": "sha512-ZJ9uKPTfxYheTKSKYB+GCvcj+izw9WGzRLhjn8n254q0jWLojUzn7Vw0l4R/Gq7Wdpf/qmk/ptD+6CCXHNVCaw==", + "dependencies": { + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.3.0.tgz", + "integrity": "sha512-VsOAG2YQ8ykjSmKO+CIXdJBIWFo6AAvG6Iw95BakBTqk66/4BI7XyqLevoNSq/lZ6NgZv24sLmrcIN+fLDWBCg==", + "dependencies": { + "@smithy/middleware-serde": "^2.0.16", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/shared-ini-file-loader": "^2.2.8", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.26.tgz", + "integrity": "sha512-Qzpxo0U5jfNiq9iD38U3e2bheXwvTEX4eue9xruIvEgh+UKq6dKuGqcB66oBDV7TD/mfoJi9Q/VmaiqwWbEp7A==", + "dependencies": { + "@smithy/node-config-provider": "^2.1.9", + "@smithy/protocol-http": "^3.0.12", + "@smithy/service-error-classification": "^2.0.9", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/util-middleware": "^2.0.9", + "@smithy/util-retry": "^2.0.9", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.16.tgz", + "integrity": "sha512-5EAd4t30pcc4M8TSSGq7q/x5IKrxfXR5+SrU4bgxNy7RPHQo2PSWBUco9C+D9Tfqp/JZvprRpK42dnupZafk2g==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.10.tgz", + "integrity": "sha512-I2rbxctNq9FAPPEcuA1ntZxkTKOPQFy7YBPOaD/MLg1zCvzv21CoNxR0py6J8ZVC35l4qE4nhxB0f7TF5/+Ldw==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.1.9.tgz", + "integrity": "sha512-tUyW/9xrRy+s7RXkmQhgYkAPMpTIF8izK4orhHjNFEKR3QZiOCbWB546Y8iB/Fpbm3O9+q0Af9rpywLKJOwtaQ==", + "dependencies": { + "@smithy/property-provider": "^2.0.17", + "@smithy/shared-ini-file-loader": "^2.2.8", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.2.2.tgz", + "integrity": "sha512-XO58TO/Eul/IBQKFKaaBtXJi0ItEQQCT+NI4IiKHCY/4KtqaUT6y/wC1EvDqlA9cP7Dyjdj7FdPs4DyynH3u7g==", + "dependencies": { + "@smithy/abort-controller": "^2.0.16", + "@smithy/protocol-http": "^3.0.12", + "@smithy/querystring-builder": "^2.0.16", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.17.tgz", + "integrity": "sha512-+VkeZbVu7qtQ2DjI48Qwaf9fPOr3gZIwxQpuLJgRRSkWsdSvmaTCxI3gzRFKePB63Ts9r4yjn4HkxSCSkdWmcQ==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.0.12.tgz", + "integrity": "sha512-Xz4iaqLiaBfbQpB9Hgi3VcZYbP7xRDXYhd8XWChh4v94uw7qwmvlxdU5yxzfm6ACJM66phHrTbS5TVvj5uQ72w==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.16.tgz", + "integrity": "sha512-Q/GsJT0C0mijXMRs7YhZLLCP5FcuC4797lYjKQkME5CZohnLC4bEhylAd2QcD3gbMKNjCw8+T2I27WKiV/wToA==", + "dependencies": { + "@smithy/types": "^2.8.0", + "@smithy/util-uri-escape": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.16.tgz", + "integrity": "sha512-c4ueAuL6BDYKWpkubjrQthZKoC3L5kql5O++ovekNxiexRXTlLIVlCR4q3KziOktLIw66EU9SQljPXd/oN6Okg==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.9.tgz", + "integrity": "sha512-0K+8GvtwI7VkGmmInPydM2XZyBfIqLIbfR7mDQ+oPiz8mIinuHbV6sxOLdvX1Jv/myk7XTK9orgt3tuEpBu/zg==", + "dependencies": { + "@smithy/types": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.2.8.tgz", + "integrity": "sha512-E62byatbwSWrtq9RJ7xN40tqrRKDGrEL4EluyNpaIDvfvet06a/QC58oHw2FgVaEgkj0tXZPjZaKrhPfpoU0qw==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.19.tgz", + "integrity": "sha512-nwc3JihdM+kcJjtORv/n7qRHN2Kfh7S2RJI2qr8pz9UcY5TD8rSCRGQ0g81HgyS3jZ5X9U/L4p014P3FonBPhg==", + "dependencies": { + "@smithy/eventstream-codec": "^2.0.16", + "@smithy/is-array-buffer": "^2.0.0", + "@smithy/types": "^2.8.0", + "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/util-middleware": "^2.0.9", + "@smithy/util-uri-escape": "^2.0.0", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.2.1.tgz", + "integrity": "sha512-SpD7FLK92XV2fon2hMotaNDa2w5VAy5/uVjP9WFmjGSgWM8pTPVkHcDl1yFs5Z8LYbij0FSz+DbCBK6i+uXXUA==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "@smithy/util-stream": "^2.0.24", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.8.0.tgz", + "integrity": "sha512-h9sz24cFgt/W1Re22OlhQKmUZkNh244ApgRsUDYinqF8R+QgcsBIX344u2j61TPshsTz3CvL6HYU1DnQdsSrHA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.16.tgz", + "integrity": "sha512-Wfz5WqAoRT91TjRy1JeLR0fXtkIXHGsMbgzKFTx7E68SrZ55TB8xoG+vm11Ru4gheFTMXjAjwAxv1jQdC+pAQA==", + "dependencies": { + "@smithy/querystring-parser": "^2.0.16", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.1.tgz", + "integrity": "sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==", + "dependencies": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.1.tgz", + "integrity": "sha512-NXYp3ttgUlwkaug4bjBzJ5+yIbUbUx8VsSLuHZROQpoik+gRkIBeEG9MPVYfvPNpuXb/puqodeeUXcKFe7BLOQ==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz", + "integrity": "sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz", + "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==", + "dependencies": { + "@smithy/is-array-buffer": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.1.0.tgz", + "integrity": "sha512-S6V0JvvhQgFSGLcJeT1CBsaTR03MM8qTuxMH9WPCCddlSo2W0V5jIHimHtIQALMLEDPGQ0ROSRr/dU0O+mxiQg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "2.0.24", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.24.tgz", + "integrity": "sha512-TsP5mBuLgO2C21+laNG2nHYZEyUdkbGURv2tHvSuQQxLz952MegX95uwdxOY2jR2H4GoKuVRfdJq7w4eIjGYeg==", + "dependencies": { + "@smithy/property-provider": "^2.0.17", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "2.0.32", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.32.tgz", + "integrity": "sha512-d0S33dXA2cq1NyorVMroMrEtqKMr3MlyLITcfTBf9pXiigYiPMOtbSI7czHIfDbuVuM89Cg0urAgpt73QV9mPQ==", + "dependencies": { + "@smithy/config-resolver": "^2.0.23", + "@smithy/credential-provider-imds": "^2.1.5", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/property-provider": "^2.0.17", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.0.8.tgz", + "integrity": "sha512-l8zVuyZZ61IzZBYp5NWvsAhbaAjYkt0xg9R4xUASkg5SEeTT2meHOJwJHctKMFUXe4QZbn9fR2MaBYjP2119+w==", + "dependencies": { + "@smithy/node-config-provider": "^2.1.9", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", + "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.9.tgz", + "integrity": "sha512-PnCnBJ07noMX1lMDTEefmxSlusWJUiLfrme++MfK5TD0xz8NYmakgoXy5zkF/16zKGmiwOeKAztWT/Vjk1KRIQ==", + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.9.tgz", + "integrity": "sha512-46BFWe9RqB6g7f4mxm3W3HlqknqQQmWHKlhoqSFZuGNuiDU5KqmpebMbvC3tjTlUkqn4xa2Z7s3Hwb0HNs5scw==", + "dependencies": { + "@smithy/service-error-classification": "^2.0.9", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "2.0.24", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.24.tgz", + "integrity": "sha512-hRpbcRrOxDriMVmbya+Mv77VZVupxRAsfxVDKS54XuiURhdiwCUXJP0X1iJhHinuUf6n8pBF0MkG9C8VooMnWw==", + "dependencies": { + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/types": "^2.8.0", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-buffer-from": "^2.0.0", + "@smithy/util-hex-encoding": "^2.0.0", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz", + "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.2.tgz", + "integrity": "sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==", + "dependencies": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@swc/core": { "version": "1.3.99", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.99.tgz", @@ -1778,6 +2890,27 @@ "integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==", "dev": true }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", + "integrity": "sha512-KeAKtlObirLJk/na6jHBFEdTDjDfFS6Vcr0eG2FjiHKn3Nw8axJFfIu0Y9TpwaauRldQBj/pZm/MHtK76r6OWg==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "node_modules/@types/lodash.isequal": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", + "integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2814,6 +3947,67 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/aws-sdk": { + "version": "2.1532.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1532.0.tgz", + "integrity": "sha512-4QVQs01LEAxo7UpSHlq/HaO+SJ1WrYF8W1otO2WhKpVRYXkSxXIgZgfYaK+sQ762XTtB6tSuD2ZS2HGsKNXVLw==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/aws-sdk/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/aws-sdk/node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", @@ -2926,6 +4120,11 @@ "node": ">= 6" } }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -4354,6 +5553,27 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" }, + "node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastify": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.24.3.tgz", @@ -4492,9 +5712,9 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -5272,6 +6492,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -5390,6 +6625,20 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5897,6 +7146,19 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", + "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", + "integrity": "sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw==", + "dependencies": { + "libsodium": "^0.7.13" + } + }, "node_modules/light-my-request": { "version": "5.11.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", @@ -5986,6 +7248,11 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -7422,6 +8689,15 @@ "node": ">=6" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8318,6 +9594,11 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -9448,6 +10729,32 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/backend-pg/package.json b/backend-pg/package.json index b8352e786..09cd5f4b0 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -35,6 +35,8 @@ "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "^9.0.5", "@types/jsrp": "^0.2.6", + "@types/libsodium-wrappers": "^0.7.13", + "@types/lodash.isequal": "^4.5.8", "@types/node": "^20.9.5", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", @@ -63,6 +65,7 @@ "vitest": "^1.0.4" }, "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.485.0", "@casl/ability": "^6.5.0", "@fastify/cookie": "^9.2.0", "@fastify/cors": "^8.4.1", @@ -77,6 +80,7 @@ "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", + "aws-sdk": "^2.1532.0", "axios": "^1.6.2", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", @@ -91,6 +95,8 @@ "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", "knex": "^3.0.1", + "libsodium-wrappers": "^0.7.13", + "lodash.isequal": "^4.5.0", "mysql2": "^3.6.5", "nanoid": "^5.0.4", "nodemailer": "^6.9.7", diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts index 0ffded964..428501f1f 100644 --- a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts @@ -73,7 +73,8 @@ export const secretRotationHttpFn = async ( url, headers, data: body, - timeout: EXTERNAL_REQUEST_TIMEOUT + timeout: EXTERNAL_REQUEST_TIMEOUT, + signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT) }); }; diff --git a/backend-pg/src/lib/fn/index.ts b/backend-pg/src/lib/fn/index.ts index 6ce0d2eb0..4b4a01a14 100644 --- a/backend-pg/src/lib/fn/index.ts +++ b/backend-pg/src/lib/fn/index.ts @@ -3,3 +3,4 @@ // Code taken to keep in in house and to adjust somethings for our needs export * from "./array"; export * from "./object"; +export * from "./string"; diff --git a/backend-pg/src/lib/fn/string.ts b/backend-pg/src/lib/fn/string.ts new file mode 100644 index 000000000..27d84357d --- /dev/null +++ b/backend-pg/src/lib/fn/string.ts @@ -0,0 +1,5 @@ +import path from "path"; + +// given two paths irrespective of ending with / or not +// this will return true if its equal +export const isSamePath = async (from: string, to: string) => !path.relative(from, to); diff --git a/backend-pg/src/lib/knex/index.ts b/backend-pg/src/lib/knex/index.ts index ed41e069b..be7dab042 100644 --- a/backend-pg/src/lib/knex/index.ts +++ b/backend-pg/src/lib/knex/index.ts @@ -117,13 +117,13 @@ export const ormify = ( } }, update: async ( - filter: Partial, + filter: TFindFilter, data: Tables[Tname]["update"], tx?: Knex ) => { try { const res = await (tx || db)(tableName) - .where(filter) + .where(buildFindFilter(filter)) .update(data as any) .returning("*"); return res; @@ -142,9 +142,12 @@ export const ormify = ( throw new DatabaseError({ error, name: "Delete by id" }); } }, - delete: async (filter: Partial, tx?: Knex) => { + delete: async (filter: TFindFilter, tx?: Knex) => { try { - const res = await (tx || db)(tableName).where(filter).delete().returning("*"); + const res = await (tx || db)(tableName) + .where(buildFindFilter(filter)) + .delete() + .returning("*"); return res; } catch (error) { throw new DatabaseError({ error, name: "Delete" }); diff --git a/backend-pg/src/queue/queue-service.ts b/backend-pg/src/queue/queue-service.ts index 0023b02de..b72ffa03e 100644 --- a/backend-pg/src/queue/queue-service.ts +++ b/backend-pg/src/queue/queue-service.ts @@ -5,12 +5,16 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; export enum QueueName { SecretRotation = "secret-rotation", - AuditLog = "audit-log" + AuditLog = "audit-log", + IntegrationSync = "sync-integrations", + SecretWebhook = "secret-webhook" } export enum QueueJobs { SecretRotation = "secret-rotation-job", - AuditLog = "audit-log-job" + AuditLog = "audit-log-job", + SecWebhook = "secret-webhook-trigger", + IntegrationSync = "secret-integration-pull" } export type TQueueJobTypes = { @@ -22,6 +26,14 @@ export type TQueueJobTypes = { name: QueueJobs.AuditLog; payload: TCreateAuditLogDTO; }; + [QueueName.SecretWebhook]: { + name: QueueJobs.SecWebhook; + payload: { projectId: string; environment: string; secretPath: string }; + }; + [QueueName.IntegrationSync]: { + name: QueueJobs.IntegrationSync; + payload: { projectId: string; environment: string; secretPath: string }; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend-pg/src/server/routes/index.ts b/backend-pg/src/server/routes/index.ts index 70f66e5b1..1b0e19ffe 100644 --- a/backend-pg/src/server/routes/index.ts +++ b/backend-pg/src/server/routes/index.ts @@ -67,6 +67,7 @@ import { projectRoleDalFactory } from "@app/services/project-role/project-role-d import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { secretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal"; import { secretDalFactory } from "@app/services/secret/secret-dal"; +import { secretQueueFactory } from "@app/services/secret/secret-queue"; import { secretServiceFactory } from "@app/services/secret/secret-service"; import { secretVersionDalFactory } from "@app/services/secret/secret-version-dal"; import { secretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -245,16 +246,12 @@ export const registerRoutes = async ( folderVersionDal, permissionService }); - - const secretService = secretServiceFactory({ - folderDal, - secretVersionDal, - secretBlindIndexDal, + const webhookService = webhookServiceFactory({ permissionService, - secretDal, - secretTagDal, - snapshotService + webhookDal, + projectEnvDal }); + const secretTagService = secretTagServiceFactory({ secretTagDal, permissionService }); const folderService = secretFolderServiceFactory({ permissionService, @@ -271,7 +268,34 @@ export const registerRoutes = async ( secretDal }); const projectBotService = projectBotServiceFactory({ permissionService, projectBotDal }); - + const integrationAuthService = integrationAuthServiceFactory({ + integrationAuthDal, + integrationDal, + permissionService, + projectBotDal, + projectBotService + }); + const secretQueueService = secretQueueFactory({ + queueService, + webhookService, + secretDal, + folderDal, + secretImportService, + integrationAuthService, + projectBotService, + integrationDal, + secretImportDal + }); + const secretService = secretServiceFactory({ + folderDal, + secretVersionDal, + secretBlindIndexDal, + permissionService, + secretDal, + secretTagDal, + snapshotService, + secretQueueService + }); const sarService = secretApprovalRequestServiceFactory({ permissionService, folderDal, @@ -303,18 +327,6 @@ export const registerRoutes = async ( integrationDal, integrationAuthDal }); - const integrationAuthService = integrationAuthServiceFactory({ - integrationAuthDal, - integrationDal, - permissionService, - projectBotDal, - projectBotService - }); - const webhookService = webhookServiceFactory({ - permissionService, - webhookDal, - projectEnvDal - }); const serviceTokenService = serviceTokenServiceFactory({ projectEnvDal, serviceTokenDal, diff --git a/backend-pg/src/services/integration-auth/integration-auth-service.ts b/backend-pg/src/services/integration-auth/integration-auth-service.ts index dffb7e416..3c0fab359 100644 --- a/backend-pg/src/services/integration-auth/integration-auth-service.ts +++ b/backend-pg/src/services/integration-auth/integration-auth-service.ts @@ -1019,6 +1019,7 @@ export const integrationAuthServiceFactory = ({ getRailwayEnvironments, getNorthFlankSecretGroups, getTeamcityBuildConfigs, - getBitbucketWorkspaces + getBitbucketWorkspaces, + getIntegrationAccessToken }; }; diff --git a/backend-pg/src/services/integration-auth/integration-sync-secret.ts b/backend-pg/src/services/integration-auth/integration-sync-secret.ts new file mode 100644 index 000000000..11ff5c5c2 --- /dev/null +++ b/backend-pg/src/services/integration-auth/integration-sync-secret.ts @@ -0,0 +1,3261 @@ +/* eslint-disable no-param-reassign,no-await-in-loop */ +import { + CreateSecretCommand, + GetSecretValueCommand, + ResourceNotFoundException, + SecretsManagerClient, + UpdateSecretCommand +} from "@aws-sdk/client-secrets-manager"; +import { Octokit } from "@octokit/rest"; +import AWS from "aws-sdk"; +import sodium from "libsodium-wrappers"; +import isEqual from "lodash.isequal"; +import { z } from "zod"; + +import { TIntegrationAuths, TIntegrations } from "@app/db/schemas"; +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; + +import { Integrations, IntegrationUrls } from "./integration-list"; + +const getSecretKeyValuePair = ( + secrets: Record +) => + Object.keys(secrets).reduce>((prev, key) => { + // eslint-disable-next-line + prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value; + return prev; + }, {}); + +const ZGetTenantEnv = z.object({ + data: z.object({ + getTenantEnv: z.object({ + hash: z.string(), + envVars: z.object({ + environment: z.record(z.any()).optional() + }) + }) + }) +}); + +const ZUpdateTenantEnv = z.object({ + data: z.object({ + updateTenantEnv: z.object({ + hash: z.string(), + envVars: z.record(z.any()) + }) + }) +}); + +/** + * Sync/push [secrets] to GCP secret manager project + */ +const syncSecretsGCPSecretManager = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface GCPSecret { + name: string; + createTime: string; + } + + interface GCPSMListSecretsRes { + secrets?: GCPSecret[]; + totalSize?: number; + nextPageToken?: string; + } + + let gcpSecrets: GCPSecret[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + const metadata = z.record(z.any()).parse(integration.metadata); + const filterParam = metadata.secretGCPLabel + ? `?filter=labels.${metadata.secretGCPLabel.labelName}=${metadata.secretGCPLabel.labelValue}` + : ""; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + const res: GCPSMListSecretsRes = ( + await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data; + + if (res.secrets) { + const filteredSecrets = res.secrets?.filter((gcpSecret) => { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + let isValid = true; + + if (metadata.secretPrefix && !key.startsWith(metadata.secretPrefix)) { + isValid = false; + } + + if (metadata.secretSuffix && !key.endsWith(metadata.secretSuffix)) { + isValid = false; + } + + return isValid; + }); + + gcpSecrets = gcpSecrets.concat(filteredSecrets); + } + + if (!res.nextPageToken) { + hasMorePages = false; + } + + pageToken = res.nextPageToken; + } + + const res: { [key: string]: string } = {}; + + interface GCPLatestSecretVersionAccess { + name: string; + payload: { + data: string; + }; + } + + for await (const gcpSecret of gcpSecrets) { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + const secretLatest: GCPLatestSecretVersionAccess = ( + await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data; + + res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); + } + + for await (const key of Object.keys(secrets)) { + if (!(key in res)) { + // case: create secret + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets`, + { + replication: { + automatic: {} + }, + ...(metadata.secretGCPLabel + ? { + labels: { + [metadata.secretGCPLabel.labelName]: metadata.secretGCPLabel.labelValue + } + } + : {}) + }, + { + params: { + secretId: key + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secrets[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // case: delete secret + await request.delete( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else if (secrets[key].value !== res[key]) { + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secrets[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } +}; + +/** + * Sync/push [secrets] to Azure Key Vault with vault URI [integration.app] + */ +const syncSecretsAzureKeyVault = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface GetAzureKeyVaultSecret { + id: string; // secret URI + attributes: { + enabled: true; + created: number; + updated: number; + recoveryLevel: string; + recoverableDays: number; + }; + } + + interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { + key: string; + } + + /** + * Return all secrets from Azure Key Vault by paginating through URL [url] + * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault + * @returns + */ + const paginateAzureKeyVaultSecrets = async (url: string) => { + let result: GetAzureKeyVaultSecret[] = []; + while (url) { + const res = await request.get(url, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + result = result.concat(res.data.value); + + url = res.data.nextLink; + } + + return result; + }; + + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets( + `${integration.app}/secrets?api-version=7.3` + ); + + let lastSlashIndex: number; + const res = ( + await Promise.all( + getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + if (!lastSlashIndex) { + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); + } + + const azureKeyVaultSecret = await request.get( + `${getAzureKeyVaultSecret.id}?api-version=7.3`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + return { + ...azureKeyVaultSecret.data, + key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1) + }; + }) + ) + ).reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), + {} + ); + + const setSecrets: { + key: string; + value: string; + }[] = []; + + Object.keys(secrets).forEach((key) => { + const hyphenatedKey = key.replace(/_/g, "-"); + if (!(hyphenatedKey in res)) { + // case: secret has been created + setSecrets.push({ + key: hyphenatedKey, + value: secrets[key].value + }); + } else if (secrets[key] !== res[hyphenatedKey].value) { + // case: secret has been updated + setSecrets.push({ + key: hyphenatedKey, + value: secrets[key].value + }); + } + }); + + const deleteSecrets: AzureKeyVaultSecret[] = []; + + Object.keys(res).forEach((key) => { + const underscoredKey = key.replace(/-/g, "_"); + if (!(underscoredKey in secrets)) { + deleteSecrets.push(res[key]); + } + }); + + const setSecretAzureKeyVault = async ({ + key, + value, + integration: azIntegration, + accessToken: accToken + }: { + key: string; + value: string; + integration: TIntegrations; + accessToken: string; + }) => { + let isSecretSet = false; + let maxTries = 6; + + while (!isSecretSet && maxTries > 0) { + // try to set secret + try { + await request.put( + `${azIntegration.app}/secrets/${key}?api-version=7.3`, + { + value + }, + { + headers: { + Authorization: `Bearer ${accToken}` + } + } + ); + + isSecretSet = true; + } catch (err) { + const error: any = err; + if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { + await request.post( + `${azIntegration.app}/deletedsecrets/${key}/recover?api-version=7.3`, + {}, + { + headers: { + Authorization: `Bearer ${accToken}` + } + } + ); + await new Promise((resolve) => { + setTimeout(resolve, 10000); + }); + } else { + await new Promise((resolve) => { + setTimeout(resolve, 10000); + }); + maxTries -= 1; + } + } + } + }; + + // Sync/push set secrets + for await (const setSecret of setSecrets) { + const { key, value } = setSecret; + setSecretAzureKeyVault({ + key, + value, + integration, + accessToken + }); + } + + for await (const deleteSecret of deleteSecrets) { + const { key } = deleteSecret; + await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } +}; + +/** + * Sync/push [secrets] to AWS parameter store + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessId - access id for AWS parameter store integration + * @param {String} obj.accessToken - access token for AWS parameter store integration + */ +const syncSecretsAWSParameterStore = async ({ + integration, + secrets, + accessId, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessId: string | null; + accessToken: string; +}) => { + if (!accessId) return; + + AWS.config.update({ + region: integration.region as string, + accessKeyId: accessId, + secretAccessKey: accessToken + }); + + const ssm = new AWS.SSM({ + apiVersion: "2014-11-06", + region: integration.region as string + }); + + const params = { + Path: integration.path as string, + Recursive: true, + WithDecryption: true + }; + + const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters; + + let awsParameterStoreSecretsObj: { + [key: string]: any; + } = {}; + + if (parameterList) { + awsParameterStoreSecretsObj = parameterList.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.Name.substring((integration.path as string).length)]: secret + }), + {} + ); + } + + // Identify secrets to create + Object.keys(secrets).map(async (key) => { + if (!(key in awsParameterStoreSecretsObj)) { + // case: secret does not exist in AWS parameter store + // -> create secret + await ssm + .putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key].value, + Overwrite: true + }) + .promise(); + // case: secret exists in AWS parameter store + } else if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { + // case: secret value doesn't match one in AWS parameter store + // -> update secret + await ssm + .putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key].value, + Overwrite: true + }) + .promise(); + } + }); + + // Identify secrets to delete + Object.keys(awsParameterStoreSecretsObj).map(async (key) => { + if (!(key in secrets)) { + // case: + // -> delete secret + await ssm + .deleteParameter({ + Name: awsParameterStoreSecretsObj[key].Name + }) + .promise(); + } + }); + + AWS.config.update({ + region: undefined, + accessKeyId: undefined, + secretAccessKey: undefined + }); +}; + +/** + * Sync/push [secrets] to AWS Secrets Manager + */ +const syncSecretsAWSSecretManager = async ({ + integration, + secrets, + accessId, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessId: string | null; + accessToken: string; +}) => { + let secretsManager; + const secKeyVal = getSecretKeyValuePair(secrets); + try { + if (!accessId) return; + + AWS.config.update({ + region: integration.region as string, + accessKeyId: accessId, + secretAccessKey: accessToken + }); + + secretsManager = new SecretsManagerClient({ + region: integration.region as string, + credentials: { + accessKeyId: accessId, + secretAccessKey: accessToken + } + }); + + const awsSecretManagerSecret = await secretsManager.send( + new GetSecretValueCommand({ + SecretId: integration.app as string + }) + ); + + let awsSecretManagerSecretObj: { [key: string]: any } = {}; + + if (awsSecretManagerSecret?.SecretString) { + awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); + } + + if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) { + await secretsManager.send( + new UpdateSecretCommand({ + SecretId: integration.app as string, + SecretString: JSON.stringify(secKeyVal) + }) + ); + } + + AWS.config.update({ + region: undefined, + accessKeyId: undefined, + secretAccessKey: undefined + }); + } catch (err) { + if (err instanceof ResourceNotFoundException && secretsManager) { + await secretsManager.send( + new CreateSecretCommand({ + Name: integration.app as string, + SecretString: JSON.stringify(secKeyVal) + }) + ); + } + AWS.config.update({ + region: undefined, + accessKeyId: undefined, + secretAccessKey: undefined + }); + } +}; + +/** + * Sync/push [secrets] to Heroku app named [integration.app] + */ +const syncSecretsHeroku = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + const herokuSecrets = ( + await request.get(`${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`, { + headers: { + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ).data; + + Object.keys(herokuSecrets).forEach((key) => { + if (!(key in secrets)) { + secrets[key] = null; + } + }); + + await request.patch( + `${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`, + getSecretKeyValuePair(secrets), + { + headers: { + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to Vercel project named [integration.app] + */ +const syncSecretsVercel = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: TIntegrations; + integrationAuth: TIntegrationAuths; + secrets: Record; + accessToken: string; +}) => { + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + gitBranch?: string; + } + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: "true", + ...(integrationAuth?.teamId + ? { + teamId: integrationAuth.teamId + } + : {}), + ...(integration?.path + ? { + gitBranch: integration?.path + } + : {}) + }; + + const vercelSecrets: VercelSecret[] = ( + await request.get(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ).data.envs.filter((secret: VercelSecret) => { + if (!secret.target.includes(integration.targetEnvironment as string)) { + // case: secret does not have the same target environment + return false; + } + + if ( + integration.targetEnvironment === "preview" && + secret.gitBranch && + integration.path !== secret.gitBranch + ) { + // case: secret on preview environment does not have same target git branch + return false; + } + + return true; + }); + + const res: { [key: string]: VercelSecret } = {}; + + for await (const vercelSecret of vercelSecrets) { + if (vercelSecret.type === "encrypted") { + // case: secret is encrypted -> need to decrypt + const decryptedSecret = ( + await request.get( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data; + + res[vercelSecret.key] = decryptedSecret; + } else { + res[vercelSecret.key] = vercelSecret; + } + } + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).forEach((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key, + value: secrets[key].value, + type: "encrypted", + target: [integration.targetEnvironment as string], + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) + }); + } + }); + + // Identify secrets to update and delete + Object.keys(res).forEach((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key].value) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key, + value: secrets[key].value, + type: res[key].type, + target: res[key].target.includes(integration.targetEnvironment as string) + ? [...res[key].target] + : [...res[key].target, integration.targetEnvironment as string], + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key, + value: res[key].value, + type: "encrypted", // value doesn't matter + target: [integration.targetEnvironment as string], + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) + }); + } + }); + + // Sync/push new secrets + if (newSecrets.length > 0) { + await request.post( + `${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + + for await (const secret of updateSecrets) { + if (secret.type !== "sensitive") { + const { id, ...updatedSecret } = secret; + await request.patch( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, + updatedSecret, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + + for await (const secret of deleteSecrets) { + await request.delete( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } +}; + +/** + * Sync/push [secrets] to Netlify site with id [integration.appId] + */ +const syncSecretsNetlify = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: TIntegrations; + integrationAuth: TIntegrationAuths; + secrets: Record; + accessToken: string; +}) => { + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; + } + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + const getParams = new URLSearchParams({ + context_name: "all", // integration.context or all + site_id: integration.appId as string + }); + + const res = ( + await request.get( + `${IntegrationUrls.NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), + {} + ); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).forEach((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [ + { + value: secrets[key].value, + context: integration.targetEnvironment as string + } + ] + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values.reduce( + (obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value + }), + {} + ); + + if ((integration.targetEnvironment as string) in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.targetEnvironment as string].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment as string, + value: secrets[key].value + } + ] + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment as string, + value: secrets[key].value + } + ] + }); + } + } + }); + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).forEach((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.targetEnvironment) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [ + { + id: value.id, + context: integration.targetEnvironment, + value: value.value + } + ] + }); + } + } + }); + } + }); + + const syncParams = new URLSearchParams({ + site_id: integration.appId as string + }); + + if (newSecrets.length > 0) { + await request.post( + `${IntegrationUrls.NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await request.patch( + `${IntegrationUrls.NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + }); + } + + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await request.delete( + `${IntegrationUrls.NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + }); + } + + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await request.delete( + `${IntegrationUrls.NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + }); + } +}; + +/** + * Sync/push [secrets] to GitHub repo with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {TIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for GitHub integration + */ +const syncSecretsGitHub = async ({ + integration, + secrets, + accessToken, + appendices +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; + appendices?: { prefix: string; suffix: string }; +}) => { + interface GitHubRepoKey { + key_id: string; + key: string; + } + + interface GitHubSecret { + name: string; + created_at: string; + updated_at: string; + } + + interface GitHubSecretRes { + [index: string]: GitHubSecret; + } + + const octokit = new Octokit({ + auth: accessToken + }); + + // const user = (await octokit.request('GET /user', {})).data; + const repoPublicKey: GitHubRepoKey = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { + owner: integration.owner as string, + repo: integration.app as string + }) + ).data; + + // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key + let encryptedSecrets: GitHubSecretRes = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { + owner: integration.owner as string, + repo: integration.app as string + }) + ).data.secrets.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret + }), + {} + ); + + encryptedSecrets = Object.keys(encryptedSecrets).reduce( + ( + result: { + [key: string]: GitHubSecret; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = encryptedSecrets[key]; + } + return result; + }, + {} + ); + + Object.keys(encryptedSecrets).map(async (key) => { + if (!(key in secrets)) { + await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: key + }); + } + }); + + Object.keys(secrets).forEach((key) => { + // let encryptedSecret; + sodium.ready.then(async () => { + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL); + const binsec = sodium.from_string(secrets[key].value); + + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); + + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL); + + await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id + }); + }); + }); +}; + +/** + * Sync/push [secrets] to Render service with id [integration.appId] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Render integration + */ +const syncSecretsRender = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + await request.put( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, + Object.keys(secrets).map((key) => ({ + key, + value: secrets[key].value + })), + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to Laravel Forge sites with id [integration.appId] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Laravel Forge integration + */ +const syncSecretsLaravelForge = async ({ + integration, + secrets, + accessId, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessId: string | null; + accessToken: string; +}) => { + function transformObjectToString(obj: any) { + let result = ""; + for (const key in obj) { + if (obj.hasOwnPropery(key)) { + result += `${key}=${obj[key].value}\n`; + } + } + return result; + } + + await request.put( + `${IntegrationUrls.LARAVELFORGE_API_URL}/api/v1/servers/${accessId}/sites/${integration.appId}/env`, + { + content: transformObjectToString(secrets) + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to Railway project with id [integration.appId] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Railway integration + */ +const syncSecretsRailway = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + const query = ` + mutation UpsertVariables($input: VariableCollectionUpsertInput!) { + variableCollectionUpsert(input: $input) + } + `; + + const input = { + projectId: integration.appId, + environmentId: integration.targetEnvironmentId, + ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), + replace: true, + variables: getSecretKeyValuePair(secrets) + }; + + await request.post( + IntegrationUrls.RAILWAY_API_URL, + { + query, + variables: { + input + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to Fly.io app + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Render integration + */ +const syncSecretsFlyio = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + // set secrets + const SetSecrets = ` + mutation($input: SetSecretsInput!) { + setSecrets(input: $input) { + release { + id + version + reason + description + user { + id + email + name + } + evaluationId + createdAt + } + } + } + `; + + await request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: SetSecrets, + variables: { + input: { + appId: integration.app, + secrets: Object.entries(secrets).map(([key, data]) => ({ + key, + value: data.value + })) + } + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // get secrets + interface FlyioSecret { + name: string; + digest: string; + createdAt: string; + } + + const GetSecrets = `query ($appName: String!) { + app(name: $appName) { + secrets { + name + digest + createdAt + } + } + }`; + + const getSecretsRes = ( + await request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: GetSecrets, + variables: { + appName: integration.app + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ) + ).data.data.app.secrets; + + const deleteSecretsKeys = getSecretsRes + .filter((secret: FlyioSecret) => !(secret.name in secrets)) + .map((secret: FlyioSecret) => secret.name); + + // unset (delete) secrets + const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { + unsetSecrets(input: $input) { + release { + id + version + reason + description + user { + id + email + name + } + evaluationId + createdAt + } + } + }`; + + await request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: DeleteSecrets, + variables: { + input: { + appId: integration.app, + keys: deleteSecretsKeys + } + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to CircleCI project + */ +const syncSecretsCircleCI = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + const circleciOrganizationDetail = ( + await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + } + }) + ).data[0]; + + const { slug } = circleciOrganizationDetail; + + // sync secrets to CircleCI + await Promise.all( + Object.keys(secrets).map(async (key) => + request.post( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + name: key, + value: secrets[key].value + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } + ) + ) + ); + + // get secrets from CircleCI + const getSecretsRes = ( + await request.get( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + } + } + ) + ).data?.items; + + // delete secrets from CircleCI + getSecretsRes.forEach(async (sec: any) => { + if (!(sec.name in secrets)) { + await request.delete( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } + ); + } + }); +}; + +/** + * Sync/push [secrets] to TravisCI project + */ +const syncSecretsTravisCI = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + // get secrets from travis-ci + const getSecretsRes = ( + await request.get( + `${IntegrationUrls.TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, + { + headers: { + Authorization: `token ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data?.env_vars.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret + }), + {} + ); + + // add secrets + for await (const key of Object.keys(secrets)) { + if (!(key in getSecretsRes)) { + // case: secret does not exist in travis ci + // -> add secret + await request.post( + `${IntegrationUrls.TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, + { + env_var: { + name: key, + value: secrets[key].value + } + }, + { + headers: { + Authorization: `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); + } else { + // case: secret exists in travis ci + // -> update/set secret + await request.patch( + `${IntegrationUrls.TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, + { + env_var: { + name: key, + value: secrets[key].value + } + }, + { + headers: { + Authorization: `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // delete secret + await request.delete( + `${IntegrationUrls.TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, + { + headers: { + Authorization: `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); + } + } +}; + +/** + * Sync/push [secrets] to GitLab repo with name [integration.app] + */ +const syncSecretsGitLab = async ({ + integrationAuth, + integration, + secrets, + accessToken +}: { + integrationAuth: TIntegrationAuths; + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface GitLabSecret { + key: string; + value: string; + environment_scope: string; + } + + const gitLabApiUrl = integrationAuth.url + ? `${integrationAuth.url}/api` + : IntegrationUrls.GITLAB_API_URL; + + const getAllEnvVariables = async (integrationAppId: string, accToken: string) => { + const headers = { + Authorization: `Bearer ${accToken}`, + "Accept-Encoding": "application/json", + "Content-Type": "application/json" + }; + + let allEnvVariables: GitLabSecret[] = []; + let url: string | null = + `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`; + + while (url) { + const response: any = await request.get(url, { headers }); + allEnvVariables = [...allEnvVariables, ...response.data]; + + const linkHeader = response.headers.link; + const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"')); + + if (nextLink) { + url = nextLink.trim().split(";")[0].slice(1, -1); + } else { + url = null; + } + } + + return allEnvVariables; + }; + + const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken); + const getSecretsRes: GitLabSecret[] = allEnvVariables + .filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment) + .filter((gitLabSecret) => { + let isValid = true; + + const metadata = z.record(z.any()).parse(integration.metadata); + if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) { + isValid = false; + } + + if (metadata.secretSuffix && !gitLabSecret.key.endsWith(metadata.secretSuffix)) { + isValid = false; + } + + return isValid; + }); + + for await (const key of Object.keys(secrets)) { + const existingSecret = getSecretsRes.find((s: any) => s.key === key); + if (!existingSecret) { + await request.post( + `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables`, + { + key, + value: secrets[key].value, + protected: false, + masked: false, + raw: false, + environment_scope: integration.targetEnvironment + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); + } else if (secrets[key].value !== existingSecret.value) { + await request.put( + `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, + { + ...existingSecret, + value: secrets[existingSecret.key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); + } + } + + // delete secrets + for await (const sec of getSecretsRes) { + if (!(sec.key in secrets)) { + await request.delete( + `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } + } +}; + +/** + * Sync/push [secrets] to Supabase with name [integration.app] + */ +const syncSecretsSupabase = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + const { data: getSecretsRes } = await request.get( + `${IntegrationUrls.SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // convert the secrets to [{}] format + const modifiedFormatForSecretInjection = Object.keys(secrets).map((key) => ({ + name: key, + value: secrets[key].value + })); + + await request.post( + `${IntegrationUrls.SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + const secretsToDelete: any = []; + getSecretsRes?.forEach((secretObj: any) => { + if ( + !(secretObj.name in secrets) && + // supbase reserved secret ref: https://supabase.com/docs/guides/functions/secrets#default-secrets + ![ + "SUPABASE_ANON_KEY", + "SUPABASE_SERVICE_ROLE_KEY", + "SUPABASE_DB_URL", + "SUPABASE_URL" + ].includes(secretObj.name) + ) { + secretsToDelete.push(secretObj.name); + } + }); + + await request.delete( + `${IntegrationUrls.SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + }, + data: secretsToDelete + } + ); +}; + +/** + * Sync/push [secrets] to Checkly app/group + */ +const syncSecretsCheckly = async ({ + integration, + secrets, + accessToken, + appendices +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; + appendices?: { prefix: string; suffix: string }; +}) => { + if (integration.targetServiceId) { + // sync secrets to checkly group envars + + let getGroupSecretsRes = ( + await request.get( + `${IntegrationUrls.CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ) + ).data.environmentVariables.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret.value + }), + {} + ); + + getGroupSecretsRes = Object.keys(getGroupSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getGroupSecretsRes[key]; + } + return result; + }, + {} + ); + + const groupEnvironmentVariables = Object.keys(secrets).map((key) => ({ + key, + value: secrets[key].value + })); + + await request.put( + `${IntegrationUrls.CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, + { + environmentVariables: groupEnvironmentVariables + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } else { + // sync secrets to checkly global envars + + let getSecretsRes = ( + await request.get(`${IntegrationUrls.CHECKLY_API_URL}/v1/variables`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + "X-Checkly-Account": integration.appId + } + }) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret.value + }), + {} + ); + + getSecretsRes = Object.keys(getSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getSecretsRes[key]; + } + return result; + }, + {} + ); + + // add secrets + for await (const key of Object.keys(secrets)) { + if (!(key in getSecretsRes)) { + // case: secret does not exist in checkly + // -> add secret + await request.post( + `${IntegrationUrls.CHECKLY_API_URL}/v1/variables`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + // case: secret exists in checkly + // -> update/set secret + } else if (secrets[key] !== getSecretsRes[key]) { + await request.put( + `${IntegrationUrls.CHECKLY_API_URL}/v1/variables/${key}`, + { + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + } + ); + } + + for await (const getSecKey of Object.keys(getSecretsRes)) { + if (!(getSecKey in secrets)) { + // delete secret + await request.delete(`${IntegrationUrls.CHECKLY_API_URL}/v1/variables/${getSecKey}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "X-Checkly-Account": integration.appId + } + }); + } + } + } + } +}; + +/** + * Sync/push [secrets] to Qovery app + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Qovery integration + */ +const syncSecretsQovery = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + const getSecretsRes = ( + await request.get( + `${IntegrationUrls.QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, + { + headers: { + Authorization: `Token ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data.results.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: { id: secret.id, value: secret.value } + }), + {} + ); + + // add secrets + for await (const key of Object.keys(secrets)) { + if (!(key in getSecretsRes)) { + // case: secret does not exist in qovery + // -> add secret + await request.post( + `${IntegrationUrls.QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Token ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json" + } + } + ); + // case: secret exists in qovery + // -> update/set secret + } else if (secrets[key].value !== getSecretsRes[key].value) { + await request.put( + `${IntegrationUrls.QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Token ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + } + } + + // This one is dangerous because there might be a lot of qovery-specific secrets + + // for await (const key of Object.keys(getSecretsRes)) { + // if (!(key in secrets)) { + // console.log(3) + // // delete secret + // await request.delete(`${IntegrationUrls.QOVERY_API_URL}/application/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`, { + // headers: { + // Authorization: `Token ${accessToken}`, + // Accept: "application/json", + // "X-Qovery-Account": integration.appId + // } + // }); + // } + // } +}; + +/** + * Sync/push [secrets] to Terraform Cloud project with id [integration.appId] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Terraform Cloud API + */ +const syncSecretsTerraformCloud = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + // get secrets from Terraform Cloud + const getSecretsRes = ( + await request.get( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.attributes.key]: secret + }), + {} + ); + + // create or update secrets on Terraform Cloud + for await (const key of Object.keys(secrets)) { + if (!(key in getSecretsRes)) { + // case: secret does not exist in Terraform Cloud + // -> add secret + await request.post( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, + { + data: { + type: "vars", + attributes: { + key, + value: secrets[key].value, + category: integration.targetService + } + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json" + } + } + ); + // case: secret exists in Terraform Cloud + } else if (secrets[key].value !== getSecretsRes[key].attributes.value) { + // -> update secret + await request.patch( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`, + { + data: { + type: "vars", + id: getSecretsRes[key].id, + attributes: { + ...getSecretsRes[key], + value: secrets[key].value + } + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json" + } + } + ); + } + } + + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // case: delete secret + await request.delete( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json" + } + } + ); + } + } +}; + +/** + * Sync/push [secrets] to TeamCity project (and optionally build config) + */ +const syncSecretsTeamCity = async ({ + integrationAuth, + integration, + secrets, + accessToken +}: { + integrationAuth: TIntegrationAuths; + integration: TIntegrations; + secrets: any; + accessToken: string; +}) => { + interface TeamCitySecret { + name: string; + value: string; + } + + interface TeamCityBuildConfigParameter { + name: string; + value: string; + inherited: boolean; + } + interface GetTeamCityBuildConfigParametersRes { + href: string; + count: number; + property: TeamCityBuildConfigParameter[]; + } + + if (integration.targetEnvironment && integration.targetEnvironmentId) { + // case: sync to specific build-config in TeamCity project + const res = ( + await request.get( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.property + .filter((parameter) => !parameter.inherited) + .reduce((obj: any, secret: TeamCitySecret) => { + const secretName = secret.name.replace(/^env\./, ""); + return { + ...obj, + [secretName]: secret.value + }; + }, {}); + + for await (const key of Object.keys(secrets)) { + if (!(key in res) || (key in res && secrets[key].value !== res[key])) { + // case: secret does not exist in TeamCity or secret value has changed + // -> create/update secret + await request.post( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, + { + name: `env.${key}`, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await request.delete( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters/env.${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + } else { + // case: sync to TeamCity project + const res = ( + await request.get( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.property.reduce((obj: any, secret: TeamCitySecret) => { + const secretName = secret.name.replace(/^env\./, ""); + return { + ...obj, + [secretName]: secret.value + }; + }, {}); + + for await (const key of Object.keys(secrets)) { + if (!(key in res) || (key in res && secrets[key] !== res[key])) { + // case: secret does not exist in TeamCity or secret value has changed + // -> create/update secret + await request.post( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + { + name: `env.${key}`, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await request.delete( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + } +}; + +/** + * Sync/push [secrets] to HashiCorp Vault path + */ +const syncSecretsHashiCorpVault = async ({ + integration, + integrationAuth, + secrets, + accessId, + accessToken +}: { + integration: TIntegrations; + integrationAuth: TIntegrationAuths; + secrets: Record; + accessId: string | null; + accessToken: string; +}) => { + if (!accessId) return; + + interface LoginAppRoleRes { + auth: { + client_token: string; + }; + } + + // get Vault client token (could be optimized) + const { data }: { data: LoginAppRoleRes } = await request.post( + `${integrationAuth.url}/v1/auth/approle/login`, + { + role_id: accessId, + secret_id: accessToken + }, + { + headers: { + "X-Vault-Namespace": integrationAuth.namespace + } + } + ); + + const clientToken = data.auth.client_token; + + await request.post( + `${integrationAuth.url}/v1/${integration.app}/data/${integration.path}`, + { + data: getSecretKeyValuePair(secrets) + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json", + "X-Vault-Token": clientToken, + "X-Vault-Namespace": integrationAuth.namespace + } + } + ); +}; + +/** + * Sync/push [secrets] to Cloudflare Pages project with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - API token for Cloudflare + */ +const syncSecretsCloudflarePages = async ({ + integration, + secrets, + accessId, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessId: string | null; + accessToken: string; +}) => { + // get secrets from cloudflare pages + const getSecretsRes = ( + await request.get( + `${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.result.deployment_configs[integration.targetEnvironment as string].env_vars; + + // copy the secrets object, so we can set deleted keys to null + const secretsObj: any = getSecretKeyValuePair(secrets); + + for (const [key, val] of Object.entries(secretsObj)) { + secretsObj[key] = { type: "secret_text", value: val }; + } + + if (getSecretsRes) { + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // case: secret does not exist in infisical + // -> delete secret from cloudflare pages + secretsObj[key] = null; + } + } + } + + const data = { + deployment_configs: { + [integration.targetEnvironment as string]: { + env_vars: secretsObj + } + } + }; + + await request.patch( + `${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to Cloudflare Workers project with name [integration.app] + */ +const syncSecretsCloudflareWorkers = async ({ + integration, + secrets, + accessId, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessId: string | null; + accessToken: string; +}) => { + // get secrets from cloudflare workers + const getSecretsRes = ( + await request.get( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accessId}/workers/scripts/${integration.app}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.result; + + const secretsObj: any = getSecretKeyValuePair(secrets); + + for (const [key, val] of Object.entries(secretsObj)) { + secretsObj[key] = { type: "secret_text", value: val }; + } + + // get deleted secrets list + const deletedSecretKeys: string[] = []; + if (getSecretsRes) { + getSecretsRes.forEach((secretRes: any) => { + if (!Object.keys(secrets).includes(secretRes.name)) { + deletedSecretKeys.push(secretRes.name); + } + }); + } + + deletedSecretKeys.forEach(async (secretKey) => { + await request.delete( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accessId}/workers/scripts/${integration.app}/secrets/${secretKey}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + }); + + interface ConvertedSecret { + name: string; + text: string; + type: string; + } + + interface SecretsObj { + [key: string]: { + type: string; + value: string; + }; + } + + const data: ConvertedSecret[] = Object.entries(secretsObj as SecretsObj).map( + ([name, secret]) => ({ + name, + text: secret.value, + type: "secret_text" + }) + ); + + data.forEach(async (secret) => { + await request.put( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accessId}/workers/scripts/${integration.app}/secrets`, + secret, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + }); +}; + +/** + * Sync/push [secrets] to BitBucket repo with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {TIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for BitBucket integration + */ +const syncSecretsBitBucket = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface VariablesResponse { + size: number; + page: number; + pageLen: number; + next: string; + previous: string; + values: Array; + } + + interface BitbucketVariable { + type: string; + uuid: string; + key: string; + value: string; + secured: boolean; + } + + const res: { [key: string]: BitbucketVariable } = {}; + + let hasNextPage = true; + let variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`; + + while (hasNextPage) { + const { data }: { data: VariablesResponse } = await request.get(variablesUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); + + if (data?.values.length > 0) { + data.values.forEach((variable) => { + res[variable.key] = variable; + }); + } + + if (data.next) { + variablesUrl = data.next; + } else { + hasNextPage = false; + } + } + + for await (const key of Object.keys(secrets)) { + if (key in res) { + // update existing secret + await request.put( + `${variablesUrl}/${res[key].uuid}`, + { + key, + value: secrets[key].value, + secured: true + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } else { + // create new secret + await request.post( + variablesUrl, + { + key, + value: secrets[key].value, + secured: true + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await request.delete(`${variablesUrl}/${res[key].uuid}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); + } + } +}; + +/** + * Sync/push [secrets] to Codefresh project with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {TIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Codefresh integration + */ +const syncSecretsCodefresh = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + await request.patch( + `${IntegrationUrls.CODEFRESH_API_URL}/projects/${integration.appId}`, + { + variables: Object.keys(secrets).map((key) => ({ + key, + value: secrets[key].value + })) + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to DigitalOcean App Platform application with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {TIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for integration + */ +const syncSecretsDigitalOceanAppPlatform = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + // get current app settings + const appSettings = ( + await request.get(`${IntegrationUrls.DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }) + ).data.app.spec; + + await request.put( + `${IntegrationUrls.DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`, + { + spec: { + name: integration.app, + ...appSettings, + envs: Object.entries(secrets).map(([key, data]) => ({ key, value: data.value })) + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); +}; + +/** + * Sync/push [secrets] to Windmill with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {TIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for windmill integration + * @param {Object} obj.secretComments - secret comments to push to integration (object where keys are secret keys and values are comment values) + */ +const syncSecretsWindmill = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface WindmillSecret { + path: string; + value: string; + is_secret: boolean; + description?: string; + } + + // get secrets stored in windmill workspace + const res = ( + await request.get(`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/list`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ).data.reduce( + (obj: any, secret: WindmillSecret) => ({ + ...obj, + [secret.path]: secret + }), + {} + ); + + // eslint-disable-next-line + const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); + + for await (const key of Object.keys(secrets)) { + if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { + if (!(key in res)) { + // case: secret does not exist in windmill + // -> create secret + + await request.post( + `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`, + { + path: key, + value: secrets[key].value, + is_secret: true, + description: secrets[key]?.comment || "" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else { + // -> update secret + await request.post( + `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, + { + path: key, + value: secrets[key].value, + is_secret: true, + description: secrets[key]?.comment || "" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // -> delete secret + await request.delete( + `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" + } + } + ); + } + } +}; + +/** + * Sync/push [secrets] to Cloud66 application with name [integration.app] + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {TIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Cloud66 integration + */ +const syncSecretsCloud66 = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + interface Cloud66Secret { + id: number; + key: string; + value: string; + readonly: boolean; + created_at: string; + updated_at: string; + is_password: boolean; + is_generated: boolean; + history: any[]; + } + + // get all current secrets + const res = ( + await request.get( + `${IntegrationUrls.CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) + ).data.response + .filter((secret: Cloud66Secret) => !secret.readonly || !secret.is_generated) + .reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), + {} + ); + + for await (const key of Object.keys(secrets)) { + if (key in res) { + // update existing secret + await request.put( + `${IntegrationUrls.CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } else { + // create new secret + await request.post( + `${IntegrationUrls.CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`, + { + key, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await request.delete( + `${IntegrationUrls.CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } +}; + +/** Sync/push [secrets] to Northflank + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Northflank integration + */ +const syncSecretsNorthflank = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + await request.patch( + `${IntegrationUrls.NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${integration.targetServiceId}`, + { + secrets: { + variables: getSecretKeyValuePair(secrets) + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); +}; + +/** Sync/push [secrets] to Hasura Cloud + * @param {Object} obj + * @param {TIntegrations} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Hasura Cloud integration + */ +const syncSecretsHasuraCloud = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + const res = await request.post( + IntegrationUrls.HASURA_CLOUD_API_URL, + { + query: + "query MyQuery($tenantId: uuid!) { getTenantEnv(tenantId: $tenantId) { hash envVars } }", + variables: { + tenantId: integration.appId + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const { + data: { + getTenantEnv: { hash, envVars } + } + } = ZGetTenantEnv.parse(res.data); + + let currentHash = hash; + + const secretsToUpdate = Object.keys(secrets).map((key) => ({ + key, + value: secrets[key].value + })); + + if (secretsToUpdate.length) { + // update secrets + + const addRequest = await request.post( + IntegrationUrls.HASURA_CLOUD_API_URL, + { + query: + "mutation MyQuery($currentHash: String!, $envs: [UpdateEnvObject!]!, $tenantId: uuid!) { updateTenantEnv(currentHash: $currentHash, envs: $envs, tenantId: $tenantId) { hash envVars} }", + variables: { + currentHash, + envs: secretsToUpdate, + tenantId: integration.appId + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const addRequestResponse = ZUpdateTenantEnv.safeParse(addRequest.data); + if (addRequestResponse.success) { + currentHash = addRequestResponse.data.data.updateTenantEnv.hash; + } + } + + const secretsToDelete = envVars.environment + ? Object.keys(envVars.environment).filter((key) => !(key in secrets)) + : []; + + if (secretsToDelete.length) { + await request.post( + IntegrationUrls.HASURA_CLOUD_API_URL, + { + query: ` + mutation deleteTenantEnv($id: uuid!, $currentHash: String!, $env: [String!]!) { + deleteTenantEnv(tenantId: $id, currentHash: $currentHash, deleteEnvs: $env) { + hash + envVars + } + } + `, + variables: { + id: integration.appId, + currentHash, + env: secretsToDelete + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + } +}; + +/** + * Sync/push [secrets] to [app] in integration named [integration] + */ +export const syncIntegrationSecrets = async ({ + integration, + integrationAuth, + secrets, + accessId, + accessToken, + appendices +}: { + integration: TIntegrations; + integrationAuth: TIntegrationAuths; + secrets: Record; + accessId: string | null; + accessToken: string; + appendices?: { prefix: string; suffix: string }; +}) => { + switch (integration.integration) { + case Integrations.GCP_SECRET_MANAGER: + await syncSecretsGCPSecretManager({ + integration, + secrets, + accessToken + }); + break; + case Integrations.AZURE_KEY_VAULT: + await syncSecretsAzureKeyVault({ + integration, + secrets, + accessToken + }); + break; + case Integrations.AWS_PARAMETER_STORE: + await syncSecretsAWSParameterStore({ + integration, + secrets, + accessId, + accessToken + }); + break; + case Integrations.AWS_SECRET_MANAGER: + await syncSecretsAWSSecretManager({ + integration, + secrets, + accessId, + accessToken + }); + break; + case Integrations.HEROKU: + await syncSecretsHeroku({ + integration, + secrets, + accessToken + }); + break; + case Integrations.VERCEL: + await syncSecretsVercel({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; + case Integrations.NETLIFY: + await syncSecretsNetlify({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; + case Integrations.GITHUB: + await syncSecretsGitHub({ + integration, + secrets, + accessToken, + appendices + }); + break; + case Integrations.GITLAB: + await syncSecretsGitLab({ + integrationAuth, + integration, + secrets, + accessToken + }); + break; + case Integrations.RENDER: + await syncSecretsRender({ + integration, + secrets, + accessToken + }); + break; + case Integrations.RAILWAY: + await syncSecretsRailway({ + integration, + secrets, + accessToken + }); + break; + case Integrations.FLYIO: + await syncSecretsFlyio({ + integration, + secrets, + accessToken + }); + break; + case Integrations.CIRCLECI: + await syncSecretsCircleCI({ + integration, + secrets, + accessToken + }); + break; + case Integrations.LARAVELFORGE: + await syncSecretsLaravelForge({ + integration, + secrets, + accessId, + accessToken + }); + break; + case Integrations.TRAVISCI: + await syncSecretsTravisCI({ + integration, + secrets, + accessToken + }); + break; + case Integrations.SUPABASE: + await syncSecretsSupabase({ + integration, + secrets, + accessToken + }); + break; + case Integrations.CHECKLY: + await syncSecretsCheckly({ + integration, + secrets, + accessToken, + appendices + }); + break; + case Integrations.QOVERY: + await syncSecretsQovery({ + integration, + secrets, + accessToken + }); + break; + case Integrations.TERRAFORM_CLOUD: + await syncSecretsTerraformCloud({ + integration, + secrets, + accessToken + }); + break; + case Integrations.HASHICORP_VAULT: + await syncSecretsHashiCorpVault({ + integration, + integrationAuth, + secrets, + accessId, + accessToken + }); + break; + case Integrations.CLOUDFLARE_PAGES: + await syncSecretsCloudflarePages({ + integration, + secrets, + accessId, + accessToken + }); + break; + case Integrations.CLOUDFLARE_WORKERS: + await syncSecretsCloudflareWorkers({ + integration, + secrets, + accessId, + accessToken + }); + break; + case Integrations.CODEFRESH: + await syncSecretsCodefresh({ + integration, + secrets, + accessToken + }); + break; + case Integrations.TEAMCITY: + await syncSecretsTeamCity({ + integrationAuth, + integration, + secrets, + accessToken + }); + break; + case Integrations.BITBUCKET: + await syncSecretsBitBucket({ + integration, + secrets, + accessToken + }); + break; + case Integrations.DIGITAL_OCEAN_APP_PLATFORM: + await syncSecretsDigitalOceanAppPlatform({ + integration, + secrets, + accessToken + }); + break; + case Integrations.CLOUD_66: + await syncSecretsCloud66({ + integration, + secrets, + accessToken + }); + break; + case Integrations.NORTHFLANK: + await syncSecretsNorthflank({ + integration, + secrets, + accessToken + }); + break; + case Integrations.WINDMILL: + await syncSecretsWindmill({ + integration, + secrets, + accessToken + }); + break; + + case Integrations.HASURA_CLOUD: + await syncSecretsHasuraCloud({ + integration, + secrets, + accessToken + }); + break; + default: + throw new BadRequestError({ message: "Invalid integration" }); + } +}; diff --git a/backend-pg/src/services/integration/integration-dal.ts b/backend-pg/src/services/integration/integration-dal.ts index f42684546..80a9957e6 100644 --- a/backend-pg/src/services/integration/integration-dal.ts +++ b/backend-pg/src/services/integration/integration-dal.ts @@ -89,5 +89,105 @@ export const integrationDalFactory = (db: TDbClient) => { } }; - return { ...integrationOrm, find, findOne, findById, findByProjectId }; + // used for syncing secrets + // this will populate integration auth also + const findByProjectIdV2 = async (projectId: string, environment: string, tx?: Knex) => { + const docs = await (tx || db)(TableName.Integration) + .where(`${TableName.Environment}.projectId`, projectId) + .where("isActive", true) + .where(`${TableName.Environment}.slug`, environment) + .join(TableName.Environment, `${TableName.Integration}.envId`, `${TableName.Environment}.id`) + .join( + TableName.IntegrationAuth, + `${TableName.IntegrationAuth}.id`, + `${TableName.Integration}.integrationAuthId` + ) + .select(db.ref("name").withSchema(TableName.Environment).as("envName")) + .select(db.ref("slug").withSchema(TableName.Environment).as("envSlug")) + .select(db.ref("id").withSchema(TableName.Environment).as("envId")) + .select(db.ref("projectId").withSchema(TableName.Environment)) + .select(selectAllTableCols(TableName.Integration)) + .select( + db.ref("id").withSchema(TableName.IntegrationAuth).as("idAu"), + db.ref("integration").withSchema(TableName.IntegrationAuth).as("integrationAu"), + db.ref("teamId").withSchema(TableName.IntegrationAuth).as("teamIdAu"), + db.ref("url").withSchema(TableName.IntegrationAuth).as("urlAu"), + db.ref("namespace").withSchema(TableName.IntegrationAuth).as("namespaceAu"), + db.ref("accountId").withSchema(TableName.IntegrationAuth).as("accountIdAu"), + db.ref("refreshCiphertext").withSchema(TableName.IntegrationAuth).as("refreshCiphertextAu"), + db.ref("refreshIV").withSchema(TableName.IntegrationAuth).as("refreshIVAu"), + db.ref("refreshTag").withSchema(TableName.IntegrationAuth).as("refreshTagAu"), + db + .ref("accessIdCiphertext") + .withSchema(TableName.IntegrationAuth) + .as("accessIdCiphertextAu"), + db.ref("accessIdIV").withSchema(TableName.IntegrationAuth).as("accessIdIVAu"), + db.ref("accessIdTag").withSchema(TableName.IntegrationAuth).as("accessIdTagAu"), + db.ref("accessIV").withSchema(TableName.IntegrationAuth).as("accessIVAu"), + db.ref("accessTag").withSchema(TableName.IntegrationAuth).as("accessTagAu"), + db.ref("accessCiphertext").withSchema(TableName.IntegrationAuth).as("accessCiphertextAu"), + db.ref("accessExpiresAt").withSchema(TableName.IntegrationAuth).as("accessExpiresAtAu"), + db.ref("metadata").withSchema(TableName.IntegrationAuth).as("metadataAu"), + db.ref("algorithm").withSchema(TableName.IntegrationAuth).as("algorithmAu"), + db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu") + ); + return docs.map( + ({ + envId, + envName, + envSlug, + idAu: id, + integrationAu: integration, + teamIdAu: teamId, + urlAu: url, + namespaceAu: namespace, + accountIdAu: accountId, + refreshIVAu: refreshIV, + refreshCiphertextAu: refreshCiphertext, + refreshTagAu: refreshTag, + accessIVAu: accessIV, + accessCiphertextAu: accessCiphertext, + accessTagAu: accessTag, + accessIdIVAu: accessIdIV, + accessIdTagAu: accessIdTag, + accessIdCiphertextAu: accessIdCiphertext, + metadataAu: metadata, + algorithmAu: algorithm, + keyEncodingAu: keyEncoding, + accessExpiresAtAu: accessExpiresAt, + ...el + }) => ({ + ...el, + envId, + environment: { + id: envId, + name: envName, + slug: envSlug + }, + integrationAuth: { + id, + integration, + teamId, + url, + namespace, + accountId, + refreshTag, + refreshIV, + refreshCiphertext, + accessIdCiphertext, + accessIdIV, + accessIdTag, + accessIV, + accessCiphertext, + accessTag, + metadata, + algorithm, + keyEncoding, + accessExpiresAt + } + }) + ); + }; + + return { ...integrationOrm, find, findOne, findById, findByProjectId, findByProjectIdV2 }; }; diff --git a/backend-pg/src/services/secret-import/secret-import-service.ts b/backend-pg/src/services/secret-import/secret-import-service.ts index 1f2c791c2..07a2c4f04 100644 --- a/backend-pg/src/services/secret-import/secret-import-service.ts +++ b/backend-pg/src/services/secret-import/secret-import-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { SecretType, TSecretImports } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -175,6 +176,37 @@ export const secretImportServiceFactory = ({ return secImports; }; + const fnSecretsFromImports = async ( + allowedImports: (Omit & { + importEnv: { id: string; slug: string; name: string }; + })[] + ) => { + const importedFolders = await folderDal.findByManySecretPath( + allowedImports.map(({ importEnv, importPath }) => ({ + envId: importEnv.id, + secretPath: importPath + })) + ); + const folderIds = importedFolders.map((el) => el?.id).filter(Boolean) as string[]; + if (!folderIds.length) { + return []; + } + const importedSecrets = await secretDal.find({ + $in: { folderId: folderIds }, + type: SecretType.Shared + }); + + const importedSecsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); + return allowedImports.map(({ importPath, importEnv }, i) => ({ + secretPath: importPath, + environment: importEnv, + folderId: importedFolders?.[i]?.id, + secrets: importedFolders?.[i]?.id + ? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string] + : [] + })); + }; + const getSecretsFromImports = async ({ path, environment, @@ -202,29 +234,7 @@ export const secretImportServiceFactory = ({ }) ) ); - const importedFolders = await folderDal.findByManySecretPath( - allowedImports.map(({ importEnv, importPath }) => ({ - envId: importEnv.id, - secretPath: importPath - })) - ); - const folderIds = importedFolders.map((el) => el?.id).filter(Boolean) as string[]; - if (!folderIds.length) { - return []; - } - const importedSecrets = await secretDal.find({ - $in: { folderId: folderIds } - }); - - const importedSecsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); - return allowedImports.map(({ importPath, importEnv }, i) => ({ - secretPath: importPath, - environment: importEnv, - folderId: importedFolders?.[i]?.id, - secrets: importedFolders?.[i]?.id - ? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string] - : [] - })); + return fnSecretsFromImports(allowedImports); }; return { @@ -232,6 +242,7 @@ export const secretImportServiceFactory = ({ updateImport, deleteImport, getImports, - getSecretsFromImports + getSecretsFromImports, + fnSecretsFromImports }; }; diff --git a/backend-pg/src/services/secret/secret-fns.ts b/backend-pg/src/services/secret/secret-fns.ts new file mode 100644 index 000000000..03859cdc0 --- /dev/null +++ b/backend-pg/src/services/secret/secret-fns.ts @@ -0,0 +1,179 @@ +/* eslint-disable no-await-in-loop */ +import path from "path"; + +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; + +import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretDalFactory } from "./secret-dal"; + +type TInterpolateSecretArg = { + projectId: string; + secretEncKey: string; + secretDal: Pick; + folderDal: Pick; +}; + +export const interpolateSecrets = ({ + projectId, + secretEncKey, + secretDal, + folderDal +}: TInterpolateSecretArg) => { + const fetchSecretsCrossEnv = () => { + const fetchCache: Record> = {}; + + return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => { + const secRefPathUrl = path.join("/", ...secRefPath); + const uniqKey = `${secRefEnv}-${secRefPathUrl}`; + + if (fetchCache?.[uniqKey]) { + return fetchCache[uniqKey][secRefKey]; + } + + const folder = await folderDal.findBySecretPath(projectId, secRefEnv, secRefPathUrl); + if (!folder) return ""; + const secrets = await secretDal.findByFolderId(folder.id); + + const decryptedSec = secrets.reduce>((prev, secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: secretEncKey + }); + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: secretEncKey + }); + + // eslint-disable-next-line + prev[secretKey] = secretValue; + return prev; + }, {}); + + fetchCache[uniqKey] = decryptedSec; + + return fetchCache[uniqKey][secRefKey]; + }; + }; + + const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g; + const recursivelyExpandSecret = async ( + expandedSec: Record, + interpolatedSec: Record, + fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise, + recursionChainBreaker: Record, + key: string + ) => { + if (expandedSec?.[key] !== undefined) { + return expandedSec[key]; + } + if (recursionChainBreaker?.[key]) { + return ""; + } + // eslint-disable-next-line + recursionChainBreaker[key] = true; + + let interpolatedValue = interpolatedSec[key]; + if (!interpolatedValue) { + // eslint-disable-next-line no-console + console.error(`Couldn't find referenced value - ${key}`); + return ""; + } + + const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG); + if (refs) { + for (const interpolationSyntax of refs) { + const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); + const entities = interpolationKey.trim().split("."); + + if (entities.length === 1) { + const val = await recursivelyExpandSecret( + expandedSec, + interpolatedSec, + fetchCrossEnv, + recursionChainBreaker, + interpolationKey + ); + if (val) { + interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); + } + // eslint-disable-next-line + continue; + } + + if (entities.length > 1) { + const secRefEnv = entities[0]; + const secRefPath = entities.slice(1, entities.length - 1); + const secRefKey = entities[entities.length - 1]; + + const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey); + if (val) { + interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); + } + } + } + } + + // eslint-disable-next-line + expandedSec[key] = interpolatedValue; + return interpolatedValue; + }; + + // used to convert multi line ones to quotes ones with \n + const formatMultiValueEnv = (val?: string) => { + if (!val) return ""; + if (!val.match("\n")) return val; + return `"${val.replace(/\n/g, "\\n")}"`; + }; + + const expandSecrets = async ( + secrets: Record + ) => { + const expandedSec: Record = {}; + const interpolatedSec: Record = {}; + + const crossSecEnvFetch = fetchSecretsCrossEnv(); + + Object.keys(secrets).forEach((key) => { + if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) { + interpolatedSec[key] = secrets[key].value; + } else { + expandedSec[key] = secrets[key].value; + } + }); + + for (const key of Object.keys(secrets)) { + if (expandedSec?.[key]) { + // should not do multi line encoding if user has set it to skip + // eslint-disable-next-line + secrets[key].value = secrets[key].skipMultilineEncoding + ? expandedSec[key] + : formatMultiValueEnv(expandedSec[key]); + // eslint-disable-next-line + continue; + } + + // this is to avoid recursion loop. So the graph should be direct graph rather than cyclic + // so for any recursion building if there is an entity two times same key meaning it will be looped + const recursionChainBreaker: Record = {}; + const expandedVal = await recursivelyExpandSecret( + expandedSec, + interpolatedSec, + crossSecEnvFetch, + recursionChainBreaker, + key + ); + + // eslint-disable-next-line + secrets[key].value = secrets[key].skipMultilineEncoding + ? expandedVal + : formatMultiValueEnv(expandedVal); + } + + return secrets; + }; + return expandSecrets; +}; diff --git a/backend-pg/src/services/secret/secret-queue.ts b/backend-pg/src/services/secret/secret-queue.ts new file mode 100644 index 000000000..06ba0c3fb --- /dev/null +++ b/backend-pg/src/services/secret/secret-queue.ts @@ -0,0 +1,226 @@ +/* eslint-disable no-await-in-loop */ +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { isSamePath } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { TIntegrationDalFactory } from "../integration/integration-dal"; +import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; +import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret"; +import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; +import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretImportDalFactory } from "../secret-import/secret-import-dal"; +import { TSecretImportServiceFactory } from "../secret-import/secret-import-service"; +import { TWebhookServiceFactory } from "../webhook/webhook-service"; +import { TSecretDalFactory } from "./secret-dal"; +import { interpolateSecrets } from "./secret-fns"; + +export type TSecretQueueFactory = ReturnType; + +type TSecretQueueFactoryDep = { + queueService: TQueueServiceFactory; + webhookService: Pick; + integrationDal: Pick; + projectBotService: Pick; + integrationAuthService: Pick; + folderDal: Pick; + secretDal: Pick; + secretImportDal: Pick; + secretImportService: Pick; +}; + +export type TGetSecrets = { + secretPath: string; + projectId: string; + environment: string; +}; + +export const secretQueueFactory = ({ + queueService, + webhookService, + integrationDal, + projectBotService, + integrationAuthService, + secretDal, + secretImportDal, + secretImportService, + folderDal +}: TSecretQueueFactoryDep) => { + const syncSecrets = async (dto: TGetSecrets) => { + queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, { + jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`, + removeOnFail: { count: 5 }, + removeOnComplete: true, + delay: 1000, + attempts: 5, + backoff: { + type: "exponential", + delay: 3000 + } + }); + + queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { + attempts: 5, + delay: 1000, + backoff: { + type: "exponential", + delay: 3000 + }, + removeOnComplete: true, + removeOnFail: { + count: 5 // keep the most recent jobs + } + }); + }; + + const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => { + const secrets = await secretDal.findByFolderId(dto.folderId); + if (!secrets.length) return {}; + + // get imported secrets + const secretImport = await secretImportDal.find({ folderId: dto.folderId }); + const importedSecrets = await secretImportService.fnSecretsFromImports(secretImport); + const content: Record< + string, + { value: string; comment?: string; skipMultilineEncoding?: boolean } + > = {}; + + importedSecrets.forEach(({ secrets: secs }) => { + secs.forEach((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key + }); + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + content[secretKey] = { value: secretValue }; + content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key + }); + content[secretKey].comment = commentValue; + } + }); + }); + console.log(secrets.filter(({ type }) => type === "personal")); + secrets.forEach((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }); + + content[secretKey] = { value: secretValue }; + + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key + }); + content[secretKey].comment = commentValue; + } + + content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + }); + const expandSecrets = interpolateSecrets({ + projectId: dto.projectId, + secretEncKey: key, + folderDal, + secretDal + }); + await expandSecrets(content); + return content; + }; + + queueService.start(QueueName.IntegrationSync, async (job) => { + logger.info("Secret integration sync started", job.data, job.id); + const { environment, projectId, secretPath } = job.data; + const folder = await folderDal.findBySecretPath(projectId, environment, secretPath); + if (!folder) { + logger.error("Secret path not found"); + return; + } + + const integrations = await integrationDal.findByProjectIdV2(projectId, environment); + const toBeSyncedIntegrations = integrations.filter( + ({ secretPath: integrationSecPath, isActive }) => + isActive && isSamePath(secretPath, integrationSecPath) + ); + + for (const integration of toBeSyncedIntegrations) { + const integrationAuth = { + ...integration.integrationAuth, + createdAt: new Date(), + updatedAt: new Date(), + projectId: integration.projectId + }; + + const botKey = await projectBotService.getBotKey(projectId); + const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken( + integrationAuth, + botKey + ); + const secrets = await getIntegrationSecrets( + { environment, projectId, secretPath, folderId: folder.id }, + botKey + ); + const suffixedSecrets: typeof secrets = {}; + const metadata = integration.metadata as Record; + if (metadata) { + Object.keys(secrets).forEach((key) => { + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + const newKey = prefix + key + suffix; + suffixedSecrets[newKey] = secrets[key]; + }); + } + + await syncIntegrationSecrets({ + integration, + integrationAuth, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, + accessId: accessId as string, + accessToken, + appendices: { + prefix: metadata?.secretPrefix || "", + suffix: metadata?.secretSuffix || "" + } + }); + } + + logger.info("Secret integration sync ended", job.id); + }); + + queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => { + logger.error("Failed to sync integration", job?.data, err); + }); + + queueService.start(QueueName.SecretWebhook, async (job) => { + logger.info("Secret webhook job started", job.data, job.id); + await webhookService.fnTriggerWebhook(job.data); + logger.info("Secret webhook job ended", job.id); + }); + + return { syncSecrets }; +}; diff --git a/backend-pg/src/services/secret/secret-service.ts b/backend-pg/src/services/secret/secret-service.ts index cde6aea52..087248f13 100644 --- a/backend-pg/src/services/secret/secret-service.ts +++ b/backend-pg/src/services/secret/secret-service.ts @@ -23,6 +23,7 @@ import { TSecretFolderDalFactory } from "../secret-folder/secret-folder-dal"; import { TSecretTagDalFactory } from "../secret-tag/secret-tag-dal"; import { TSecretBlindIndexDalFactory } from "./secret-blind-index-dal"; import { TSecretDalFactory } from "./secret-dal"; +import { TSecretQueueFactory } from "./secret-queue"; import { TCreateBulkSecretDTO, TCreateSecretDTO, @@ -49,6 +50,7 @@ type TSecretServiceFactoryDep = { secretBlindIndexDal: TSecretBlindIndexDalFactory; permissionService: Pick; snapshotService: Pick; + secretQueueService: Pick; }; export type TSecretServiceFactory = ReturnType; @@ -77,7 +79,8 @@ export const secretServiceFactory = ({ folderDal, secretBlindIndexDal, permissionService, - snapshotService + snapshotService, + secretQueueService }: TSecretServiceFactoryDep) => { // utility function to get secret blind index data const interalGenSecBlindIndexByName = async (projectId: string, secretName: string) => { @@ -329,6 +332,7 @@ export const secretServiceFactory = ({ ); await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot return { ...secret[0], tags }; }; @@ -421,7 +425,7 @@ export const secretServiceFactory = ({ ); await snapshotService.performSnapshot(folderId); - + await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot return updatedSecret[0]; }; @@ -475,6 +479,7 @@ export const secretServiceFactory = ({ ); await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); // TODO(akhilmhdh-pg): licence check, posthog service and snapshot return deletedSecret[0]; @@ -576,6 +581,8 @@ export const secretServiceFactory = ({ ); await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); + return newSecrets; }; @@ -649,6 +656,8 @@ export const secretServiceFactory = ({ ); await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); + return secrets; }; @@ -695,6 +704,8 @@ export const secretServiceFactory = ({ ); await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ secretPath: path, projectId, environment }); + return secretsDeleted; }; diff --git a/backend-pg/src/services/webhook/webhook-dal.ts b/backend-pg/src/services/webhook/webhook-dal.ts index e5afceabd..138a1da18 100644 --- a/backend-pg/src/services/webhook/webhook-dal.ts +++ b/backend-pg/src/services/webhook/webhook-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TWebhooks } from "@app/db/schemas"; +import { TableName, TWebhooks, TWebhooksUpdate } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -101,5 +101,17 @@ export const webhookDalFactory = (db: TDbClient) => { } }; - return { ...webhookOrm, findById, findOne, find, findAllWebhooks }; + const bulkUpdate = async (data: Array, tx?: Knex) => { + try { + const queries = data.map(({ id, ...el }) => + (tx || db)(TableName.Webhook).where({ id }).update(el) + ); + const docs = await Promise.all(queries); + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + return { ...webhookOrm, findById, findOne, find, findAllWebhooks, bulkUpdate }; }; diff --git a/backend-pg/src/services/webhook/webhook-fns.ts b/backend-pg/src/services/webhook/webhook-fns.ts index 65a92f4f2..96b8e6e0a 100644 --- a/backend-pg/src/services/webhook/webhook-fns.ts +++ b/backend-pg/src/services/webhook/webhook-fns.ts @@ -5,6 +5,7 @@ import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { decryptSymmetric, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; export const triggerWebhookRequest = async ( { url, encryptedSecretKey, iv, tag, keyEncoding }: TWebhooks, data: Record @@ -39,10 +40,14 @@ export const triggerWebhookRequest = async ( .createHmac("sha256", secretKey) .update(JSON.stringify(payload)) .digest("hex"); - headers["x-infisical-signature"] = `t=${data.timestamp};${webhookSign}`; + headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`; } } - const req = await request.post(url, payload, { headers }); + const req = await request.post(url, payload, { + headers, + timeout: WEBHOOK_TRIGGER_TIMEOUT, + signal: AbortSignal.timeout(WEBHOOK_TRIGGER_TIMEOUT) + }); return req; }; diff --git a/backend-pg/src/services/webhook/webhook-service.ts b/backend-pg/src/services/webhook/webhook-service.ts index 7fb916c32..81c8ca127 100644 --- a/backend-pg/src/services/webhook/webhook-service.ts +++ b/backend-pg/src/services/webhook/webhook-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import picomatch from "picomatch"; import { SecretEncryptionAlgo, SecretKeyEncoding, TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -16,6 +17,7 @@ import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; import { TCreateWebhookDTO, TDeleteWebhookDTO, + TFnTriggerWebhookDTO, TListWebhookDTO, TTestWebhookDTO, TUpdateWebhookDTO @@ -83,7 +85,7 @@ export const webhookServiceFactory = ({ const webhook = await webhookDal.create(insertDoc); // TODO(akhilmhdh-pg): add audit log - return { ...webhook,projectId, environment: env }; + return { ...webhook, projectId, environment: env }; }; const updateWebhook = async ({ actorId, actor, id, isDisabled }: TUpdateWebhookDTO) => { @@ -101,7 +103,7 @@ export const webhookServiceFactory = ({ ); const updatedWebhook = await webhookDal.updateById(id, { isDisabled }); - return { ...webhook,...updatedWebhook }; + return { ...webhook, ...updatedWebhook }; }; const deleteWebhook = async ({ id, actor, actorId }: TDeleteWebhookDTO) => { @@ -119,7 +121,7 @@ export const webhookServiceFactory = ({ ); const deletedWebhook = await webhookDal.deleteById(id); - return { ...webhook,...deletedWebhook }; + return { ...webhook, ...deletedWebhook }; }; const testWebhook = async ({ id, actor, actorId }: TTestWebhookDTO) => { @@ -150,7 +152,7 @@ export const webhookServiceFactory = ({ lastStatus: isSuccess ? "success" : "failed", lastRunErrorMessage: isSuccess ? null : webhookError }); - return {...webhook,...updatedWebhook} + return { ...webhook, ...updatedWebhook }; }; const listWebhooks = async ({ @@ -169,11 +171,63 @@ export const webhookServiceFactory = ({ return webhookDal.findAllWebhooks(projectId, environment, secretPath); }; + // this is reusable function + // used in secret queue to trigger webhook and update status when secrets changes + const fnTriggerWebhook = async ({ environment, secretPath, projectId }: TFnTriggerWebhookDTO) => { + const webhooks = await webhookDal.findAllWebhooks(projectId, environment); + const toBeTriggeredHooks = webhooks.filter( + ({ secretPath: hookSecretPath, isDisabled }) => + !isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false }) + ); + if (!toBeTriggeredHooks.length) return; + const webhooksTriggered = await Promise.allSettled( + toBeTriggeredHooks.map((hook) => + triggerWebhookRequest( + hook, + getWebhookPayload("secrets.modified", projectId, environment, secretPath) + ) + ) + ); + // filter hooks by status + const successWebhooks = webhooksTriggered + .filter(({ status }) => status === "fulfilled") + .map((_, i) => toBeTriggeredHooks[i].id); + const failedWebhooks = webhooksTriggered + .filter(({ status }) => status === "rejected") + .map((data, i) => ({ + id: toBeTriggeredHooks[i].id, + error: data.status === "rejected" && data.reason.message + })); + + await webhookDal.transaction(async (tx) => { + const env = await projectEnvDal.findOne({ projectId, slug: environment }, tx); + if (!env) throw new BadRequestError({ message: "Env not found" }); + if (successWebhooks.length) { + await webhookDal.update( + { envId: env.id, $in: { id: successWebhooks } }, + { lastStatus: "success", lastRunErrorMessage: null }, + tx + ); + } + if (failedWebhooks.length) { + await webhookDal.bulkUpdate( + failedWebhooks.map(({ id, error }) => ({ + id, + lastRunErrorMessage: error, + lastStatus: "failed" + })), + tx + ); + } + }); + }; + return { createWebhook, deleteWebhook, listWebhooks, updateWebhook, - testWebhook + testWebhook, + fnTriggerWebhook }; }; diff --git a/backend-pg/src/services/webhook/webhook-types.ts b/backend-pg/src/services/webhook/webhook-types.ts index 7a6e92c80..ff8feded7 100644 --- a/backend-pg/src/services/webhook/webhook-types.ts +++ b/backend-pg/src/services/webhook/webhook-types.ts @@ -24,3 +24,9 @@ export type TListWebhookDTO = { environment?: string; secretPath?: string; } & TProjectPermission; + +export type TFnTriggerWebhookDTO = { + projectId: string; + secretPath: string; + environment: string; +};