diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index 486ff12b2..9dc767e31 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -65,6 +65,15 @@ jobs: INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} DD_GIT_REPOSITORY_URL=${{ github.server_url }}/${{ github.repository }} DD_GIT_COMMIT_SHA=${{ github.sha }} + - name: Snyk to check Docker image for vulnerabilities + continue-on-error: true + uses: snyk/actions/docker@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + image: infisical/infisical:${{ steps.extract_version.outputs.version }} + command: monitor + args: --file=Dockerfile.standalone-infisical --project-name="infisical-core-docker-image" infisical-fips-standalone: name: Build infisical standalone image postgres @@ -141,4 +150,4 @@ jobs: echo "Successfully created tag $TAG_NAME" fi env: - GH_TOKEN: ${{ secrets.OMNIBUS_RELEASE_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.OMNIBUS_RELEASE_TOKEN }} diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index 974ce4a42..1c03f752a 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -3,7 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG CAPTCHA_SITE_KEY=captcha-site-key -FROM node:20-slim AS base +FROM node:20.19.5-trixie-slim AS base + +# Fixes NPM vulnerability: https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230 +RUN npm install -g npm@11 FROM base AS frontend-dependencies WORKDIR /app @@ -155,7 +158,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.41.89 \ + && apt-get update && apt-get install -y infisical=0.42.6 \ && rm -rf /var/lib/apt/lists/* RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 9ca5e1dea..bea94d6b6 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -3,7 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG CAPTCHA_SITE_KEY=captcha-site-key -FROM node:20-slim AS base +FROM node:20.19.5-trixie-slim AS base + +# Fixes NPM vulnerability: https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230 +RUN npm install -g npm@11 FROM base AS frontend-dependencies @@ -139,7 +142,7 @@ RUN apt-get update && apt-get install -y \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.41.89 \ + && apt-get update && apt-get install -y infisical=0.42.6 \ && rm -rf /var/lib/apt/lists/* WORKDIR / diff --git a/backend/Dockerfile b/backend/Dockerfile index bca974f26..fa3d0e509 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM node:20-slim AS build +FROM node:20.19.5-trixie-slim AS build WORKDIR /app @@ -26,7 +26,7 @@ COPY . . RUN npm run build # Production stage -FROM node:20-slim +FROM node:20.19.5-trixie-slim WORKDIR /app ENV npm_config_cache /home/node/.npm diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index de5648797..5e17cf2bb 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:20-slim +FROM node:20.19.5-trixie-slim # ? Setup a test SoftHSM module. In production a real HSM is used. diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index b954ccd50..db5107985 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -1,4 +1,4 @@ -FROM node:20-slim +FROM node:20.19.5-trixie-slim # ? Setup a test SoftHSM module. In production a real HSM is used. diff --git a/backend/package-lock.json b/backend/package-lock.json index db4e9e7dd..adda83043 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,7 @@ "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^9.1.1", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", @@ -63,7 +63,7 @@ "ajv": "^8.12.0", "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "axios-ntlm": "^1.4.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", @@ -74,7 +74,7 @@ "cron": "^3.1.7", "dd-trace": "^5.40.0", "dotenv": "^16.4.1", - "fastify": "^4.28.1", + "fastify": "^4.29.1", "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", @@ -141,6 +141,7 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", + "@react-email/preview-server": "^4.3.0", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -176,7 +177,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "4.0.7", + "react-email": "^4.3.0", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -196,6 +197,19 @@ "node": ">=0.10.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -5150,30 +5164,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -5221,16 +5237,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -5238,10 +5254,11 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -5286,14 +5303,15 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -5447,6 +5465,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-hoist-variables": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", @@ -5473,29 +5501,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -5618,10 +5646,11 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -5642,26 +5671,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -6998,14 +7028,14 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -7013,19 +7043,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" @@ -7048,15 +7078,6 @@ } } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/traverse/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7064,9 +7085,9 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7329,12 +7350,13 @@ "integrity": "sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg==" }, "node_modules/@elastic/elasticsearch": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz", - "integrity": "sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-9.1.1.tgz", + "integrity": "sha512-s/JZtHZjtbAYC2gdSzm4LLOSReR724e7cf7ZauIAZlGvAyMgZPZCJpq7xHazSy4rZZhule4ubMs4vepBgWvKQA==", "license": "Apache-2.0", "dependencies": { - "@elastic/transport": "^8.7.0", + "@elastic/transport": "^9.0.1", + "apache-arrow": "18.x - 20.x", "tslib": "^2.4.0" }, "engines": { @@ -7342,30 +7364,55 @@ } }, "node_modules/@elastic/transport": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.7.1.tgz", - "integrity": "sha512-2eeMVkz57Ayxv+UAZkIKzzrUu7nm96jr3+N3kLfbBqALYe2jwDpLr9pR0jc/x9HyJKAM909YGaNlHFDZeb0+Mw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-9.2.0.tgz", + "integrity": "sha512-2HpxEX9eQE/viokiKHqRa1n3RaFqNKoOU5gc7AOJ4ahG9xZbim+Z3OdBwshW9aKuFeIn1WPtZxSrfghZ0UJFtg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "1.x", - "debug": "^4.3.4", - "hpagent": "^1.0.0", + "@opentelemetry/core": "2.x", + "debug": "^4.4.1", + "hpagent": "^1.2.0", "ms": "^2.1.3", - "secure-json-parse": "^2.4.0", - "tslib": "^2.4.0", - "undici": "^6.12.0" + "secure-json-parse": "^4.0.0", + "tslib": "^2.8.1", + "undici": "^7.16.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/@elastic/transport/node_modules/@opentelemetry/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", + "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@elastic/transport/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", + "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" } }, "node_modules/@elastic/transport/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7376,16 +7423,26 @@ } } }, - "node_modules/@elastic/transport/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" + "node_modules/@elastic/transport/node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "dev": true, "license": "MIT", "optional": true, @@ -7732,6 +7789,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", @@ -8234,6 +8308,48 @@ "p-limit": "^3.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@gitbeaker/core": { "version": "42.5.0", "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz", @@ -8535,10 +8651,20 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", "cpu": [ "arm64" ], @@ -8555,13 +8681,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", "cpu": [ "x64" ], @@ -8578,13 +8704,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.3" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", "cpu": [ "arm64" ], @@ -8599,9 +8725,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", "cpu": [ "x64" ], @@ -8616,9 +8742,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", "cpu": [ "arm" ], @@ -8633,9 +8759,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", "cpu": [ "arm64" ], @@ -8649,10 +8775,27 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", "cpu": [ "s390x" ], @@ -8667,9 +8810,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", "cpu": [ "x64" ], @@ -8684,9 +8827,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", "cpu": [ "arm64" ], @@ -8701,9 +8844,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", "cpu": [ "x64" ], @@ -8718,9 +8861,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", "cpu": [ "arm" ], @@ -8737,13 +8880,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", "cpu": [ "arm64" ], @@ -8760,13 +8903,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", "cpu": [ "s390x" ], @@ -8783,13 +8949,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", "cpu": [ "x64" ], @@ -8806,13 +8972,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", "cpu": [ "arm64" ], @@ -8829,13 +8995,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", "cpu": [ "x64" ], @@ -8852,13 +9018,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", "cpu": [ "wasm32" ], @@ -8866,7 +9032,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.5.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -8875,10 +9041,30 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", "cpu": [ "ia32" ], @@ -8896,9 +9082,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", "cpu": [ "x64" ], @@ -9026,6 +9212,29 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -9084,17 +9293,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { @@ -9116,20 +9322,34 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -9232,6 +9452,26 @@ "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.3.tgz", + "integrity": "sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.42.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.42.0.tgz", + "integrity": "sha512-Zr2LCaOAoPCsdAQgeLyCSiQ1+xrAJtRCyuEYDj0qR5heUwpc+Pxbb88JyTVumcXFfKOBMOMmrlsTScLz2mrvQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -9453,16 +9693,16 @@ ] }, "node_modules/@next/env": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", - "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", + "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", "dev": true, "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", - "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", "cpu": [ "arm64" ], @@ -9477,9 +9717,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", - "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", "cpu": [ "x64" ], @@ -9494,9 +9734,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", - "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", "cpu": [ "arm64" ], @@ -9511,9 +9751,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", - "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", "cpu": [ "arm64" ], @@ -9528,9 +9768,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", - "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", + "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", "cpu": [ "x64" ], @@ -9545,9 +9785,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", - "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", "cpu": [ "x64" ], @@ -9562,9 +9802,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", - "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", "cpu": [ "arm64" ], @@ -9579,9 +9819,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", - "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", "cpu": [ "x64" ], @@ -11100,6 +11340,787 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@react-email/body": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz", @@ -11314,6 +12335,849 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-email/preview-server": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.0.tgz", + "integrity": "sha512-cUaSrxezCzdg2hF6PzIxVrtagLdw3z3ovHeB3y2RDkmDZpp7EeIoNyJm22Ch2S0uAqTZNAgqu67aroLn3mFC1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.26.10", + "@babel/parser": "7.27.0", + "@babel/traverse": "7.27.0", + "@lottiefiles/dotlottie-react": "0.13.3", + "@radix-ui/colors": "3.0.0", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@types/node": "22.14.1", + "@types/normalize-path": "3.0.2", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@types/webpack": "5.28.5", + "autoprefixer": "10.4.21", + "clsx": "2.1.1", + "esbuild": "0.25.10", + "framer-motion": "12.23.22", + "json5": "2.2.3", + "log-symbols": "4.1.0", + "module-punycode": "npm:punycode@2.3.1", + "next": "15.5.2", + "node-html-parser": "7.0.1", + "ora": "5.4.1", + "pretty-bytes": "6.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "sharp": "0.34.4", + "socket.io-client": "4.8.1", + "sonner": "2.0.3", + "source-map-js": "1.2.1", + "spamc": "0.0.5", + "stacktrace-parser": "0.1.11", + "tailwind-merge": "3.2.0", + "tailwindcss": "3.4.0", + "use-debounce": "10.0.4", + "zod": "3.24.3" + } + }, + "node_modules/@react-email/preview-server/node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@react-email/preview-server/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@react-email/preview-server/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@react-email/preview-server/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@react-email/preview-server/node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/@react-email/preview-server/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@react-email/preview-server/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-email/preview-server/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-email/preview-server/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-email/preview-server/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@react-email/preview-server/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-email/preview-server/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@react-email/render": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", @@ -12996,18 +14860,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -13088,6 +14944,18 @@ "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "license": "MIT" }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "license": "MIT" + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -13115,6 +14983,28 @@ "@types/ms": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -13284,6 +15174,13 @@ "@types/node": "*" } }, + "node_modules/@types/normalize-path": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.2.tgz", + "integrity": "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/oauth": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.4.tgz", @@ -13427,6 +15324,13 @@ "pkcs11js": "*" } }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -13454,6 +15358,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, "node_modules/@types/readable-stream": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", @@ -13599,6 +15513,18 @@ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", @@ -14073,6 +15999,167 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@xmldom/is-dom-node": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz", @@ -14091,6 +16178,20 @@ "node": ">=10.0.0" } }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@yao-pkg/pkg": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.12.0.tgz", @@ -14365,9 +16466,10 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -14383,6 +16485,19 @@ "acorn": "^8" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -14490,6 +16605,19 @@ } } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -14546,6 +16674,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/apache-arrow": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-20.0.0.tgz", + "integrity": "sha512-JUeK0jFRUd7rbmrhhzR3O2KXjLaZ4YYYFOptyUfxOsMIoZCPi6bZR58gVi/xi3HTBMPseXm9PXyQ2V916930pA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^25.1.24", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -14665,6 +16813,28 @@ "node": ">=0.8.0" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -14905,10 +17075,52 @@ "node": ">=8.0.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -15014,9 +17226,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -15161,6 +17373,16 @@ "node": ">=6.0.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz", + "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -15311,6 +17533,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/botbuilder": { "version": "4.23.2", "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz", @@ -15590,9 +17819,9 @@ ] }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -15608,11 +17837,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -15662,6 +17893,13 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bullmq": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz", @@ -15707,18 +17945,6 @@ "esbuild": ">=0.17" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -15855,16 +18081,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -15885,6 +18110,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -15902,10 +18143,20 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001639", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", - "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "version": "1.0.30001748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", + "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", "dev": true, "funding": [ { @@ -15920,7 +18171,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/cassandra-driver": { "version": "4.7.2", @@ -15965,6 +18217,58 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk-template/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -16012,13 +18316,37 @@ "node": ">=10" } }, - "node_modules/cipher-base": { + "node_modules/chrome-trace-event": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.5.tgz", + "integrity": "sha512-xq7ICKB4TMHUx7Tz1L9O2SGKOhYMOTR32oir45Bq28/AQTpHogKgHcoYFSdRbMtddl+ozNXfXY9jWcgYKmde0w==", + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" } }, "node_modules/cjs-module-lexer": { @@ -16145,6 +18473,16 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -16239,6 +18577,44 @@ "node": ">= 0.8" } }, + "node_modules/command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + }, + "engines": { + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -16252,6 +18628,13 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -16269,6 +18652,16 @@ "express-session": ">=1" } }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -16414,6 +18807,49 @@ "resolved": "https://registry.npmjs.org/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz", "integrity": "sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==" }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", @@ -16816,9 +19252,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -16832,12 +19268,26 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/dev-null": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", "license": "MIT" }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -16868,6 +19318,13 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -17028,10 +19485,11 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.816", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", - "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==", - "dev": true + "version": "1.5.232", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz", + "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "10.3.0", @@ -17092,6 +19550,60 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -17143,10 +19655,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -17259,6 +19772,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -17354,9 +19874,10 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -18080,6 +20601,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -18225,9 +20753,9 @@ } }, "node_modules/fastify": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", - "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", "funding": [ { "type": "github", @@ -18443,6 +20971,23 @@ "node": ">=14" } }, + "node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -18508,6 +21053,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -18540,19 +21091,27 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -18602,6 +21161,48 @@ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.23.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", + "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.21", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -18920,6 +21521,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -18952,6 +21566,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -19062,6 +21686,13 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -19456,6 +22087,16 @@ "node": ">=0.10.0" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/helmet": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", @@ -20304,11 +22945,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -20354,8 +22996,7 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isexe": { "version": "2.0.0", @@ -20421,6 +23062,57 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -20600,6 +23292,14 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -20611,6 +23311,13 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -20846,6 +23553,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/knex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/knex/-/knex-3.0.1.tgz", @@ -21143,6 +23860,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -21568,6 +24295,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -21797,6 +24537,17 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, + "node_modules/module-punycode": { + "name": "punycode", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -21903,6 +24654,23 @@ "node": ">=16" } }, + "node_modules/motion-dom": { + "version": "12.23.21", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", + "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -22109,16 +24877,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", - "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", + "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/env": "15.2.4", - "@swc/counter": "0.1.3", + "@next/env": "15.5.2", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -22130,19 +24896,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.4", - "@next/swc-darwin-x64": "15.2.4", - "@next/swc-linux-arm64-gnu": "15.2.4", - "@next/swc-linux-arm64-musl": "15.2.4", - "@next/swc-linux-x64-gnu": "15.2.4", - "@next/swc-linux-x64-musl": "15.2.4", - "@next/swc-win32-arm64-msvc": "15.2.4", - "@next/swc-win32-x64-msvc": "15.2.4", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.2", + "@next/swc-darwin-x64": "15.5.2", + "@next/swc-linux-arm64-gnu": "15.5.2", + "@next/swc-linux-arm64-musl": "15.5.2", + "@next/swc-linux-x64-gnu": "15.5.2", + "@next/swc-linux-x64-musl": "15.5.2", + "@next/swc-win32-arm64-msvc": "15.5.2", + "@next/swc-win32-x64-msvc": "15.5.2", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -22403,11 +25169,23 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/node-html-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", + "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" }, "node_modules/nodemailer": { "version": "6.9.9", @@ -22491,6 +25269,16 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -22529,11 +25317,63 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.18", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz", "integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==" }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/nypm/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/oauth": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", @@ -25580,6 +28420,15 @@ "node": ">=12" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -25609,6 +28458,50 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, "node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -25644,6 +28537,53 @@ } } }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -25772,6 +28712,19 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -25798,6 +28751,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -26047,6 +29014,20 @@ "node": ">=6" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -26328,47 +29309,36 @@ } }, "node_modules/react-email": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.7.tgz", - "integrity": "sha512-XCXlfZLKv9gHd/ZwUEhCpRGc/FJLZGYczeuG1kVR/be2PlkwEB4gjX9ARBbRFv86ncbtpOu/wI6jD6kadRyAKw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.0.tgz", + "integrity": "sha512-XFHCSfhdlO7k5q2TYGwC0HsVh5Yn13YaOdahuJEUEOfOJKHEpSP4PKg7R/RiKFoK9cDvzunhY+58pXxz0vE2zA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "7.24.5", - "@babel/traverse": "7.25.6", - "chalk": "4.1.2", - "chokidar": "4.0.3", - "commander": "11.1.0", - "debounce": "2.0.0", - "esbuild": "0.25.0", - "glob": "10.3.4", - "log-symbols": "4.1.0", - "mime-types": "2.1.35", - "next": "15.2.4", - "normalize-path": "3.0.0", - "ora": "5.4.1", - "socket.io": "4.8.1" + "@babel/parser": "^7.27.0", + "@babel/traverse": "^7.27.0", + "chokidar": "^4.0.3", + "commander": "^13.0.0", + "debounce": "^2.0.0", + "esbuild": "^0.25.0", + "glob": "^11.0.0", + "jiti": "2.4.2", + "log-symbols": "^7.0.0", + "mime-types": "^3.0.0", + "normalize-path": "^3.0.0", + "nypm": "0.6.0", + "ora": "^8.0.0", + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tsconfig-paths": "4.2.0" }, "bin": { - "email": "dist/cli/index.js" + "email": "dist/index.js" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/react-email/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -26760,80 +29730,6 @@ "node": ">=18" } }, - "node_modules/react-email/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-email/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/react-email/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/react-email/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/react-email/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/react-email/node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -26851,26 +29747,29 @@ } }, "node_modules/react-email/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-email/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/react-email/node_modules/esbuild": { @@ -26915,131 +29814,219 @@ } }, "node_modules/react-email/node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-email/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-email/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/react-email/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/react-email/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-email/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "node_modules/react-email/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/react-email/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/react-email/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react-email/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 6" + "node": ">= 0.6" + } + }, + "node_modules/react-email/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/react-email/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/react-email/node_modules/readdirp": { @@ -27057,43 +30044,64 @@ } }, "node_modules/react-email/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/react-email/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/supports-color": { + "node_modules/react-email/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-is": { @@ -27117,6 +30125,98 @@ "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -27698,6 +30798,26 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/scim-patch": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz", @@ -27744,9 +30864,9 @@ "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -27834,6 +30954,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -27896,29 +31026,36 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -27927,40 +31064,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/sharp/node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" } }, "node_modules/shebang-command": { @@ -28104,6 +31229,13 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/sjcl": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", @@ -28405,6 +31537,40 @@ } } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -28517,6 +31683,17 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -28535,6 +31712,23 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spamc": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/spamc/-/spamc-0.0.5.tgz", + "integrity": "sha512-jYXItuZuiWZyG9fIdvgTUbp2MNRuyhuSwvvhhpPJd4JK/9oSZxkD7zAj53GJtowSlXwCJzLg6sCKAoE9wXsKgg==", + "dev": true + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -28637,6 +31831,29 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -28738,15 +31955,6 @@ "node": ">=4.0.0" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -29060,13 +32268,130 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/tailwindcss/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -29253,6 +32578,78 @@ "node": ">= 6" } }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -29316,6 +32713,13 @@ "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", "dev": true }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinypool": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", @@ -29355,6 +32759,20 @@ "resolved": "https://registry.npmjs.org/tlhunter-sorted-set/-/tlhunter-sorted-set-0.1.0.tgz", "integrity": "sha512-eGYW4bjf1DtrHzUYxYfAcSytpOkA44zsr7G2n3PV7yOUR23vmkGe3LL4R+1jL9OsXtbsFOwe8XtbCrabeaEFnw==" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -29571,9 +32989,9 @@ } }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsup": { @@ -30262,14 +33680,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -30339,6 +33757,15 @@ "node": ">=14.17" } }, + "node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/ufo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", @@ -30396,12 +33823,12 @@ "dev": true }, "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -30502,9 +33929,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -30520,9 +33947,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -30572,6 +34000,64 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-debounce": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", + "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -31370,6 +34856,20 @@ "node": ">=18" } }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -31385,6 +34885,96 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -31450,15 +35040,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -31595,6 +35188,15 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -31813,6 +35415,15 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xpath": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", @@ -31939,6 +35550,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.24.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", diff --git a/backend/package.json b/backend/package.json index f06c1d69f..17be524f2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -79,12 +79,17 @@ "keywords": [], "author": "", "license": "ISC", + "overrides": { + "cipher-base": "1.0.5", + "sha.js": "2.4.12" + }, "devDependencies": { "@babel/cli": "^7.18.10", "@babel/core": "^7.18.10", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", + "@react-email/preview-server": "^4.3.0", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -120,7 +125,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "4.0.7", + "react-email": "^4.3.0", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -138,7 +143,7 @@ "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^9.1.1", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", @@ -185,7 +190,7 @@ "ajv": "^8.12.0", "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "axios-ntlm": "^1.4.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", @@ -196,7 +201,7 @@ "cron": "^3.1.7", "dd-trace": "^5.40.0", "dotenv": "^16.4.1", - "fastify": "^4.28.1", + "fastify": "^4.29.1", "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c814ab9d8..088d99ee9 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -28,6 +28,10 @@ import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; +import { TPamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service"; +import { TPamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service"; +import { TPamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service"; +import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types"; @@ -315,6 +319,10 @@ declare module "fastify" { identityAuthTemplate: TIdentityAuthTemplateServiceFactory; notification: TNotificationServiceFactory; offlineUsageReport: TOfflineUsageReportServiceFactory; + pamFolder: TPamFolderServiceFactory; + pamResource: TPamResourceServiceFactory; + pamAccount: TPamAccountServiceFactory; + pamSession: TPamSessionServiceFactory; upgradePath: TUpgradePathService; }; // this is exclusive use for middlewares in which we need to inject data diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index b6bde44bc..c4d45ca27 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -530,6 +530,10 @@ import { TMicrosoftTeamsIntegrationsInsert, TMicrosoftTeamsIntegrationsUpdate } from "@app/db/schemas/microsoft-teams-integrations"; +import { TPamAccounts, TPamAccountsInsert, TPamAccountsUpdate } from "@app/db/schemas/pam-accounts"; +import { TPamFolders, TPamFoldersInsert, TPamFoldersUpdate } from "@app/db/schemas/pam-folders"; +import { TPamResources, TPamResourcesInsert, TPamResourcesUpdate } from "@app/db/schemas/pam-resources"; +import { TPamSessions, TPamSessionsInsert, TPamSessionsUpdate } from "@app/db/schemas/pam-sessions"; import { TProjectMicrosoftTeamsConfigs, TProjectMicrosoftTeamsConfigsInsert, @@ -1308,5 +1312,9 @@ declare module "knex/types/tables" { TKeyValueStoreInsert, TKeyValueStoreUpdate >; + [TableName.PamFolder]: KnexOriginal.CompositeTableType; + [TableName.PamResource]: KnexOriginal.CompositeTableType; + [TableName.PamAccount]: KnexOriginal.CompositeTableType; + [TableName.PamSession]: KnexOriginal.CompositeTableType; } } diff --git a/backend/src/db/migrations/20250602155452_pit-projects-commits-initialization.ts b/backend/src/db/migrations/20250602155452_pit-projects-commits-initialization.ts index dd4034c57..f382db0d6 100644 --- a/backend/src/db/migrations/20250602155452_pit-projects-commits-initialization.ts +++ b/backend/src/db/migrations/20250602155452_pit-projects-commits-initialization.ts @@ -226,7 +226,7 @@ export async function up(knex: Knex): Promise { // Insert New Commits in batches of 9000 const newCommits = foldersCommitsList.map((folderCommit) => folderCommit.commit); const commitBatches = chunkArray(newCommits, 9000); - + let pendingDeepTreeCommitResources: TFolderCommits[] = []; let j = 0; for (const commitBatch of commitBatches) { j += 1; @@ -265,12 +265,14 @@ export async function up(knex: Knex): Promise { }); // Create folder commit changes + const currentBatchFolderIds = new Set(newCommitsInserted.map((commit) => commit.folderId)); // eslint-disable-next-line no-await-in-loop await knex.batchInsert( TableName.FolderCommitChanges, foldersCommitsList .map((folderCommit) => folderCommit.changes) .flat() + .filter((change) => currentBatchFolderIds.has(change.folderId)) .map((change) => ({ folderCommitId: newCommitsMap[change.folderId], changeType: change.changeType, @@ -289,6 +291,7 @@ export async function up(knex: Knex): Promise { foldersCommitsList .map((folderCommit) => folderCommit.changes) .flat() + .filter((change) => currentBatchFolderIds.has(change.folderId)) .map((change) => ({ folderCheckpointId: newCheckpointsMap[change.folderId], folderVersionId: change.folderVersionId, @@ -303,9 +306,11 @@ export async function up(knex: Knex): Promise { const newTreeCheckpoints = (await knex .batchInsert( TableName.FolderTreeCheckpoint, - Object.keys(rootFoldersMap).map((folderId) => ({ - folderCommitId: newCommitsMap[folderId] - })) + Object.keys(rootFoldersMap) + .filter((folderId) => currentBatchFolderIds.has(folderId)) + .map((folderId) => ({ + folderCommitId: newCommitsMap[folderId] + })) ) .returning("*")) as TFolderTreeCheckpoints[]; @@ -317,18 +322,31 @@ export async function up(knex: Knex): Promise { }); // Create Folder Tree Checkpoint Resources - // eslint-disable-next-line no-await-in-loop - await knex - .batchInsert( - TableName.FolderTreeCheckpointResources, - newCommitsInserted.map((folderCommit) => ({ - folderTreeCheckpointId: newTreeCheckpointsMap[folderCommit.envId], - folderId: folderCommit.folderId, - folderCommitId: folderCommit.id - })) - ) - .returning("*"); + const commitsToProcess = pendingDeepTreeCommitResources.concat(newCommitsInserted); + const unprocessableCommits: TFolderCommits[] = []; + const processableCommits = commitsToProcess.filter((folderCommit) => { + const isProcessable = newTreeCheckpointsMap[folderCommit.envId]; + if (!isProcessable) { + unprocessableCommits.push(folderCommit); + } + return isProcessable; + }); + if (processableCommits.length > 0) { + // eslint-disable-next-line no-await-in-loop + await knex + .batchInsert( + TableName.FolderTreeCheckpointResources, + processableCommits.map((folderCommit) => ({ + folderTreeCheckpointId: newTreeCheckpointsMap[folderCommit.envId], + folderId: folderCommit.folderId, + folderCommitId: folderCommit.id + })) + ) + .returning("*"); + } + + pendingDeepTreeCommitResources = unprocessableCommits; logger.info(`Finished inserting folder tree checkpoint resources - batch ${j} of ${commitBatches.length}`); } } diff --git a/backend/src/db/migrations/20250917052037_pam.ts b/backend/src/db/migrations/20250917052037_pam.ts new file mode 100644 index 000000000..031b407db --- /dev/null +++ b/backend/src/db/migrations/20250917052037_pam.ts @@ -0,0 +1,165 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + // PAM Folders + if (!(await knex.schema.hasTable(TableName.PamFolder))) { + await knex.schema.createTable(TableName.PamFolder, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.index("projectId"); + + t.uuid("parentId").nullable(); + t.foreign("parentId").references("id").inTable(TableName.PamFolder).onDelete("CASCADE"); + t.index("parentId"); + + t.string("name").notNullable(); + t.index("name"); + + // Enforce uniqueness for sub-folders + t.unique(["projectId", "parentId", "name"], { + indexName: "uidx_pam_folder_children_name", + predicate: knex.whereNotNull("parentId") + }); + + // Enforce uniqueness for root-level folders + t.unique(["projectId", "name"], { + indexName: "uidx_pam_folder_root_name", + predicate: knex.whereNull("parentId") + }); + + t.text("description").nullable(); + + t.timestamps(true, true, true); + }); + } + + // PAM Resources + if (!(await knex.schema.hasTable(TableName.PamResource))) { + await knex.schema.createTable(TableName.PamResource, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.index("projectId"); + + t.string("name").notNullable(); + t.index("name"); + + t.uuid("gatewayId").notNullable(); + t.foreign("gatewayId").references("id").inTable(TableName.GatewayV2); + t.index("gatewayId"); + + t.string("resourceType").notNullable(); + t.index("resourceType"); + + t.binary("encryptedConnectionDetails").notNullable(); + + t.timestamps(true, true, true); + }); + } + + // PAM Accounts + if (!(await knex.schema.hasTable(TableName.PamAccount))) { + await knex.schema.createTable(TableName.PamAccount, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.index("projectId"); + + t.uuid("folderId").nullable(); + t.foreign("folderId").references("id").inTable(TableName.PamFolder).onDelete("CASCADE"); + t.index("folderId"); + + t.uuid("resourceId").notNullable(); + t.foreign("resourceId").references("id").inTable(TableName.PamResource); + t.index("resourceId"); + + t.string("name").notNullable(); + t.index("name"); + + // Enforce uniqueness for folders + t.unique(["projectId", "folderId", "name"], { + indexName: "uidx_pam_account_children_name", + predicate: knex.whereNotNull("folderId") + }); + + // Enforce uniqueness for root-level + t.unique(["projectId", "name"], { + indexName: "uidx_pam_account_root_name", + predicate: knex.whereNull("folderId") + }); + + t.text("description").nullable(); + t.binary("encryptedCredentials").notNullable(); + + t.timestamps(true, true, true); + }); + } + + // PAM Sessions + if (!(await knex.schema.hasTable(TableName.PamSession))) { + await knex.schema.createTable(TableName.PamSession, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.index("projectId"); + + t.uuid("accountId").nullable(); + t.foreign("accountId").references("id").inTable(TableName.PamAccount).onDelete("SET NULL"); + t.index("accountId"); + + // To be used in the event of an account deletion + t.string("resourceType").notNullable(); + t.string("resourceName").notNullable(); + t.string("accountName").notNullable(); + + t.uuid("userId").nullable(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + t.index("userId"); + + // To be used in the event of user deletion + t.string("actorName").notNullable(); + t.string("actorEmail").notNullable(); + + t.string("actorIp").notNullable(); + t.string("actorUserAgent").notNullable(); + + t.string("status").notNullable(); + t.index("status"); + + t.binary("encryptedLogsBlob").nullable(); + + t.datetime("expiresAt").notNullable(); + + t.datetime("startedAt").nullable(); // Not when the row is created, but when the end-to-end connection between user and resource is established + t.datetime("endedAt").nullable(); + t.index(["startedAt", "endedAt"]); + + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.PamFolder); + await createOnUpdateTrigger(knex, TableName.PamResource); + await createOnUpdateTrigger(knex, TableName.PamAccount); + await createOnUpdateTrigger(knex, TableName.PamSession); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.PamSession); + await knex.schema.dropTableIfExists(TableName.PamAccount); + await knex.schema.dropTableIfExists(TableName.PamResource); + await knex.schema.dropTableIfExists(TableName.PamFolder); + + await dropOnUpdateTrigger(knex, TableName.PamSession); + await dropOnUpdateTrigger(knex, TableName.PamAccount); + await dropOnUpdateTrigger(knex, TableName.PamResource); + await dropOnUpdateTrigger(knex, TableName.PamFolder); +} diff --git a/backend/src/db/migrations/20250926000000_saml-configs-group-sync-fields.ts b/backend/src/db/migrations/20250926000000_saml-configs-group-sync-fields.ts new file mode 100644 index 000000000..4b637e809 --- /dev/null +++ b/backend/src/db/migrations/20250926000000_saml-configs-group-sync-fields.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasEnableGroupSyncCol = await knex.schema.hasColumn(TableName.SamlConfig, "enableGroupSync"); + + if (!hasEnableGroupSyncCol) { + await knex.schema.alterTable(TableName.SamlConfig, (tb) => { + tb.boolean("enableGroupSync").notNullable().defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEnableGroupSyncCol = await knex.schema.hasColumn(TableName.SamlConfig, "enableGroupSync"); + + if (hasEnableGroupSyncCol) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + t.dropColumn("enableGroupSync"); + }); + } +} diff --git a/backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts b/backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts new file mode 100644 index 000000000..b2bb10003 --- /dev/null +++ b/backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.GatewayV2, "encryptedPamSessionKey"))) { + await knex.schema.alterTable(TableName.GatewayV2, (t) => { + t.binary("encryptedPamSessionKey"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.GatewayV2, "encryptedPamSessionKey")) { + await knex.schema.alterTable(TableName.GatewayV2, (t) => { + t.dropColumn("encryptedPamSessionKey"); + }); + } +} diff --git a/backend/src/db/schemas/gateways-v2.ts b/backend/src/db/schemas/gateways-v2.ts index 6aff8a168..1362793f6 100644 --- a/backend/src/db/schemas/gateways-v2.ts +++ b/backend/src/db/schemas/gateways-v2.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const GatewaysV2Schema = z.object({ @@ -15,7 +17,8 @@ export const GatewaysV2Schema = z.object({ identityId: z.string().uuid(), relayId: z.string().uuid().nullable().optional(), name: z.string(), - heartbeat: z.date().nullable().optional() + heartbeat: z.date().nullable().optional(), + encryptedPamSessionKey: zodBuffer.nullable().optional() }); export type TGatewaysV2 = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 8cbaa00bb..f8ac885b4 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -83,6 +83,10 @@ export * from "./org-memberships"; export * from "./org-relay-config"; export * from "./org-roles"; export * from "./organizations"; +export * from "./pam-accounts"; +export * from "./pam-folders"; +export * from "./pam-resources"; +export * from "./pam-sessions"; export * from "./pki-alerts"; export * from "./pki-collection-items"; export * from "./pki-collections"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 385a328b6..09ecb367a 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -189,7 +189,13 @@ export enum TableName { Relay = "relays", GatewayV2 = "gateways_v2", - KeyValueStore = "key_value_store" + KeyValueStore = "key_value_store", + + // PAM + PamFolder = "pam_folders", + PamResource = "pam_resources", + PamAccount = "pam_accounts", + PamSession = "pam_sessions" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; @@ -281,7 +287,8 @@ export enum ProjectType { CertificateManager = "cert-manager", KMS = "kms", SSH = "ssh", - SecretScanning = "secret-scanning" + SecretScanning = "secret-scanning", + PAM = "pam" } export enum ActionProjectType { @@ -290,6 +297,7 @@ export enum ActionProjectType { KMS = ProjectType.KMS, SSH = ProjectType.SSH, SecretScanning = ProjectType.SecretScanning, + PAM = ProjectType.PAM, // project operations that happen on all types Any = "any" } diff --git a/backend/src/db/schemas/pam-accounts.ts b/backend/src/db/schemas/pam-accounts.ts new file mode 100644 index 000000000..5a9a45617 --- /dev/null +++ b/backend/src/db/schemas/pam-accounts.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const PamAccountsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + folderId: z.string().uuid().nullable().optional(), + resourceId: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + encryptedCredentials: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TPamAccounts = z.infer; +export type TPamAccountsInsert = Omit, TImmutableDBKeys>; +export type TPamAccountsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/pam-folders.ts b/backend/src/db/schemas/pam-folders.ts new file mode 100644 index 000000000..80243c1bc --- /dev/null +++ b/backend/src/db/schemas/pam-folders.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const PamFoldersSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + parentId: z.string().uuid().nullable().optional(), + name: z.string(), + description: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TPamFolders = z.infer; +export type TPamFoldersInsert = Omit, TImmutableDBKeys>; +export type TPamFoldersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/pam-resources.ts b/backend/src/db/schemas/pam-resources.ts new file mode 100644 index 000000000..d34017d0f --- /dev/null +++ b/backend/src/db/schemas/pam-resources.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const PamResourcesSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + name: z.string(), + gatewayId: z.string().uuid(), + resourceType: z.string(), + encryptedConnectionDetails: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TPamResources = z.infer; +export type TPamResourcesInsert = Omit, TImmutableDBKeys>; +export type TPamResourcesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/pam-sessions.ts b/backend/src/db/schemas/pam-sessions.ts new file mode 100644 index 000000000..12e4adfcf --- /dev/null +++ b/backend/src/db/schemas/pam-sessions.ts @@ -0,0 +1,35 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const PamSessionsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + accountId: z.string().uuid().nullable().optional(), + resourceType: z.string(), + resourceName: z.string(), + accountName: z.string(), + userId: z.string().uuid().nullable().optional(), + actorName: z.string(), + actorEmail: z.string(), + actorIp: z.string(), + actorUserAgent: z.string(), + status: z.string(), + encryptedLogsBlob: zodBuffer.nullable().optional(), + expiresAt: z.date(), + startedAt: z.date().nullable().optional(), + endedAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TPamSessions = z.infer; +export type TPamSessionsInsert = Omit, TImmutableDBKeys>; +export type TPamSessionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 350e84492..de85653c2 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -28,7 +28,8 @@ export const SamlConfigsSchema = z.object({ lastUsed: z.date().nullable().optional(), encryptedSamlEntryPoint: zodBuffer, encryptedSamlIssuer: zodBuffer, - encryptedSamlCertificate: zodBuffer + encryptedSamlCertificate: zodBuffer, + enableGroupSync: z.boolean().default(false) }); export type TSamlConfigs = z.infer; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 56d450df3..42392ba55 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -23,6 +23,12 @@ import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOidcRouter } from "./oidc-router"; import { registerOrgRoleRouter } from "./org-role-router"; +import { PAM_ACCOUNT_REGISTER_ROUTER_MAP } from "./pam-account-routers"; +import { registerPamAccountRouter } from "./pam-account-routers/pam-account-router"; +import { registerPamFolderRouter } from "./pam-folder-router"; +import { PAM_RESOURCE_REGISTER_ROUTER_MAP } from "./pam-resource-routers"; +import { registerPamResourceRouter } from "./pam-resource-routers/pam-resource-router"; +import { registerPamSessionRouter } from "./pam-session-router"; import { registerPITRouter } from "./pit-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; @@ -166,4 +172,40 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }, { prefix: "/kmip" } ); + + await server.register( + async (pamRouter) => { + await pamRouter.register(registerPamFolderRouter, { prefix: "/folders" }); + await pamRouter.register(registerPamSessionRouter, { prefix: "/sessions" }); + + await pamRouter.register( + async (pamAccountRouter) => { + await pamAccountRouter.register(registerPamAccountRouter); + + // Provider-specific endpoints + await Promise.all( + Object.entries(PAM_ACCOUNT_REGISTER_ROUTER_MAP).map(([provider, router]) => + pamAccountRouter.register(router, { prefix: `/${provider}` }) + ) + ); + }, + { prefix: "/accounts" } + ); + + await pamRouter.register( + async (pamResourceRouter) => { + await pamResourceRouter.register(registerPamResourceRouter); + + // Provider-specific endpoints + await Promise.all( + Object.entries(PAM_RESOURCE_REGISTER_ROUTER_MAP).map(([provider, router]) => + pamResourceRouter.register(router, { prefix: `/${provider}` }) + ) + ); + }, + { prefix: "/resources" } + ); + }, + { prefix: "/pam" } + ); }; diff --git a/backend/src/ee/routes/v1/pam-account-routers/index.ts b/backend/src/ee/routes/v1/pam-account-routers/index.ts new file mode 100644 index 000000000..568412c84 --- /dev/null +++ b/backend/src/ee/routes/v1/pam-account-routers/index.ts @@ -0,0 +1,20 @@ +import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { + CreatePostgresAccountSchema, + SanitizedPostgresAccountWithResourceSchema, + UpdatePostgresAccountSchema +} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; + +import { registerPamResourceEndpoints } from "./pam-account-endpoints"; + +export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record Promise> = { + [PamResource.Postgres]: async (server: FastifyZodProvider) => { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.Postgres, + accountResponseSchema: SanitizedPostgresAccountWithResourceSchema, + createAccountSchema: CreatePostgresAccountSchema, + updateAccountSchema: UpdatePostgresAccountSchema + }); + } +}; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts new file mode 100644 index 000000000..0ed7e238a --- /dev/null +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts @@ -0,0 +1,159 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { TPamAccount } from "@app/ee/services/pam-resource/pam-resource-types"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerPamResourceEndpoints = ({ + server, + resourceType, + createAccountSchema, + updateAccountSchema, + accountResponseSchema +}: { + server: FastifyZodProvider; + resourceType: PamResource; + createAccountSchema: z.ZodType<{ + credentials: C["credentials"]; + resourceId: C["resourceId"]; + folderId?: C["folderId"]; + name: C["name"]; + description?: C["description"]; + }>; + updateAccountSchema: z.ZodType<{ + credentials?: C["credentials"]; + name?: C["name"]; + description?: C["description"]; + }>; + accountResponseSchema: z.ZodTypeAny; +}) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create PAM account", + body: createAccountSchema, + response: { + 200: z.object({ + account: accountResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const account = await server.services.pamAccount.create(req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: account.projectId, + event: { + type: EventType.PAM_ACCOUNT_CREATE, + metadata: { + resourceId: req.body.resourceId, + resourceType, + folderId: req.body.folderId, + name: req.body.name, + description: req.body.description + } + } + }); + + return { account }; + } + }); + + server.route({ + method: "PATCH", + url: "/:accountId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update PAM account", + params: z.object({ + accountId: z.string().uuid() + }), + body: updateAccountSchema, + response: { + 200: z.object({ + account: accountResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const account = await server.services.pamAccount.updateById( + { + ...req.body, + accountId: req.params.accountId + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: account.projectId, + event: { + type: EventType.PAM_ACCOUNT_UPDATE, + metadata: { + accountId: req.params.accountId, + resourceId: account.resourceId, + resourceType, + name: req.body.name, + description: req.body.description + } + } + }); + + return { account }; + } + }); + + server.route({ + method: "DELETE", + url: "/:accountId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete PAM account", + params: z.object({ + accountId: z.string().uuid() + }), + response: { + 200: z.object({ + account: accountResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const account = await server.services.pamAccount.deleteById(req.params.accountId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: account.projectId, + event: { + type: EventType.PAM_ACCOUNT_DELETE, + metadata: { + accountId: req.params.accountId, + accountName: account.name, + resourceId: account.resourceId, + resourceType + } + } + }); + + return { account }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts new file mode 100644 index 000000000..647f39d8d --- /dev/null +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -0,0 +1,131 @@ +import { z } from "zod"; + +import { PamFoldersSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +// Use z.union([...]) when more resources are added +const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema; + +export const registerPamAccountRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List PAM accounts", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + accounts: SanitizedAccountSchema.array(), + folders: PamFoldersSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const response = await server.services.pamAccount.list(req.query.projectId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.PAM_ACCOUNT_LIST, + metadata: { + accountCount: response.accounts.length, + folderCount: response.folders.length + } + } + }); + + return response; + } + }); + + server.route({ + method: "POST", + url: "/access", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Access PAM account", + body: z.object({ + accountId: z.string().uuid(), + duration: z + .string() + .min(1) + .transform((val, ctx) => { + const parsedMs = ms(val); + + if (typeof parsedMs !== "number" || parsedMs <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid duration format. Must be a positive duration (e.g., '1h', '30m', '2d')." + }); + return z.NEVER; + } + return parsedMs; + }) + }), + response: { + 200: z.object({ + sessionId: z.string(), + resourceType: z.nativeEnum(PamResource), + relayClientCertificate: z.string(), + relayClientPrivateKey: z.string(), + relayServerCertificateChain: z.string(), + gatewayClientCertificate: z.string(), + gatewayClientPrivateKey: z.string(), + gatewayServerCertificateChain: z.string(), + relayHost: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + // To prevent type errors when accessing req.auth + if (req.auth.authMode !== AuthMode.JWT) { + throw new BadRequestError({ message: "You can only access PAM accounts using JWT auth tokens." }); + } + + const response = await server.services.pamAccount.access( + { + actorEmail: req.auth.user.email ?? "", + actorIp: req.realIp, + actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(), + actorUserAgent: req.auditLogInfo.userAgent ?? "", + ...req.body + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: response.projectId, + event: { + type: EventType.PAM_ACCOUNT_ACCESS, + metadata: { + accountId: req.body.accountId, + accountName: response.account.name, + duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined + } + } + }); + + return response; + } + }); +}; diff --git a/backend/src/ee/routes/v1/pam-folder-router.ts b/backend/src/ee/routes/v1/pam-folder-router.ts new file mode 100644 index 000000000..cd2506aba --- /dev/null +++ b/backend/src/ee/routes/v1/pam-folder-router.ts @@ -0,0 +1,150 @@ +import { z } from "zod"; + +import { PamFoldersSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { isValidFolderName } from "@app/lib/validator"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerPamFolderRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create PAM folder", + body: z.object({ + projectId: z.string().uuid(), + parentId: z.string().uuid().nullable().optional(), + name: z + .string() + .trim() + .refine((name) => isValidFolderName(name), { + message: "Folder name can only contain alphanumeric characters, dashes, and underscores." + }), + description: z.string().trim().max(512).nullable().optional() + }), + response: { + 200: z.object({ + folder: PamFoldersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const folder = await server.services.pamFolder.createFolder(req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.body.projectId, + event: { + type: EventType.PAM_FOLDER_CREATE, + metadata: { + name: req.body.name, + description: req.body.description, + parentId: req.body.parentId + } + } + }); + + return { folder }; + } + }); + + server.route({ + method: "PATCH", + url: "/:folderId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update PAM folder", + params: z.object({ + folderId: z.string().uuid() + }), + body: z.object({ + name: z + .string() + .trim() + .optional() + .refine((name) => (name ? isValidFolderName(name) : true), { + message: "Folder name can only contain alphanumeric characters, dashes, and underscores." + }), + description: z.string().trim().max(512).nullable().optional() + }), + response: { + 200: z.object({ + folder: PamFoldersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const folder = await server.services.pamFolder.updateFolder( + { + ...req.body, + id: req.params.folderId + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: folder.projectId, + event: { + type: EventType.PAM_FOLDER_UPDATE, + metadata: { + folderId: req.params.folderId, + name: req.body.name, + description: req.body.description + } + } + }); + + return { folder }; + } + }); + + server.route({ + method: "DELETE", + url: "/:folderId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete PAM folder", + params: z.object({ + folderId: z.string().uuid() + }), + response: { + 200: z.object({ + folder: PamFoldersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const folder = await server.services.pamFolder.deleteFolder(req.params.folderId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: folder.projectId, + event: { + type: EventType.PAM_FOLDER_DELETE, + metadata: { + folderName: folder.name, + folderId: req.params.folderId + } + } + }); + + return { folder }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts new file mode 100644 index 000000000..a63b67d94 --- /dev/null +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -0,0 +1,20 @@ +import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { + CreatePostgresResourceSchema, + PostgresResourceSchema, + UpdatePostgresResourceSchema +} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; + +import { registerPamResourceEndpoints } from "./pam-resource-endpoints"; + +export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record Promise> = { + [PamResource.Postgres]: async (server: FastifyZodProvider) => { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.Postgres, + resourceResponseSchema: PostgresResourceSchema, + createResourceSchema: CreatePostgresResourceSchema, + updateResourceSchema: UpdatePostgresResourceSchema + }); + } +}; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts new file mode 100644 index 000000000..776de8e48 --- /dev/null +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts @@ -0,0 +1,198 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { TPamResource } from "@app/ee/services/pam-resource/pam-resource-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerPamResourceEndpoints = ({ + server, + resourceType, + createResourceSchema, + updateResourceSchema, + resourceResponseSchema +}: { + server: FastifyZodProvider; + resourceType: PamResource; + createResourceSchema: z.ZodType<{ + projectId: T["projectId"]; + connectionDetails: T["connectionDetails"]; + gatewayId: T["gatewayId"]; + name: T["name"]; + }>; + updateResourceSchema: z.ZodType<{ + connectionDetails?: T["connectionDetails"]; + gatewayId?: T["gatewayId"]; + name?: T["name"]; + }>; + resourceResponseSchema: z.ZodTypeAny; +}) => { + server.route({ + method: "GET", + url: "/:resourceId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get PAM resource", + params: z.object({ + resourceId: z.string().uuid() + }), + response: { + 200: z.object({ + resource: resourceResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const resource = await server.services.pamResource.getById(req.params.resourceId, resourceType, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: resource.projectId, + event: { + type: EventType.PAM_RESOURCE_GET, + metadata: { + resourceId: resource.id, + resourceType: resource.resourceType, + name: resource.name + } + } + }); + + return { resource }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create PAM resource", + body: createResourceSchema, + response: { + 200: z.object({ + resource: resourceResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const resource = await server.services.pamResource.create( + { + ...req.body, + resourceType + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.body.projectId, + event: { + type: EventType.PAM_RESOURCE_CREATE, + metadata: { + resourceType, + gatewayId: req.body.gatewayId, + name: req.body.name + } + } + }); + + return { resource }; + } + }); + + server.route({ + method: "PATCH", + url: "/:resourceId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update PAM resource", + params: z.object({ + resourceId: z.string().uuid() + }), + body: updateResourceSchema, + response: { + 200: z.object({ + resource: resourceResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const resource = await server.services.pamResource.updateById( + { + ...req.body, + resourceId: req.params.resourceId + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: resource.projectId, + event: { + type: EventType.PAM_RESOURCE_UPDATE, + metadata: { + resourceId: req.params.resourceId, + resourceType, + gatewayId: req.body.gatewayId, + name: req.body.name + } + } + }); + + return { resource }; + } + }); + + server.route({ + method: "DELETE", + url: "/:resourceId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete PAM resource", + params: z.object({ + resourceId: z.string().uuid() + }), + response: { + 200: z.object({ + resource: resourceResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const resource = await server.services.pamResource.deleteById(req.params.resourceId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: resource.projectId, + event: { + type: EventType.PAM_RESOURCE_DELETE, + metadata: { + resourceId: req.params.resourceId, + resourceType + } + } + }); + + return { resource }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts new file mode 100644 index 000000000..c19c2030d --- /dev/null +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + PostgresResourceListItemSchema, + PostgresResourceSchema +} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +// Use z.union([...]) when more resources are added +const ResourceSchema = PostgresResourceSchema; + +const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]); + +export const registerPamResourceRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + description: "List PAM resource types", + response: { + 200: z.object({ + resourceOptions: ResourceOptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: () => { + const resourceOptions = server.services.pamResource.listResourceOptions(); + + return { resourceOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List PAM resources", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + resources: ResourceSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const response = await server.services.pamResource.list(req.query.projectId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.PAM_RESOURCE_LIST, + metadata: { + count: response.resources.length + } + } + }); + + return response; + } + }); +}; diff --git a/backend/src/ee/routes/v1/pam-session-router.ts b/backend/src/ee/routes/v1/pam-session-router.ts new file mode 100644 index 000000000..c353fddfa --- /dev/null +++ b/backend/src/ee/routes/v1/pam-session-router.ts @@ -0,0 +1,224 @@ +import { z } from "zod"; + +import { PamSessionsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +// Use z.union([]) once there's multiple +const SessionCredentialsSchema = PostgresSessionCredentialsSchema; + +export const registerPamSessionRouter = async (server: FastifyZodProvider) => { + // Meant to be hit solely by gateway identities + server.route({ + method: "GET", + url: "/:sessionId/credentials", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get PAM session credentials and start session", + params: z.object({ + sessionId: z.string().uuid() + }), + response: { + 200: z.object({ + credentials: SessionCredentialsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { credentials, projectId, account } = await server.services.pamAccount.getSessionCredentials( + req.params.sessionId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event: { + type: EventType.PAM_SESSION_START, + metadata: { + sessionId: req.params.sessionId, + accountName: account.name + } + } + }); + + return { credentials }; + } + }); + + // Meant to be hit solely by gateway identities + server.route({ + method: "POST", + url: "/:sessionId/logs", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update PAM session logs", + params: z.object({ + sessionId: z.string().uuid() + }), + body: z.object({ + logs: PamSessionCommandLogSchema.array() + }), + response: { + 200: z.object({ + session: PamSessionsSchema.omit({ + encryptedLogsBlob: true + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { session, projectId } = await server.services.pamSession.updateLogsById( + { + sessionId: req.params.sessionId, + logs: req.body.logs + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event: { + type: EventType.PAM_SESSION_LOGS_UPDATE, + metadata: { + sessionId: req.params.sessionId, + accountName: session.accountName + } + } + }); + + return { session }; + } + }); + + // Meant to be hit solely by gateway identities + server.route({ + method: "POST", + url: "/:sessionId/end", + config: { + rateLimit: writeLimit + }, + schema: { + description: "End PAM session", + params: z.object({ + sessionId: z.string().uuid() + }), + response: { + 200: z.object({ + session: PamSessionsSchema.omit({ + encryptedLogsBlob: true + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { session, projectId } = await server.services.pamSession.endSessionById( + req.params.sessionId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event: { + type: EventType.PAM_SESSION_END, + metadata: { + sessionId: req.params.sessionId, + accountName: session.accountName + } + } + }); + + return { session }; + } + }); + + server.route({ + method: "GET", + url: "/:sessionId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get PAM session", + params: z.object({ + sessionId: z.string().uuid() + }), + response: { + 200: z.object({ + session: SanitizedSessionSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const response = await server.services.pamSession.getById(req.params.sessionId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: response.session.projectId, + event: { + type: EventType.PAM_SESSION_GET, + metadata: { + sessionId: req.params.sessionId + } + } + }); + + return response; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List PAM sessions", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + sessions: SanitizedSessionSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const response = await server.services.pamSession.list(req.query.projectId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.PAM_SESSION_LIST, + metadata: { + count: response.sessions.length + } + } + }); + + return response; + } + }); +}; diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index f8e371d01..76bff60e8 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -286,7 +286,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { entryPoint: z.string(), issuer: z.string(), cert: z.string(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + enableGroupSync: z.boolean().optional() }) } }, @@ -325,14 +326,15 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { isActive: z.boolean().describe(SamlSso.CREATE_CONFIG.isActive), entryPoint: z.string().trim().describe(SamlSso.CREATE_CONFIG.entryPoint), issuer: z.string().trim().describe(SamlSso.CREATE_CONFIG.issuer), - cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert) + cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert), + enableGroupSync: z.boolean().optional().describe(SamlSso.CREATE_CONFIG.enableGroupSync) }), response: { 200: SanitizedSamlConfigSchema } }, handler: async (req) => { - const { isActive, authProvider, issuer, entryPoint, cert } = req.body; + const { isActive, authProvider, issuer, entryPoint, cert, enableGroupSync } = req.body; const { permission } = req; return server.services.saml.createSamlCfg({ @@ -341,6 +343,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { issuer, entryPoint, idpCert: cert, + enableGroupSync, actor: permission.type, actorId: permission.id, actorAuthMethod: permission.authMethod, @@ -372,7 +375,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { isActive: z.boolean().describe(SamlSso.UPDATE_CONFIG.isActive), entryPoint: z.string().trim().describe(SamlSso.UPDATE_CONFIG.entryPoint), issuer: z.string().trim().describe(SamlSso.UPDATE_CONFIG.issuer), - cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert) + cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert), + enableGroupSync: z.boolean().optional().describe(SamlSso.UPDATE_CONFIG.enableGroupSync) }) .partial() .merge(z.object({ organizationId: z.string().trim().describe(SamlSso.UPDATE_CONFIG.organizationId) })), @@ -381,7 +385,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { isActive, authProvider, issuer, entryPoint, cert } = req.body; + const { isActive, authProvider, issuer, entryPoint, cert, enableGroupSync } = req.body; const { permission } = req; return server.services.saml.updateSamlCfg({ @@ -390,6 +394,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { issuer, entryPoint, idpCert: cert, + enableGroupSync, actor: permission.type, actorId: permission.id, actorAuthMethod: permission.authMethod, diff --git a/backend/src/ee/routes/v2/gateway-router.ts b/backend/src/ee/routes/v2/gateway-router.ts index a7e656a64..56284729d 100644 --- a/backend/src/ee/routes/v2/gateway-router.ts +++ b/backend/src/ee/routes/v2/gateway-router.ts @@ -1,6 +1,7 @@ import z from "zod"; import { GatewaysV2Schema } from "@app/db/schemas"; +import { zodBuffer } from "@app/lib/zod"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -130,4 +131,25 @@ export const registerGatewayV2Router = async (server: FastifyZodProvider) => { return gateway; } }); + + server.route({ + method: "GET", + url: "/pam-session-key", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: zodBuffer + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const pamSessionKey = await server.services.gatewayV2.getPamSessionKey({ + orgPermission: req.permission + }); + + return pamSessionKey; + } + }); }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index ab72dbc65..bc50283d4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -500,7 +500,26 @@ export enum EventType { DASHBOARD_LIST_SECRETS = "dashboard-list-secrets", DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value", - DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value" + DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value", + + PAM_SESSION_START = "pam-session-start", + PAM_SESSION_LOGS_UPDATE = "pam-session-logs-update", + PAM_SESSION_END = "pam-session-end", + PAM_SESSION_GET = "pam-session-get", + PAM_SESSION_LIST = "pam-session-list", + PAM_FOLDER_CREATE = "pam-folder-create", + PAM_FOLDER_UPDATE = "pam-folder-update", + PAM_FOLDER_DELETE = "pam-folder-delete", + PAM_ACCOUNT_LIST = "pam-account-list", + PAM_ACCOUNT_ACCESS = "pam-account-access", + PAM_ACCOUNT_CREATE = "pam-account-create", + PAM_ACCOUNT_UPDATE = "pam-account-update", + PAM_ACCOUNT_DELETE = "pam-account-delete", + PAM_RESOURCE_LIST = "pam-resource-list", + PAM_RESOURCE_GET = "pam-resource-get", + PAM_RESOURCE_CREATE = "pam-resource-create", + PAM_RESOURCE_UPDATE = "pam-resource-update", + PAM_RESOURCE_DELETE = "pam-resource-delete" } export const filterableSecretEvents: EventType[] = [ @@ -3687,6 +3706,162 @@ interface OrgRoleDeleteEvent { }; } +interface PamSessionStartEvent { + type: EventType.PAM_SESSION_START; + metadata: { + sessionId: string; + accountName: string; + }; +} + +interface PamSessionLogsUpdateEvent { + type: EventType.PAM_SESSION_LOGS_UPDATE; + metadata: { + sessionId: string; + accountName: string; + }; +} + +interface PamSessionEndEvent { + type: EventType.PAM_SESSION_END; + metadata: { + sessionId: string; + accountName: string; + }; +} + +interface PamSessionGetEvent { + type: EventType.PAM_SESSION_GET; + metadata: { + sessionId: string; + }; +} + +interface PamSessionListEvent { + type: EventType.PAM_SESSION_LIST; + metadata: { + count: number; + }; +} + +interface PamFolderCreateEvent { + type: EventType.PAM_FOLDER_CREATE; + metadata: { + parentId?: string | null; + name: string; + description?: string | null; + }; +} + +interface PamFolderUpdateEvent { + type: EventType.PAM_FOLDER_UPDATE; + metadata: { + folderId: string; + name?: string; + description?: string | null; + }; +} + +interface PamFolderDeleteEvent { + type: EventType.PAM_FOLDER_DELETE; + metadata: { + folderId: string; + folderName: string; + }; +} + +interface PamAccountListEvent { + type: EventType.PAM_ACCOUNT_LIST; + metadata: { + accountCount: number; + folderCount: number; + }; +} + +interface PamAccountAccessEvent { + type: EventType.PAM_ACCOUNT_ACCESS; + metadata: { + accountId: string; + accountName: string; + duration?: string; + }; +} + +interface PamAccountCreateEvent { + type: EventType.PAM_ACCOUNT_CREATE; + metadata: { + resourceId: string; + resourceType: string; + folderId?: string | null; + name: string; + description?: string | null; + }; +} + +interface PamAccountUpdateEvent { + type: EventType.PAM_ACCOUNT_UPDATE; + metadata: { + accountId: string; + resourceId: string; + resourceType: string; + name?: string; + description?: string | null; + }; +} + +interface PamAccountDeleteEvent { + type: EventType.PAM_ACCOUNT_DELETE; + metadata: { + accountName: string; + accountId: string; + resourceId: string; + resourceType: string; + }; +} + +interface PamResourceListEvent { + type: EventType.PAM_RESOURCE_LIST; + metadata: { + count: number; + }; +} + +interface PamResourceGetEvent { + type: EventType.PAM_RESOURCE_GET; + metadata: { + resourceId: string; + resourceType: string; + name: string; + }; +} + +interface PamResourceCreateEvent { + type: EventType.PAM_RESOURCE_CREATE; + metadata: { + resourceType: string; + gatewayId: string; + name: string; + }; +} + +interface PamResourceUpdateEvent { + type: EventType.PAM_RESOURCE_UPDATE; + metadata: { + resourceId: string; + resourceType: string; + gatewayId?: string; + name?: string; + }; +} + +interface PamResourceDeleteEvent { + type: EventType.PAM_RESOURCE_DELETE; + metadata: { + resourceId: string; + resourceType: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -4020,4 +4195,22 @@ export type Event = | ProjectRoleDeleteEvent | OrgRoleCreateEvent | OrgRoleUpdateEvent - | OrgRoleDeleteEvent; + | OrgRoleDeleteEvent + | PamSessionStartEvent + | PamSessionLogsUpdateEvent + | PamSessionEndEvent + | PamSessionGetEvent + | PamSessionListEvent + | PamFolderCreateEvent + | PamFolderUpdateEvent + | PamFolderDeleteEvent + | PamAccountListEvent + | PamAccountAccessEvent + | PamAccountCreateEvent + | PamAccountUpdateEvent + | PamAccountDeleteEvent + | PamResourceListEvent + | PamResourceGetEvent + | PamResourceCreateEvent + | PamResourceUpdateEvent + | PamResourceDeleteEvent; diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index f4d43f23f..1bcfa3938 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -34,6 +34,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const $getClient = async (providerInputs: z.infer) => { const connection = new ElasticSearchClient({ + requestTimeout: 30_000, node: { url: new URL(`${providerInputs.host}:${providerInputs.port}`), ...(providerInputs.ca && { diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts index e67d4e890..7e41de91c 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts @@ -1,2 +1,3 @@ export const GATEWAY_ROUTING_INFO_OID = "1.3.6.1.4.1.12345.100.1"; export const GATEWAY_ACTOR_OID = "1.3.6.1.4.1.12345.100.2"; +export const PAM_INFO_OID = "1.3.6.1.4.1.12345.100.3"; diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index e8ccc8a5e..4bf6b1aef 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -22,11 +22,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; +import { PamResource } from "../pam-resource/pam-resource-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TRelayDALFactory } from "../relay/relay-dal"; import { TRelayServiceFactory } from "../relay/relay-service"; -import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID } from "./gateway-v2-constants"; +import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID, PAM_INFO_OID } from "./gateway-v2-constants"; import { TGatewayV2DALFactory } from "./gateway-v2-dal"; import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal"; @@ -414,6 +415,176 @@ export const gatewayV2ServiceFactory = ({ }; }; + const getPAMConnectionDetails = async ({ + gatewayId, + sessionId, + duration, + resourceType, + host, + port, + actorMetadata + }: { + gatewayId: string; + sessionId: string; + resourceType: PamResource; + duration?: number; + host: string; + port: number; + actorMetadata: { id: string; type: ActorType; name: string }; + }) => { + const gateway = await gatewayV2DAL.findById(gatewayId); + if (!gateway) { + return; + } + + const orgGatewayConfig = await orgGatewayConfigV2DAL.findOne({ orgId: gateway.orgId }); + if (!orgGatewayConfig) { + throw new NotFoundError({ message: `Gateway Config for org ${gateway.orgId} not found.` }); + } + + if (!gateway.relayId) { + throw new BadRequestError({ + message: "Gateway is not associated with a relay" + }); + } + + const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways." + }); + } + + const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgGatewayConfig.orgId + }); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + + const rootGatewayCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedRootGatewayCaCertificate + }) + ); + + const gatewayClientCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaCertificate + }) + ); + + const gatewayServerCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayServerCaCertificate + }) + ); + + const gatewayClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaPrivateKey + }); + + const gatewayClientCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: gatewayClientCaPrivateKey, + format: "der", + type: "pkcs8" + }); + + const importedGatewayClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + gatewayClientCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const clientCertIssuedAt = new Date(); + const clientCertExpiration = new Date(new Date().getTime() + (duration ?? 5 * 60 * 1000)); + const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCertSerialNumber = createSerialNumber(); + + const routingInfo = { + targetHost: host, + targetPort: port + }; + + const routingExtension = new x509.Extension( + GATEWAY_ROUTING_INFO_OID, + false, + Buffer.from(JSON.stringify(routingInfo)) + ); + + const pamInfoExtension = new x509.Extension( + PAM_INFO_OID, + false, + Buffer.from( + JSON.stringify({ + sessionId, + resourceType + }) + ) + ); + + const actorExtension = new x509.Extension( + GATEWAY_ACTOR_OID, + false, + Buffer.from(JSON.stringify({ type: actorMetadata.type, id: actorMetadata.id, name: actorMetadata.name })) + ); + + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${actorMetadata.type}:${gatewayId}`, + issuer: gatewayClientCaCert.subject, + notAfter: clientCertExpiration, + notBefore: clientCertIssuedAt, + signingKey: importedGatewayClientCaPrivateKey, + publicKey: clientKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(gatewayClientCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | + x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] | + x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true), + routingExtension, + actorExtension, + pamInfoExtension + ] + }); + + const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); + + const relayCredentials = await relayService.getCredentialsForClient({ + relayId: gateway.relayId, + orgId: gateway.orgId, + orgName: gateway.orgName, + gatewayId, + gatewayName: gateway.name, + duration + }); + + return { + relayHost: relayCredentials.relayHost, + gateway: { + clientCertificate: clientCert.toString("pem"), + clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert]) + }, + relay: { + clientCertificate: relayCredentials.clientCertificate, + clientPrivateKey: relayCredentials.clientPrivateKey, + serverCertificateChain: relayCredentials.serverCertificateChain + } + }; + }; + const registerGateway = async ({ orgId, actorId, @@ -645,14 +816,75 @@ export const gatewayV2ServiceFactory = ({ OrgPermissionSubjects.Gateway ); - return gatewayV2DAL.deleteById(gateway.id); + try { + return await gatewayV2DAL.deleteById(gateway.id); + } catch (err) { + if ( + err instanceof DatabaseError && + (err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation + ) { + throw new BadRequestError({ + message: "Failed to delete gateway because it is attached to active resources" + }); + } + + throw err; + } + }; + + const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + + return gatewayV2DAL.transaction(async (tx) => { + const gateway = await gatewayV2DAL.findOne( + { + identityId: orgPermission.id + }, + tx + ); + + if (!gateway) { + throw new NotFoundError({ message: "Gateway not found" }); + } + + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgPermission.orgId + }); + + if (gateway.encryptedPamSessionKey) { + return decryptor({ cipherTextBlob: gateway.encryptedPamSessionKey }); + } + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.GatewayPamSessionKey(gateway.id)]); + + const newPamSessionKey = crypto.randomBytes(32); + const { cipherTextBlob: encryptedPamSessionKey } = encryptor({ plainText: newPamSessionKey }); + + await gatewayV2DAL.updateById(gateway.id, { encryptedPamSessionKey }, tx); + + return newPamSessionKey; + }); }; return { listGateways, registerGateway, getPlatformConnectionDetailsByGatewayId, + getPAMConnectionDetails, deleteGatewayById, - heartbeat + heartbeat, + getPamSessionKey }; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index cbe194f31..2a3cf82cc 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -66,7 +66,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ enterpriseAppConnections: false, fips: false, eventSubscriptions: false, - machineIdentityAuthTemplates: false + machineIdentityAuthTemplates: false, + pam: false }); export const setupLicenseRequestWithStore = ( diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 0fdd07396..544eeae37 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -461,17 +461,16 @@ export const licenseServiceFactory = ({ }; const fetchPlanTableFromServer = async (customerId: string | null | undefined) => { - if (!customerId) { - throw new NotFoundError({ message: "Organization customer ID is required for plan table retrieval" }); - } - - const baseUrl = `/api/license-server/v1/customers/${customerId}`; + const baseUrl = `/api/license-server/v1/customers`; if (instanceType === InstanceType.Cloud) { + if (!customerId) { + throw new NotFoundError({ message: "Organization customer ID is required for plan table retrieval" }); + } const { data } = await licenseServerCloudApi.request.get<{ head: { name: string }[]; rows: { name: string; allowed: boolean }[]; - }>(`${baseUrl}/cloud-plan/table`); + }>(`${baseUrl}/${customerId}/cloud-plan/table`); return data; } diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 266794074..9cdcfcc3d 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -80,6 +80,7 @@ export type TFeatureSet = { machineIdentityAuthTemplates: false; fips: false; eventSubscriptions: false; + pam: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts new file mode 100644 index 000000000..b62e940fe --- /dev/null +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -0,0 +1,43 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TPamAccounts } from "@app/db/schemas"; +import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; + +export type TPamAccountDALFactory = ReturnType; + +type PamAccountFindFilter = Parameters>[0]; + +export const pamAccountDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.PamAccount); + + const findWithResourceDetails = async (filter: PamAccountFindFilter, tx?: Knex) => { + const query = (tx || db.replicaNode())(TableName.PamAccount) + .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .select(selectAllTableCols(TableName.PamAccount)) + .select( + // resource + db.ref("name").withSchema(TableName.PamResource).as("resourceName"), + db.ref("resourceType").withSchema(TableName.PamResource) + ); + + if (filter) { + /* eslint-disable @typescript-eslint/no-misused-promises */ + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PamAccount, filter))); + } + + const accounts = await query; + + return accounts.map(({ resourceId, resourceName, resourceType, ...account }) => ({ + ...account, + resourceId, + resource: { + id: resourceId, + name: resourceName, + resourceType + } + })); + }; + + return { ...orm, findWithResourceDetails }; +}; diff --git a/backend/src/ee/services/pam-account/pam-account-fns.ts b/backend/src/ee/services/pam-account/pam-account-fns.ts new file mode 100644 index 000000000..fdc440991 --- /dev/null +++ b/backend/src/ee/services/pam-account/pam-account-fns.ts @@ -0,0 +1,61 @@ +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TPamAccountCredentials } from "../pam-resource/pam-resource-types"; + +export const encryptAccountCredentials = async ({ + projectId, + credentials, + kmsService +}: { + projectId: string; + credentials: TPamAccountCredentials; + kmsService: Pick; +}) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(credentials)) + }); + + return encryptedCredentialsBlob; +}; + +export const decryptAccountCredentials = async ({ + projectId, + encryptedCredentials, + kmsService +}: { + projectId: string; + encryptedCredentials: Buffer; + kmsService: Pick; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedCredentials + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials; +}; + +export const decryptAccount = async ( + account: T, + projectId: string, + kmsService: Pick +): Promise => { + return { + ...account, + credentials: await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + projectId, + kmsService + }) + } as T & { credentials: TPamAccountCredentials }; +}; diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts new file mode 100644 index 000000000..e9ea76e8c --- /dev/null +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -0,0 +1,527 @@ +import { ForbiddenError, subject } from "@casl/ability"; + +import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; +import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionActions, + ProjectPermissionPamAccountActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; + +import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; +import { TLicenseServiceFactory } from "../license/license-service"; +import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal"; +import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns"; +import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal"; +import { PamResource } from "../pam-resource/pam-resource-enums"; +import { TPamAccountCredentials } from "../pam-resource/pam-resource-types"; +import { TPamSessionDALFactory } from "../pam-session/pam-session-dal"; +import { PamSessionStatus } from "../pam-session/pam-session-enums"; +import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPamAccountDALFactory } from "./pam-account-dal"; +import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns"; +import { TAccessAccountDTO, TCreateAccountDTO, TUpdateAccountDTO } from "./pam-account-types"; + +type TPamAccountServiceFactoryDep = { + pamResourceDAL: TPamResourceDALFactory; + pamSessionDAL: TPamSessionDALFactory; + pamAccountDAL: TPamAccountDALFactory; + pamFolderDAL: TPamFolderDALFactory; + projectDAL: TProjectDALFactory; + permissionService: Pick; + licenseService: Pick; + kmsService: Pick; + gatewayV2Service: Pick< + TGatewayV2ServiceFactory, + "getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId" + >; + userDAL: TUserDALFactory; +}; + +export type TPamAccountServiceFactory = ReturnType; + +export const pamAccountServiceFactory = ({ + pamResourceDAL, + pamSessionDAL, + pamAccountDAL, + pamFolderDAL, + projectDAL, + userDAL, + permissionService, + licenseService, + kmsService, + gatewayV2Service +}: TPamAccountServiceFactoryDep) => { + const create = async ( + { credentials, resourceId, name, description, folderId }: TCreateAccountDTO, + actor: OrgServiceActor + ) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const resource = await pamResourceDAL.findById(resourceId); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: resource.projectId, + actionProjectType: ActionProjectType.PAM + }); + + const accountPath = await getFullPamFolderPath({ + pamFolderDAL, + folderId, + projectId: resource.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamAccountActions.Create, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: resource.name, + accountName: name, + accountPath + }) + ); + + const connectionDetails = await decryptResourceConnectionDetails({ + projectId: resource.projectId, + encryptedConnectionDetails: resource.encryptedConnectionDetails, + kmsService + }); + + const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource]( + resource.resourceType as PamResource, + connectionDetails, + resource.gatewayId, + gatewayV2Service + ); + const validatedCredentials = await factory.validateAccountCredentials(credentials); + + const encryptedCredentials = await encryptAccountCredentials({ + credentials: validatedCredentials, + projectId: resource.projectId, + kmsService + }); + + try { + const account = await pamAccountDAL.create({ + projectId: resource.projectId, + resourceId: resource.id, + encryptedCredentials, + name, + description, + folderId + }); + + return { + ...(await decryptAccount(account, resource.projectId, kmsService)), + resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `Account with name '${name}' already exists for this path` + }); + } + + throw err; + } + }; + + const updateById = async ( + { accountId, credentials, description, name }: TUpdateAccountDTO, + actor: OrgServiceActor + ) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const account = await pamAccountDAL.findById(accountId); + if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` }); + + const resource = await pamResourceDAL.findById(account.resourceId); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: account.projectId, + actionProjectType: ActionProjectType.PAM + }); + + const accountPath = await getFullPamFolderPath({ + pamFolderDAL, + folderId: account.folderId, + projectId: account.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamAccountActions.Edit, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: resource.name, + accountName: account.name, + accountPath + }) + ); + + const updateDoc: Partial = {}; + + if (name !== undefined) { + updateDoc.name = name; + } + + if (description !== undefined) { + updateDoc.description = description; + } + + if (credentials !== undefined) { + const connectionDetails = await decryptResourceConnectionDetails({ + projectId: account.projectId, + encryptedConnectionDetails: resource.encryptedConnectionDetails, + kmsService + }); + + const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource]( + resource.resourceType as PamResource, + connectionDetails, + resource.gatewayId, + gatewayV2Service + ); + + // Logic to prevent overwriting unedited censored values + const finalCredentials = { ...credentials }; + if (credentials.password === "******") { + const decryptedCredentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + projectId: account.projectId, + kmsService + }); + + finalCredentials.password = decryptedCredentials.password; + } + + const validatedCredentials = await factory.validateAccountCredentials(finalCredentials); + const encryptedCredentials = await encryptAccountCredentials({ + credentials: validatedCredentials, + projectId: account.projectId, + kmsService + }); + updateDoc.encryptedCredentials = encryptedCredentials; + } + + // If nothing was updated, return the fetched account + if (Object.keys(updateDoc).length === 0) { + return decryptAccount(account, account.projectId, kmsService); + } + + const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc); + + return { + ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), + resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + }; + }; + + const deleteById = async (id: string, actor: OrgServiceActor) => { + const account = await pamAccountDAL.findById(id); + if (!account) throw new NotFoundError({ message: `Account with ID '${id}' not found` }); + + const resource = await pamResourceDAL.findById(account.resourceId); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: account.projectId, + actionProjectType: ActionProjectType.PAM + }); + + const accountPath = await getFullPamFolderPath({ + pamFolderDAL, + folderId: account.folderId, + projectId: account.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamAccountActions.Delete, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: resource.name, + accountName: account.name, + accountPath + }) + ); + + const deletedAccount = await pamAccountDAL.deleteById(id); + + return { + ...(await decryptAccount(deletedAccount, account.projectId, kmsService)), + resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + }; + }; + + const list = async (projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.PAM + }); + + const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId }); + + const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders); + + const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : []; + + const decryptedAndPermittedAccounts: Array< + TPamAccounts & { + resource: Pick; + credentials: TPamAccountCredentials; + } + > = []; + + for await (const account of accountsWithResourceDetails) { + const accountPath = await getFullPamFolderPath({ + pamFolderDAL, + folderId: account.folderId, + projectId: account.projectId + }); + + // Check permission for each individual account + if ( + permission.can( + ProjectPermissionPamAccountActions.Read, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: account.resource.name, + accountName: account.name, + accountPath + }) + ) + ) { + // Decrypt the account only if the user has permission to read it + const decryptedAccount = await decryptAccount(account, account.projectId, kmsService); + decryptedAndPermittedAccounts.push({ + ...decryptedAccount, + resource: { + id: account.resource.id, + name: account.resource.name, + resourceType: account.resource.resourceType + } + }); + } + } + + return { + accounts: decryptedAndPermittedAccounts, + folders + }; + }; + + const access = async ( + { accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, + actor: OrgServiceActor + ) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const account = await pamAccountDAL.findById(accountId); + if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` }); + + const resource = await pamResourceDAL.findById(account.resourceId); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: account.projectId, + actionProjectType: ActionProjectType.PAM + }); + + const accountPath = await getFullPamFolderPath({ + pamFolderDAL, + folderId: account.folderId, + projectId: account.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamAccountActions.Access, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: resource.name, + accountName: account.name, + accountPath + }) + ); + + const session = await pamSessionDAL.create({ + accountName: account.name, + actorEmail, + actorIp, + actorName, + actorUserAgent, + projectId: account.projectId, + resourceName: resource.name, + resourceType: resource.resourceType, + status: PamSessionStatus.Starting, + accountId: account.id, + userId: actor.id, + expiresAt: new Date(Date.now() + duration) + }); + + const { connectionDetails, gatewayId, resourceType } = await decryptResource( + resource, + account.projectId, + kmsService + ); + + const user = await userDAL.findById(actor.id); + if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` }); + + const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({ + gatewayId, + duration, + sessionId: session.id, + resourceType: resource.resourceType as PamResource, + host: connectionDetails.host, + port: connectionDetails.port, + actorMetadata: { + id: actor.id, + type: actor.type, + name: user.email ?? "" + } + }); + + if (!gatewayConnectionDetails) { + throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` }); + } + + return { + sessionId: session.id, + resourceType, + relayClientCertificate: gatewayConnectionDetails.relay.clientCertificate, + relayClientPrivateKey: gatewayConnectionDetails.relay.clientPrivateKey, + relayServerCertificateChain: gatewayConnectionDetails.relay.serverCertificateChain, + gatewayClientCertificate: gatewayConnectionDetails.gateway.clientCertificate, + gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey, + gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain, + relayHost: gatewayConnectionDetails.relayHost, + projectId: account.projectId, + account + }; + }; + + const getSessionCredentials = async (sessionId: string, actor: OrgServiceActor) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + // To be hit by gateways only + if (actor.type !== ActorType.IDENTITY) { + throw new ForbiddenRequestError({ message: "Only gateways can perform this action" }); + } + + const session = await pamSessionDAL.findById(sessionId); + if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + + const project = await projectDAL.findById(session.projectId); + if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + project.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + + if (!session.accountId) throw new NotFoundError({ message: "Session is missing accountId column" }); + + // Verify that the session has not ended + if (session.endedAt || (session.expiresAt && session.expiresAt < new Date())) { + throw new BadRequestError({ message: "Session has ended or expired" }); + } + + // Verify that the session has not already had credentials fetched + if (session.status !== PamSessionStatus.Starting) { + throw new BadRequestError({ message: "Session has already been started" }); + } + + const account = await pamAccountDAL.findById(session.accountId); + if (!account) throw new NotFoundError({ message: `Account with ID '${session.accountId}' not found` }); + + const resource = await pamResourceDAL.findById(account.resourceId); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); + + if (resource.gatewayIdentityId !== actor.id) { + throw new ForbiddenRequestError({ + message: "Identity does not have access to fetch the PAM session credentials" + }); + } + + const decryptedAccount = await decryptAccount(account, session.projectId, kmsService); + + const decryptedResource = await decryptResource(resource, session.projectId, kmsService); + + // Mark session as started + await pamSessionDAL.updateById(sessionId, { + status: PamSessionStatus.Active, + startedAt: new Date() + }); + + return { + credentials: { + ...decryptedResource.connectionDetails, + ...decryptedAccount.credentials + }, + projectId: project.id, + account + }; + }; + + return { + create, + updateById, + deleteById, + list, + access, + getSessionCredentials + }; +}; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts new file mode 100644 index 000000000..514d7d780 --- /dev/null +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -0,0 +1,17 @@ +import { TPamAccount } from "../pam-resource/pam-resource-types"; + +// DTOs +export type TCreateAccountDTO = Pick; + +export type TUpdateAccountDTO = Partial> & { + accountId: string; +}; + +export type TAccessAccountDTO = { + accountId: string; + actorEmail: string; + actorIp: string; + actorName: string; + actorUserAgent: string; + duration: number; +}; diff --git a/backend/src/ee/services/pam-folder/pam-folder-dal.ts b/backend/src/ee/services/pam-folder/pam-folder-dal.ts new file mode 100644 index 000000000..aa334618d --- /dev/null +++ b/backend/src/ee/services/pam-folder/pam-folder-dal.ts @@ -0,0 +1,9 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPamFolderDALFactory = ReturnType; +export const pamFolderDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.PamFolder); + return { ...orm }; +}; diff --git a/backend/src/ee/services/pam-folder/pam-folder-fns.ts b/backend/src/ee/services/pam-folder/pam-folder-fns.ts new file mode 100644 index 000000000..bcba0ab3b --- /dev/null +++ b/backend/src/ee/services/pam-folder/pam-folder-fns.ts @@ -0,0 +1,33 @@ +import { TPamFolderDALFactory } from "./pam-folder-dal"; + +type GetFullFolderPath = { + pamFolderDAL: Pick; + folderId?: string | null; + projectId: string; +}; + +export const getFullPamFolderPath = async ({ + pamFolderDAL, + folderId, + projectId +}: GetFullFolderPath): Promise => { + if (!folderId) return "/"; + + const folders = await pamFolderDAL.find({ projectId }); + const folderMap = new Map(folders.map((folder) => [folder.id, folder])); + + if (!folderMap.has(folderId)) return ""; + + const path: string[] = []; + let currentFolderId: string | null | undefined = folderId; + + while (currentFolderId) { + const folder = folderMap.get(currentFolderId); + if (!folder) break; + + path.unshift(folder.name); + currentFolderId = folder.parentId; + } + + return `/${path.join("/")}`; +}; diff --git a/backend/src/ee/services/pam-folder/pam-folder-service.ts b/backend/src/ee/services/pam-folder/pam-folder-service.ts new file mode 100644 index 000000000..d7fb41f12 --- /dev/null +++ b/backend/src/ee/services/pam-folder/pam-folder-service.ts @@ -0,0 +1,146 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType, TPamFolders } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { TPamFolderDALFactory } from "./pam-folder-dal"; +import { TCreateFolderDTO, TUpdateFolderDTO } from "./pam-folder-types"; + +type TPamFolderServiceFactoryDep = { + pamFolderDAL: TPamFolderDALFactory; + permissionService: Pick; + licenseService: Pick; +}; + +export type TPamFolderServiceFactory = ReturnType; + +export const pamFolderServiceFactory = ({ + pamFolderDAL, + permissionService, + licenseService +}: TPamFolderServiceFactoryDep) => { + const createFolder = async ({ name, description, parentId, projectId }: TCreateFolderDTO, actor: OrgServiceActor) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PamFolders); + + if (parentId) { + if (!(await pamFolderDAL.findOne({ id: parentId, projectId }))) { + throw new NotFoundError({ + message: `Parent folder '${parentId}' not found for project '${projectId}'` + }); + } + } + + try { + const folder = await pamFolderDAL.create({ + name, + description: description ?? null, + parentId: parentId || null, + projectId + }); + + return folder; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `Folder with name '${name}' already exists for this path` + }); + } + + throw err; + } + }; + + const updateFolder = async ({ id, name, description }: TUpdateFolderDTO, actor: OrgServiceActor) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const folder = await pamFolderDAL.findById(id); + if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: folder.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PamFolders); + + const updateDoc: Partial = {}; + + if (name !== undefined) { + updateDoc.name = name; + } + + if (description !== undefined) { + updateDoc.description = description; + } + + if (Object.keys(updateDoc).length === 0) { + return folder; + } + + try { + const updatedFolder = await pamFolderDAL.updateById(id, updateDoc); + + return updatedFolder; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `Folder with name '${name}' already exists for this path` + }); + } + + throw err; + } + }; + + const deleteFolder = async (id: string, actor: OrgServiceActor) => { + const folder = await pamFolderDAL.findById(id); + if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: folder.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamFolders); + + const deletedFolder = await pamFolderDAL.deleteById(id); + + return deletedFolder; + }; + + return { createFolder, updateFolder, deleteFolder }; +}; diff --git a/backend/src/ee/services/pam-folder/pam-folder-types.ts b/backend/src/ee/services/pam-folder/pam-folder-types.ts new file mode 100644 index 000000000..c8a435637 --- /dev/null +++ b/backend/src/ee/services/pam-folder/pam-folder-types.ts @@ -0,0 +1,13 @@ +// DTOs +export interface TCreateFolderDTO { + projectId: string; + parentId?: string | null; + name: string; + description?: string | null; +} + +export interface TUpdateFolderDTO { + id: string; + name?: string; + description?: string | null; +} diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts new file mode 100644 index 000000000..1a408ca27 --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TPamResourceDALFactory = ReturnType; +export const pamResourceDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.PamResource); + + const findById = async (id: string, tx?: Knex) => { + const doc = await (tx || db.replicaNode())(TableName.PamResource) + .join(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`) + .select(selectAllTableCols(TableName.PamResource)) + .select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName")) + .select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId")) + .where(`${TableName.PamResource}.id`, id) + .first(); + + return doc; + }; + + return { ...orm, findById }; +}; diff --git a/backend/src/ee/services/pam-resource/pam-resource-enums.ts b/backend/src/ee/services/pam-resource/pam-resource-enums.ts new file mode 100644 index 000000000..fbc260fba --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-enums.ts @@ -0,0 +1,3 @@ +export enum PamResource { + Postgres = "postgres" +} diff --git a/backend/src/ee/services/pam-resource/pam-resource-factory.ts b/backend/src/ee/services/pam-resource/pam-resource-factory.ts new file mode 100644 index 000000000..298b1664c --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-factory.ts @@ -0,0 +1,9 @@ +import { PamResource } from "./pam-resource-enums"; +import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types"; +import { sqlResourceFactory } from "./shared/sql/sql-resource-factory"; + +type TPamResourceFactoryImplementation = TPamResourceFactory; + +export const PAM_RESOURCE_FACTORY_MAP: Record = { + [PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation +}; diff --git a/backend/src/ee/services/pam-resource/pam-resource-fns.ts b/backend/src/ee/services/pam-resource/pam-resource-fns.ts new file mode 100644 index 000000000..1d79e892e --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-fns.ts @@ -0,0 +1,68 @@ +import { TPamResources } from "@app/db/schemas"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types"; +import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns"; + +export const listResourceOptions = () => { + return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name)); +}; + +// Resource +export const encryptResourceConnectionDetails = async ({ + projectId, + connectionDetails, + kmsService +}: { + projectId: string; + connectionDetails: TPamResourceConnectionDetails; + kmsService: Pick; +}) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(connectionDetails)) + }); + + return encryptedConnectionDetailsBlob; +}; + +export const decryptResourceConnectionDetails = async ({ + projectId, + encryptedConnectionDetails, + kmsService +}: { + projectId: string; + encryptedConnectionDetails: Buffer; + kmsService: Pick; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedConnectionDetails + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TPamResourceConnectionDetails; +}; + +export const decryptResource = async ( + resource: TPamResources, + projectId: string, + kmsService: Pick +) => { + return { + ...resource, + connectionDetails: await decryptResourceConnectionDetails({ + encryptedConnectionDetails: resource.encryptedConnectionDetails, + projectId, + kmsService + }) + } as TPamResource; +}; diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts new file mode 100644 index 000000000..80a50a9a4 --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +import { PamAccountsSchema, PamResourcesSchema } from "@app/db/schemas"; +import { slugSchema } from "@app/server/lib/schemas"; + +// Resources +export const BasePamResourceSchema = PamResourcesSchema.omit({ + encryptedConnectionDetails: true, + resourceType: true +}); + +export const BaseCreatePamResourceSchema = z.object({ + projectId: z.string().uuid(), + gatewayId: z.string().uuid(), + name: slugSchema({ field: "name" }) +}); + +export const BaseUpdatePamResourceSchema = z.object({ + gatewayId: z.string().uuid().optional(), + name: slugSchema({ field: "name" }).optional() +}); + +// Accounts +export const BasePamAccountSchema = PamAccountsSchema.omit({ + encryptedCredentials: true +}); + +export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({ + resource: PamResourcesSchema.pick({ + id: true, + name: true, + resourceType: true + }) +}); + +export const BaseCreatePamAccountSchema = z.object({ + resourceId: z.string().uuid(), + folderId: z.string().uuid().optional(), + name: slugSchema({ field: "name" }), + description: z.string().max(512).nullable().optional() +}); + +export const BaseUpdatePamAccountSchema = z.object({ + name: slugSchema({ field: "name" }).optional(), + description: z.string().max(512).nullable().optional() +}); diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts new file mode 100644 index 000000000..312795a50 --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -0,0 +1,222 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType, TPamResources } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; +import { TLicenseServiceFactory } from "../license/license-service"; +import { TPamResourceDALFactory } from "./pam-resource-dal"; +import { PamResource } from "./pam-resource-enums"; +import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory"; +import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns"; +import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types"; + +type TPamResourceServiceFactoryDep = { + pamResourceDAL: TPamResourceDALFactory; + permissionService: Pick; + licenseService: Pick; + kmsService: Pick; + gatewayV2Service: Pick< + TGatewayV2ServiceFactory, + "getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId" + >; +}; + +export type TPamResourceServiceFactory = ReturnType; + +export const pamResourceServiceFactory = ({ + pamResourceDAL, + permissionService, + licenseService, + kmsService, + gatewayV2Service +}: TPamResourceServiceFactoryDep) => { + const getById = async (id: string, resourceType: PamResource, actor: OrgServiceActor) => { + const resource = await pamResourceDAL.findById(id); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: resource.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PamResources); + + if (resource.resourceType !== resourceType) { + throw new BadRequestError({ + message: `Resource with ID '${id}' is not of type '${resourceType}'` + }); + } + + return decryptResource(resource, resource.projectId, kmsService); + }; + + const create = async ( + { resourceType, connectionDetails, gatewayId, name, projectId }: TCreateResourceDTO, + actor: OrgServiceActor + ) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PamResources); + + const factory = PAM_RESOURCE_FACTORY_MAP[resourceType]( + resourceType, + connectionDetails, + gatewayId, + gatewayV2Service + ); + const validatedConnectionDetails = await factory.validateConnection(); + + const encryptedConnectionDetails = await encryptResourceConnectionDetails({ + connectionDetails: validatedConnectionDetails, + projectId, + kmsService + }); + + const resource = await pamResourceDAL.create({ + resourceType, + encryptedConnectionDetails, + gatewayId, + name, + projectId + }); + + return decryptResource(resource, projectId, kmsService); + }; + + const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + const resource = await pamResourceDAL.findById(resourceId); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: resource.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PamResources); + + const updateDoc: Partial = {}; + + if (name !== undefined) { + updateDoc.name = name; + } + + if (connectionDetails !== undefined) { + const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource]( + resource.resourceType as PamResource, + connectionDetails, + resource.gatewayId, + gatewayV2Service + ); + const validatedConnectionDetails = await factory.validateConnection(); + const encryptedConnectionDetails = await encryptResourceConnectionDetails({ + connectionDetails: validatedConnectionDetails, + projectId: resource.projectId, + kmsService + }); + updateDoc.encryptedConnectionDetails = encryptedConnectionDetails; + } + + // If nothing was updated, return the fetched resource + if (Object.keys(updateDoc).length === 0) { + return decryptResource(resource, resource.projectId, kmsService); + } + + const updatedResource = await pamResourceDAL.updateById(resourceId, updateDoc); + + return decryptResource(updatedResource, resource.projectId, kmsService); + }; + + const deleteById = async (id: string, actor: OrgServiceActor) => { + const resource = await pamResourceDAL.findById(id); + if (!resource) throw new NotFoundError({ message: `Resource with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: resource.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamResources); + + try { + const deletedResource = await pamResourceDAL.deleteById(id); + return await decryptResource(deletedResource, resource.projectId, kmsService); + } catch (err) { + if ( + err instanceof DatabaseError && + (err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation + ) { + throw new BadRequestError({ + message: "Failed to delete resource because it is attached to active PAM accounts" + }); + } + throw err; + } + }; + + const list = async (projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PamResources); + + const resources = await pamResourceDAL.find({ projectId }); + + return { + resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService))) + }; + }; + + return { + getById, + create, + updateById, + deleteById, + list, + listResourceOptions + }; +}; diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts new file mode 100644 index 000000000..fb1b669ed --- /dev/null +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -0,0 +1,42 @@ +import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; +import { PamResource } from "./pam-resource-enums"; +import { + TPostgresAccount, + TPostgresAccountCredentials, + TPostgresResource, + TPostgresResourceConnectionDetails +} from "./postgres/postgres-resource-types"; + +// Resource types +export type TPamResource = TPostgresResource; +export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails; + +// Account types +export type TPamAccount = TPostgresAccount; +export type TPamAccountCredentials = TPostgresAccountCredentials; + +// Resource DTOs +export type TCreateResourceDTO = Pick< + TPamResource, + "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" +>; + +export type TUpdateResourceDTO = Partial> & { + resourceId: string; +}; + +// Resource factory +export type TPamResourceFactoryValidateConnection = () => Promise; +export type TPamResourceFactoryValidateAccountCredentials = ( + credentials: C +) => Promise; + +export type TPamResourceFactory = ( + resourceType: PamResource, + connectionDetails: T, + gatewayId: string, + gatewayV2Service: Pick +) => { + validateConnection: TPamResourceFactoryValidateConnection; + validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials; +}; diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-fns.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-fns.ts new file mode 100644 index 000000000..a3329a9fb --- /dev/null +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-fns.ts @@ -0,0 +1,8 @@ +import { PostgresResourceListItemSchema } from "./postgres-resource-schemas"; + +export const getPostgresResourceListItem = () => { + return { + name: PostgresResourceListItemSchema.shape.name.value, + resource: PostgresResourceListItemSchema.shape.resource.value + }; +}; diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts new file mode 100644 index 000000000..a97e3f2e7 --- /dev/null +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; + +import { PamResource } from "../pam-resource-enums"; +import { + BaseCreatePamAccountSchema, + BaseCreatePamResourceSchema, + BasePamAccountSchema, + BasePamAccountSchemaWithResource, + BasePamResourceSchema, + BaseUpdatePamAccountSchema, + BaseUpdatePamResourceSchema +} from "../pam-resource-schemas"; +import { + BaseSqlAccountCredentialsSchema, + BaseSqlResourceConnectionDetailsSchema +} from "../shared/sql/sql-resource-schemas"; + +// Resources +export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema; + +const BasePostgresResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.Postgres) }); + +export const PostgresResourceSchema = BasePostgresResourceSchema.extend({ + connectionDetails: PostgresResourceConnectionDetailsSchema +}); + +export const PostgresResourceListItemSchema = z.object({ + name: z.literal("PostgreSQL"), + resource: z.literal(PamResource.Postgres) +}); + +export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({ + connectionDetails: PostgresResourceConnectionDetailsSchema +}); + +export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({ + connectionDetails: PostgresResourceConnectionDetailsSchema.optional() +}); + +// Accounts +export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema; + +export const PostgresAccountSchema = BasePamAccountSchema.extend({ + credentials: PostgresAccountCredentialsSchema +}); + +export const CreatePostgresAccountSchema = BaseCreatePamAccountSchema.extend({ + credentials: PostgresAccountCredentialsSchema +}); + +export const UpdatePostgresAccountSchema = BaseUpdatePamAccountSchema.extend({ + credentials: PostgresAccountCredentialsSchema.optional() +}); + +export const SanitizedPostgresAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({ + credentials: PostgresAccountCredentialsSchema.pick({ + username: true + }) +}); + +// Sessions +export const PostgresSessionCredentialsSchema = PostgresResourceConnectionDetailsSchema.and( + PostgresAccountCredentialsSchema +); diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-types.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-types.ts new file mode 100644 index 000000000..223ba6790 --- /dev/null +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-types.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { + PostgresAccountCredentialsSchema, + PostgresAccountSchema, + PostgresResourceConnectionDetailsSchema, + PostgresResourceSchema +} from "./postgres-resource-schemas"; + +// Resources +export type TPostgresResource = z.infer; +export type TPostgresResourceConnectionDetails = z.infer; + +// Accounts +export type TPostgresAccount = z.infer; +export type TPostgresAccountCredentials = z.infer; diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts new file mode 100644 index 000000000..74a2c74ae --- /dev/null +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -0,0 +1,183 @@ +import knex, { Knex } from "knex"; +import tls, { PeerCertificate } from "tls"; + +import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; +import { BadRequestError } from "@app/lib/errors"; +import { GatewayProxyProtocol } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; + +import { PamResource } from "../../pam-resource-enums"; +import { TPamResourceFactory, TPamResourceFactoryValidateAccountCredentials } from "../../pam-resource-types"; +import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "./sql-resource-types"; + +const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; + +const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test"; +const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password"; + +const SQL_CONNECTION_CLIENT_MAP = { + [PamResource.Postgres]: "pg" +}; + +const getConnectionConfig = ( + resourceType: PamResource, + { host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails +) => { + switch (resourceType) { + case PamResource.Postgres: { + return { + ssl: sslEnabled + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate, + servername: host, + // When using proxy, we need to bypass hostname validation since we connect to localhost + // but validate the certificate against the actual hostname + checkServerIdentity: (hostname: string, cert: PeerCertificate) => { + return tls.checkServerIdentity(host, cert); + } + } + : false + }; + } + default: + throw new BadRequestError({ + message: `Unhandled SQL Resource Connection Config: ${resourceType as PamResource}` + }); + } +}; + +export const executeWithGateway = async ( + config: { + connectionDetails: TSqlResourceConnectionDetails; + resourceType: PamResource; + gatewayId: string; + username?: string; + password?: string; + }, + gatewayV2Service: Pick, + operation: (client: Knex) => Promise +): Promise => { + const { connectionDetails, resourceType, gatewayId, username, password } = config; + + const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true); + const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId, + targetHost, + targetPort: connectionDetails.port + }); + + if (!platformConnectionDetails) { + throw new BadRequestError({ message: "Unable to connect to gateway, no platform connection details found" }); + } + + return withGatewayV2Proxy( + async (proxyPort) => { + const client = knex({ + client: SQL_CONNECTION_CLIENT_MAP[resourceType], + connection: { + database: connectionDetails.database, + port: proxyPort, + host: "localhost", + user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback + password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ...getConnectionConfig(resourceType, connectionDetails) + } + }); + try { + return await operation(client); + } finally { + await client.destroy(); + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + relayHost: platformConnectionDetails.relayHost, + gateway: platformConnectionDetails.gateway, + relay: platformConnectionDetails.relay + } + ); +}; + +export const sqlResourceFactory: TPamResourceFactory = ( + resourceType, + connectionDetails, + gatewayId, + gatewayV2Service +) => { + const validateConnection = async () => { + try { + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => { + await client.raw("Select 1"); + }); + return connectionDetails; + } catch (error) { + // Hacky way to know if we successfully hit the database + if (error instanceof BadRequestError) { + if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) { + return connectionDetails; + } + + if (error.message.includes("no pg_hba.conf entry for host")) { + return connectionDetails; + } + + if (error.message === "Connection terminated unexpectedly") { + throw new BadRequestError({ + message: "Connection terminated unexpectedly. Verify that host and port are correct" + }); + } + } + + throw new BadRequestError({ + message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials = async ( + credentials + ) => { + try { + await executeWithGateway( + { + connectionDetails, + gatewayId, + resourceType, + username: credentials.username, + password: credentials.password + }, + gatewayV2Service, + async (client) => { + await client.raw("Select 1"); + } + ); + return credentials; + } catch (error) { + if (error instanceof BadRequestError) { + if (error.message === `password authentication failed for user "${credentials.username}"`) { + throw new BadRequestError({ + message: "Account credentials invalid: Username or password incorrect" + }); + } + + if (error.message === "Connection terminated unexpectedly") { + throw new BadRequestError({ + message: "Connection terminated unexpectedly. Verify that host and port are correct" + }); + } + } + + throw new BadRequestError({ + message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + return { + validateConnection, + validateAccountCredentials + }; +}; diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts new file mode 100644 index 000000000..cb3abf109 --- /dev/null +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +// Resources +export const BaseSqlResourceConnectionDetailsSchema = z.object({ + host: z.string().trim().min(1).max(255), + port: z.coerce.number(), + database: z.string().trim().min(1).max(255), + sslEnabled: z.boolean(), + sslRejectUnauthorized: z.boolean(), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() +}); + +// Accounts +export const BaseSqlAccountCredentialsSchema = z.object({ + username: z.string().trim().min(1), + password: z.string().trim().min(1) +}); diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts new file mode 100644 index 000000000..f56a2a3dc --- /dev/null +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts @@ -0,0 +1,7 @@ +import { + TPostgresAccountCredentials, + TPostgresResourceConnectionDetails +} from "../../postgres/postgres-resource-types"; + +export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails; +export type TSqlAccountCredentials = TPostgresAccountCredentials; diff --git a/backend/src/ee/services/pam-session/pam-session-dal.ts b/backend/src/ee/services/pam-session/pam-session-dal.ts new file mode 100644 index 000000000..f8b3a3393 --- /dev/null +++ b/backend/src/ee/services/pam-session/pam-session-dal.ts @@ -0,0 +1,26 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TPamSessionDALFactory = ReturnType; +export const pamSessionDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.PamSession); + + const findById = async (id: string, tx?: Knex) => { + const session = await (tx || db.replicaNode())(TableName.PamSession) + .leftJoin(TableName.PamAccount, `${TableName.PamSession}.accountId`, `${TableName.PamAccount}.id`) + .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .leftJoin(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`) + .select(selectAllTableCols(TableName.PamSession)) + .select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName")) + .select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId")) + .where(`${TableName.PamSession}.id`, id) + .first(); + + return session; + }; + + return { ...orm, findById }; +}; diff --git a/backend/src/ee/services/pam-session/pam-session-enums.ts b/backend/src/ee/services/pam-session/pam-session-enums.ts new file mode 100644 index 000000000..87731f577 --- /dev/null +++ b/backend/src/ee/services/pam-session/pam-session-enums.ts @@ -0,0 +1,6 @@ +export enum PamSessionStatus { + Starting = "starting", // Starting, user connecting to resource + Active = "active", // Active, user is connected to resource + Ended = "ended", // Ended by user + Terminated = "terminated" // Terminated by an admin +} diff --git a/backend/src/ee/services/pam-session/pam-session-fns.ts b/backend/src/ee/services/pam-session/pam-session-fns.ts new file mode 100644 index 000000000..4afe205b5 --- /dev/null +++ b/backend/src/ee/services/pam-session/pam-session-fns.ts @@ -0,0 +1,43 @@ +import { TPamSessions } from "@app/db/schemas"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TPamSanitizedSession, TPamSessionCommandLog } from "./pam-session.types"; + +export const decryptSessionCommandLogs = async ({ + projectId, + encryptedLogs, + kmsService +}: { + projectId: string; + encryptedLogs: Buffer; + kmsService: Pick; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedLogs + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TPamSessionCommandLog; +}; + +export const decryptSession = async ( + session: TPamSessions, + projectId: string, + kmsService: Pick +) => { + return { + ...session, + commandLogs: session.encryptedLogsBlob + ? await decryptSessionCommandLogs({ + projectId, + encryptedLogs: session.encryptedLogsBlob, + kmsService + }) + : [] + } as TPamSanitizedSession; +}; diff --git a/backend/src/ee/services/pam-session/pam-session-schemas.ts b/backend/src/ee/services/pam-session/pam-session-schemas.ts new file mode 100644 index 000000000..2bc1d5345 --- /dev/null +++ b/backend/src/ee/services/pam-session/pam-session-schemas.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +import { PamSessionsSchema } from "@app/db/schemas"; + +export const PamSessionCommandLogSchema = z.object({ + input: z.string(), + output: z.string(), + timestamp: z.coerce.date() +}); + +export const SanitizedSessionSchema = PamSessionsSchema.omit({ + encryptedLogsBlob: true +}).extend({ + commandLogs: PamSessionCommandLogSchema.array() +}); diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts new file mode 100644 index 000000000..713383306 --- /dev/null +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -0,0 +1,190 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { ProjectPermissionPamSessionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TUpdateSessionLogsDTO } from "./pam-session.types"; +import { TPamSessionDALFactory } from "./pam-session-dal"; +import { PamSessionStatus } from "./pam-session-enums"; +import { decryptSession } from "./pam-session-fns"; + +type TPamSessionServiceFactoryDep = { + pamSessionDAL: TPamSessionDALFactory; + projectDAL: TProjectDALFactory; + permissionService: Pick; + licenseService: Pick; + kmsService: Pick; +}; + +export type TPamSessionServiceFactory = ReturnType; + +export const pamSessionServiceFactory = ({ + pamSessionDAL, + projectDAL, + permissionService, + licenseService, + kmsService +}: TPamSessionServiceFactoryDep) => { + const getById = async (sessionId: string, actor: OrgServiceActor) => { + const session = await pamSessionDAL.findById(sessionId); + if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: session.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamSessionActions.Read, + ProjectPermissionSub.PamSessions + ); + + return { + session: await decryptSession(session, session.projectId, kmsService) + }; + }; + + const list = async (projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamSessionActions.Read, + ProjectPermissionSub.PamSessions + ); + + const sessions = await pamSessionDAL.find({ projectId }); + + return { + sessions: await Promise.all(sessions.map((session) => decryptSession(session, projectId, kmsService))) + }; + }; + + const updateLogsById = async ({ sessionId, logs }: TUpdateSessionLogsDTO, actor: OrgServiceActor) => { + const orgLicensePlan = await licenseService.getPlan(actor.orgId); + if (!orgLicensePlan.pam) { + throw new BadRequestError({ + message: "PAM operation failed due to organization plan restrictions." + }); + } + + // To be hit by gateways only + if (actor.type !== ActorType.IDENTITY) { + throw new ForbiddenRequestError({ message: "Only gateways can perform this action" }); + } + + const session = await pamSessionDAL.findById(sessionId); + if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + + if (session.encryptedLogsBlob) { + throw new BadRequestError({ message: "Cannot update logs for sessions with existing logs" }); + } + + const project = await projectDAL.findById(session.projectId); + if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + project.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + + if (session.gatewayIdentityId !== actor.id) { + throw new ForbiddenRequestError({ message: "Identity does not have access to update logs for this session" }); + } + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: session.projectId + }); + + const { cipherTextBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(logs)) + }); + + const updatedSession = await pamSessionDAL.updateById(sessionId, { + encryptedLogsBlob: cipherTextBlob + }); + + return { session: updatedSession, projectId: project.id }; + }; + + const endSessionById = async (sessionId: string, actor: OrgServiceActor) => { + const session = await pamSessionDAL.findById(sessionId); + if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + + const project = await projectDAL.findById(session.projectId); + if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + project.orgId, + actor.authMethod, + actor.orgId + ); + + if (actor.type === ActorType.IDENTITY) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + + if (session.gatewayIdentityId !== actor.id) { + throw new ForbiddenRequestError({ message: "Identity does not have access to end this session" }); + } + } else if (actor.type === ActorType.USER) { + if (session.userId !== actor.id) { + throw new ForbiddenRequestError({ message: "You are not authorized to end this session" }); + } + } else { + throw new ForbiddenRequestError({ message: "Only identities and users can perform this action" }); + } + + if (session.status === PamSessionStatus.Ended) { + return { + session, + projectId: project.id + }; + } + + if (session.status !== PamSessionStatus.Active && session.status !== PamSessionStatus.Starting) { + throw new BadRequestError({ message: "Cannot end sessions that are not active or starting" }); + } + + const updatedSession = await pamSessionDAL.updateById(sessionId, { + endedAt: new Date(), + status: PamSessionStatus.Ended + }); + + return { session: updatedSession, projectId: project.id }; + }; + + return { getById, list, updateLogsById, endSessionById }; +}; diff --git a/backend/src/ee/services/pam-session/pam-session.types.ts b/backend/src/ee/services/pam-session/pam-session.types.ts new file mode 100644 index 000000000..0c87a9fa4 --- /dev/null +++ b/backend/src/ee/services/pam-session/pam-session.types.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "./pam-session-schemas"; + +export type TPamSessionCommandLog = z.infer; +export type TPamSanitizedSession = z.infer; + +// DTOs +export type TUpdateSessionLogsDTO = { + sessionId: string; + logs: TPamSessionCommandLog[]; +}; diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 83d1c44d9..b9cabe022 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -12,6 +12,8 @@ import { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPamAccountActions, + ProjectPermissionPamSessionActions, ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSyncActions, ProjectPermissionPkiTemplateActions, @@ -49,7 +51,9 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificates, ProjectPermissionSub.SshCertificateTemplates, - ProjectPermissionSub.SshHostGroups + ProjectPermissionSub.SshHostGroups, + ProjectPermissionSub.PamFolders, + ProjectPermissionSub.PamResources ].forEach((el) => { can( [ @@ -290,6 +294,19 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.AppConnections ); + can( + [ + ProjectPermissionPamAccountActions.Access, + ProjectPermissionPamAccountActions.Read, + ProjectPermissionPamAccountActions.Create, + ProjectPermissionPamAccountActions.Edit, + ProjectPermissionPamAccountActions.Delete + ], + ProjectPermissionSub.PamAccounts + ); + + can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions); + return rules; }; @@ -518,6 +535,15 @@ const buildMemberPermissionRules = () => { can(ProjectPermissionAppConnectionActions.Connect, ProjectPermissionSub.AppConnections); + can([ProjectPermissionActions.Read], ProjectPermissionSub.PamFolders); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.PamResources); + + can( + [ProjectPermissionPamAccountActions.Access, ProjectPermissionPamAccountActions.Read], + ProjectPermissionSub.PamAccounts + ); + return rules; }; @@ -579,6 +605,12 @@ const buildViewerPermissionRules = () => { ProjectPermissionSub.SecretEvents ); + can([ProjectPermissionActions.Read], ProjectPermissionSub.PamFolders); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.PamResources); + + can([ProjectPermissionPamAccountActions.Read], ProjectPermissionSub.PamAccounts); + return rules; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index d79120256..4c7f1faac 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -186,6 +186,19 @@ export enum ProjectPermissionAuditLogsActions { Read = "read" } +export enum ProjectPermissionPamAccountActions { + Access = "access", + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum ProjectPermissionPamSessionActions { + Read = "read" + // Terminate = "terminate" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -228,7 +241,11 @@ export enum ProjectPermissionSub { SecretScanningFindings = "secret-scanning-findings", SecretScanningConfigs = "secret-scanning-configs", SecretEvents = "secret-events", - AppConnections = "app-connections" + AppConnections = "app-connections", + PamFolders = "pam-folders", + PamResources = "pam-resources", + PamAccounts = "pam-accounts", + PamSessions = "pam-sessions" } export type SecretSubjectFields = { @@ -300,6 +317,12 @@ export type AppConnectionSubjectFields = { connectionId: string; }; +export type PamAccountSubjectFields = { + resourceName: string; + accountName: string; + accountPath: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -404,7 +427,14 @@ export type ProjectPermissionSet = | ProjectPermissionSub.AppConnections | (ForcedSubject & AppConnectionSubjectFields) ) - ]; + ] + | [ProjectPermissionActions, ProjectPermissionSub.PamFolders] + | [ProjectPermissionActions, ProjectPermissionSub.PamResources] + | [ + ProjectPermissionPamAccountActions, + ProjectPermissionSub.PamAccounts | (ForcedSubject & PamAccountSubjectFields) + ] + | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ @@ -427,6 +457,27 @@ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ }) .partial() ]); +const PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; +const PAM_ACCOUNT_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ + z.string().refine((val) => val.startsWith("/"), SECRET_PATH_MISSING_SLASH_ERR_MSG), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ].refine( + (val) => val.startsWith("/"), + PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG + ), + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ].refine( + (val) => val.startsWith("/"), + PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG + ), + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN].refine( + (val) => val.every((el) => el.startsWith("/")), + PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG + ), + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() +]); // akhilmhdh: don't modify this for v2 // if you want to update create a new schema const SecretConditionV1Schema = z @@ -650,6 +701,34 @@ const AppConnectionConditionSchema = z }) .partial(); +const PamAccountConditionSchema = z + .object({ + resourceName: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + accountName: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + accountPath: PAM_ACCOUNT_PATH_PERMISSION_OPERATOR_SCHEMA + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -840,6 +919,34 @@ const GeneralPermissionSchema = [ conditions: AppConnectionConditionSchema.describe( "When specified, only matching conditions will be allowed to access given resource." ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.PamFolders).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.PamResources).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.PamAccounts).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamAccountActions).describe( + "Describe what action an entity can take." + ), + conditions: PamAccountConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.PamSessions).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe( + "Describe what action an entity can take." + ) }) ]; diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 2fef71259..696bd1f4f 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -708,7 +708,8 @@ export const relayServiceFactory = ({ relayPkiClientCaCertificate, relayPkiClientCaPrivateKey, relayPkiServerCaCertificate, - relayPkiServerCaCertificateChain + relayPkiServerCaCertificateChain, + duration }: { gatewayId: string; gatewayName: string; @@ -718,6 +719,7 @@ export const relayServiceFactory = ({ relayPkiClientCaPrivateKey: Buffer; relayPkiServerCaCertificate: Buffer; relayPkiServerCaCertificateChain: Buffer; + duration?: number; }) => { const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); const relayClientCaCert = new x509.X509Certificate(relayPkiClientCaCertificate); @@ -737,7 +739,7 @@ export const relayServiceFactory = ({ ); const clientCertIssuedAt = new Date(); - const clientCertExpiration = new Date(new Date().getTime() + 5 * 60 * 1000); + const clientCertExpiration = new Date(new Date().getTime() + (duration ?? 5 * 60 * 1000)); const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); const clientCertSerialNumber = createSerialNumber(); @@ -866,13 +868,15 @@ export const relayServiceFactory = ({ orgId, orgName, gatewayId, - gatewayName + gatewayName, + duration }: { relayId: string; orgId: string; orgName: string; gatewayId: string; gatewayName: string; + duration?: number; }) => { const relay = await relayDAL.findOne({ id: relayId @@ -896,7 +900,8 @@ export const relayServiceFactory = ({ relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate, relayPkiClientCaPrivateKey: instanceCAs.instanceRelayPkiClientCaPrivateKey, relayPkiServerCaCertificate: instanceCAs.instanceRelayPkiServerCaCertificate, - relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain + relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain, + duration }); return { @@ -914,7 +919,8 @@ export const relayServiceFactory = ({ relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate, relayPkiClientCaPrivateKey: orgCAs.relayPkiClientCaPrivateKey, relayPkiServerCaCertificate: orgCAs.relayPkiServerCaCertificate, - relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain + relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain, + duration }); return { diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 31aaa70aa..f1ee313e8 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,6 +1,17 @@ +/* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; +import RE2 from "re2"; -import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; +import { + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TGroups, + TSamlConfigs, + TSamlConfigsUpdate, + TUsers +} from "@app/db/schemas"; import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; @@ -8,12 +19,16 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; +import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; @@ -22,17 +37,30 @@ import { normalizeUsername } from "@app/services/user/user-fns"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { UserAliasType } from "@app/services/user-alias/user-alias-types"; +import { TGroupDALFactory } from "../group/group-dal"; +import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "../group/group-fns"; +import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TSamlConfigDALFactory } from "./saml-config-dal"; -import { TSamlConfigServiceFactory } from "./saml-config-types"; +import { SamlProviders, TSamlConfigServiceFactory } from "./saml-config-types"; + +// SAML providers that support group sync +const GROUP_SYNC_SUPPORTED_PROVIDERS = [SamlProviders.GOOGLE_SAML] as SamlProviders[]; type TSamlConfigServiceFactoryDep = { samlConfigDAL: Pick; userDAL: Pick< TUserDALFactory, - "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId" + | "create" + | "findOne" + | "find" + | "transaction" + | "updateById" + | "findById" + | "findUserEncKeyByUserId" + | "findUserEncKeyByUserIdsBatch" >; userAliasDAL: Pick; orgDAL: Pick< @@ -41,6 +69,15 @@ type TSamlConfigServiceFactoryDep = { >; identityMetadataDAL: Pick; orgMembershipDAL: Pick; + groupDAL: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + "find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership" + >; + groupProjectDAL: Pick; + projectDAL: Pick; + projectBotDAL: Pick; + projectKeyDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; @@ -54,6 +91,12 @@ export const samlConfigServiceFactory = ({ orgMembershipDAL, userDAL, userAliasDAL, + groupDAL, + userGroupMembershipDAL, + groupProjectDAL, + projectDAL, + projectBotDAL, + projectKeyDAL, permissionService, licenseService, tokenService, @@ -61,6 +104,139 @@ export const samlConfigServiceFactory = ({ identityMetadataDAL, kmsService }: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => { + const parseSamlGroups = (groupsValue: string): string[] => { + let samlGroups: string[] = []; + + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsed = JSON.parse(groupsValue); + if (Array.isArray(parsed)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + samlGroups = parsed; + } else if (typeof parsed === "string") { + samlGroups = parsed + .split(",") + .map((g) => g.trim()) + .filter(Boolean); + } + } catch { + samlGroups = groupsValue + .split(",") + .map((g) => g.trim()) + .filter(Boolean); + } + + return samlGroups; + }; + + const syncUserGroupMemberships = async ({ + userId, + orgId, + samlGroups, + tx + }: { + userId: string; + orgId: string; + samlGroups: string[]; + tx?: Knex; + }) => { + const processGroupSync = async (transaction: Knex) => { + const currentGroupMemberships = await userGroupMembershipDAL.find( + { + userId + }, + { tx: transaction } + ); + + const orgGroups = await groupDAL.find({ orgId }, { tx: transaction }); + const orgGroupsMap = new Map(orgGroups.map((g: TGroups) => [g.name, g])); + const orgGroupIds = new Set(orgGroups.map((g) => g.id)); + + const currentOrgGroupMemberships = currentGroupMemberships.filter((m) => orgGroupIds.has(m.groupId)); + const currentGroupNames = new Set( + currentOrgGroupMemberships + .map((m) => { + const group = orgGroups.find((g) => g.id === m.groupId); + return group?.name; + }) + .filter(Boolean) + ); + + const targetGroupNames = new Set(samlGroups); + const groupsToAdd = samlGroups.filter((groupName) => !currentGroupNames.has(groupName)); + const groupsToRemove = Array.from(currentGroupNames).filter( + (groupName) => groupName && !targetGroupNames.has(groupName) + ); + // eslint-disable-next-line no-await-in-loop + for (const groupName of groupsToAdd) { + if (!orgGroupsMap.has(groupName)) { + const newGroup = await groupDAL.create( + { + name: groupName, + slug: `${groupName.toLowerCase().replace(new RE2("[^a-z0-9]", "g"), "-")}-${Date.now()}`, + orgId, + role: OrgMembershipRole.NoAccess, + roleId: null + }, + transaction + ); + orgGroupsMap.set(groupName, newGroup); + } + } + + // eslint-disable-next-line no-await-in-loop + for (const groupName of groupsToAdd) { + const group = orgGroupsMap.get(groupName); + if (group) { + try { + await addUsersToGroupByUserIds({ + userIds: [userId], + group, + userDAL, + userGroupMembershipDAL, + orgDAL, + groupProjectDAL, + projectKeyDAL, + projectDAL, + projectBotDAL, + tx: transaction + }); + } catch (error) { + // Continue if user already in group + } + } + } + + // eslint-disable-next-line no-await-in-loop + for (const groupName of groupsToRemove) { + if (groupName) { + const group = orgGroupsMap.get(groupName); + if (group) { + try { + await removeUsersFromGroupByUserIds({ + userIds: [userId], + group, + userDAL, + userGroupMembershipDAL, + groupProjectDAL, + projectKeyDAL, + tx: transaction + }); + } catch (error) { + // Continue if user not in group + } + } + } + } + }; + + if (tx) { + await processGroupSync(tx); + } else { + await userDAL.transaction(processGroupSync); + } + }; + const createSamlCfg: TSamlConfigServiceFactory["createSamlCfg"] = async ({ idpCert, actor, @@ -71,7 +247,8 @@ export const samlConfigServiceFactory = ({ actorId, isActive, entryPoint, - authProvider + authProvider, + enableGroupSync }) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); @@ -96,6 +273,18 @@ export const samlConfigServiceFactory = ({ }); } + if (enableGroupSync && !GROUP_SYNC_SUPPORTED_PROVIDERS.includes(authProvider)) { + throw new BadRequestError({ + message: "Group sync is not supported for this SAML provider." + }); + } + + if (enableGroupSync && !plan.groups) { + throw new BadRequestError({ + message: "Failed to enable SAML group sync due to plan restriction. Upgrade plan to enable group sync." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId @@ -107,7 +296,8 @@ export const samlConfigServiceFactory = ({ isActive, encryptedSamlCertificate: encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob, encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob, - encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob + encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob, + enableGroupSync: enableGroupSync || false }); return samlConfig; @@ -123,7 +313,8 @@ export const samlConfigServiceFactory = ({ issuer, isActive, entryPoint, - authProvider + authProvider, + enableGroupSync }) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); @@ -147,7 +338,27 @@ export const samlConfigServiceFactory = ({ }); } - const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; + if (enableGroupSync && authProvider && !GROUP_SYNC_SUPPORTED_PROVIDERS.includes(authProvider)) { + throw new BadRequestError({ + message: "Group sync is not supported for this SAML provider." + }); + } + + if (enableGroupSync && !plan.groups) { + throw new BadRequestError({ + message: "Failed to enable SAML group sync due to plan restriction. Upgrade plan to enable group sync." + }); + } + + const updateQuery: TSamlConfigsUpdate = { + authProvider, + isActive, + lastUsed: null + }; + + if (enableGroupSync !== undefined) { + updateQuery.enableGroupSync = enableGroupSync; + } const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId @@ -250,7 +461,8 @@ export const samlConfigServiceFactory = ({ entryPoint, issuer, cert, - lastUsed: samlConfig.lastUsed + lastUsed: samlConfig.lastUsed, + enableGroupSync: samlConfig.enableGroupSync }; }; @@ -282,6 +494,12 @@ export const samlConfigServiceFactory = ({ const organization = await orgDAL.findOrgById(orgId); if (!organization) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const samlConfig = await samlConfigDAL.findOne({ orgId }); + const groupsMetadata = metadata?.find(({ key }) => key === "groups"); + + const plan = await licenseService.getPlan(orgId); + const shouldSyncGroups = !!samlConfig?.enableGroupSync && !!plan.groups; + let user: TUsers; if (userAlias) { user = await userDAL.transaction(async (tx) => { @@ -303,7 +521,7 @@ export const samlConfigServiceFactory = ({ orgId, role, roleId, - status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, isActive: true }, tx @@ -334,6 +552,17 @@ export const samlConfigServiceFactory = ({ } } + if (shouldSyncGroups && metadata && foundUser.id) { + const samlGroups = groupsMetadata?.value ? parseSamlGroups(groupsMetadata.value) : []; + + await syncUserGroupMemberships({ + userId: foundUser.id, + orgId, + samlGroups, + tx + }); + } + return foundUser; }); } else { @@ -425,6 +654,18 @@ export const samlConfigServiceFactory = ({ ); } } + + if (shouldSyncGroups && metadata && newUser.id) { + const samlGroups = groupsMetadata?.value ? parseSamlGroups(groupsMetadata.value) : []; + + await syncUserGroupMemberships({ + userId: newUser.id, + orgId, + samlGroups, + tx + }); + } + return newUser; }); } diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index f4ede04fa..bdf65b988 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -17,6 +17,7 @@ export type TCreateSamlCfgDTO = { entryPoint: string; issuer: string; idpCert: string; + enableGroupSync?: boolean; } & TOrgPermission; export type TUpdateSamlCfgDTO = Partial<{ @@ -25,6 +26,7 @@ export type TUpdateSamlCfgDTO = Partial<{ entryPoint: string; issuer: string; idpCert: string; + enableGroupSync?: boolean; }> & TOrgPermission; @@ -71,6 +73,7 @@ export type TSamlConfigServiceFactory = { issuer: string; cert: string; lastUsed: Date | null | undefined; + enableGroupSync?: boolean; }>; samlLogin: (arg: TSamlLoginDTO) => Promise<{ isUserCompleted: boolean; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 17f4aa493..3155fe05c 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -23,6 +23,7 @@ export const PgSqlLock = { InstanceRelayConfigInit: () => pgAdvisoryLockHashText("instance-relay-config-init"), OrgGatewayV2Init: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-v2-init:${orgId}`), OrgRelayConfigInit: (orgId: string) => pgAdvisoryLockHashText(`org-relay-config-init:${orgId}`), + GatewayPamSessionKey: (gatewayId: string) => pgAdvisoryLockHashText(`gateway-pam-session-key:${gatewayId}`), IdentityLogin: (identityId: string, nonce: string) => pgAdvisoryLockHashText(`identity-login:${identityId}:${nonce}`) } as const; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 2ac8f996e..2d0de59a2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2872,7 +2872,9 @@ export const SamlSso = { entryPoint: "The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.", issuer: "The SAML provider issuer URL or entity ID.", - cert: "The certificate to use for SAML authentication." + cert: "The certificate to use for SAML authentication.", + enableGroupSync: + "Whether to enable automatic synchronization of group memberships from the SAML provider to Infisical groups." }, CREATE_CONFIG: { organizationId: "The ID of the organization to create the SAML config for.", @@ -2881,7 +2883,9 @@ export const SamlSso = { entryPoint: "The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.", issuer: "The SAML provider issuer URL or entity ID.", - cert: "The certificate to use for SAML authentication." + cert: "The certificate to use for SAML authentication.", + enableGroupSync: + "Whether to enable automatic synchronization of group memberships from the SAML provider to Infisical groups." } }; diff --git a/backend/src/lib/fn/object.ts b/backend/src/lib/fn/object.ts index 65d0b7859..6ff7278aa 100644 --- a/backend/src/lib/fn/object.ts +++ b/backend/src/lib/fn/object.ts @@ -53,3 +53,53 @@ export const titleCaseToCamelCase = (obj: unknown): unknown => { return result; }; + +export const deepEqual = (obj1: unknown, obj2: unknown): boolean => { + if (obj1 === obj2) return true; + + if (obj1 === null || obj2 === null || obj1 === undefined || obj2 === undefined) { + return obj1 === obj2; + } + + if (typeof obj1 !== typeof obj2) return false; + + if (typeof obj1 !== "object") return obj1 === obj2; + + if (Array.isArray(obj1) !== Array.isArray(obj2)) return false; + + if (Array.isArray(obj1)) { + const arr1 = obj1 as unknown[]; + const arr2 = obj2 as unknown[]; + if (arr1.length !== arr2.length) return false; + return arr1.every((val, idx) => deepEqual(val, arr2[idx])); + } + + const keys1 = Object.keys(obj1 as Record).sort(); + const keys2 = Object.keys(obj2 as Record).sort(); + + if (keys1.length !== keys2.length) return false; + if (keys1.some((key, idx) => key !== keys2[idx])) return false; + + return keys1.every((key) => + deepEqual((obj1 as Record)[key], (obj2 as Record)[key]) + ); +}; + +export const deepEqualSkipFields = (obj1: unknown, obj2: unknown, skipFields: string[] = []): boolean => { + if (skipFields.length === 0) { + return deepEqual(obj1, obj2); + } + + if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null) { + return deepEqual(obj1, obj2); + } + + const filtered1 = Object.fromEntries( + Object.entries(obj1 as Record).filter(([key]) => !skipFields.includes(key)) + ); + const filtered2 = Object.fromEntries( + Object.entries(obj2 as Record).filter(([key]) => !skipFields.includes(key)) + ); + + return deepEqual(filtered1, filtered2); +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f6c70884c..406daa63d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -66,6 +66,14 @@ import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseServiceFactory } from "@app/ee/services/license/license-service"; import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; +import { pamAccountDALFactory } from "@app/ee/services/pam-account/pam-account-dal"; +import { pamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service"; +import { pamFolderDALFactory } from "@app/ee/services/pam-folder/pam-folder-dal"; +import { pamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service"; +import { pamResourceDALFactory } from "@app/ee/services/pam-resource/pam-resource-dal"; +import { pamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service"; +import { pamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal"; +import { pamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service"; import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; @@ -623,6 +631,12 @@ export const registerRoutes = async ( userDAL, userAliasDAL, samlConfigDAL, + groupDAL, + userGroupMembershipDAL, + groupProjectDAL, + projectDAL, + projectBotDAL, + projectKeyDAL, licenseService, tokenService, smtpService, @@ -2104,6 +2118,46 @@ export const registerRoutes = async ( appConnectionDAL }); + const pamFolderDAL = pamFolderDALFactory(db); + const pamResourceDAL = pamResourceDALFactory(db); + const pamAccountDAL = pamAccountDALFactory(db); + const pamSessionDAL = pamSessionDALFactory(db); + + const pamFolderService = pamFolderServiceFactory({ + pamFolderDAL, + permissionService, + licenseService + }); + + const pamResourceService = pamResourceServiceFactory({ + pamResourceDAL, + permissionService, + licenseService, + kmsService, + gatewayV2Service + }); + + const pamAccountService = pamAccountServiceFactory({ + pamAccountDAL, + gatewayV2Service, + kmsService, + licenseService, + pamFolderDAL, + pamResourceDAL, + pamSessionDAL, + permissionService, + projectDAL, + userDAL + }); + + const pamSessionService = pamSessionServiceFactory({ + pamSessionDAL, + projectDAL, + permissionService, + licenseService, + kmsService + }); + // setup the communication with license key server await licenseService.init(); @@ -2242,6 +2296,10 @@ export const registerRoutes = async ( bus: eventBusService, sse: sseService, notification: notificationService, + pamFolder: pamFolderService, + pamResource: pamResourceService, + pamAccount: pamAccountService, + pamSession: pamSessionService, upgradePath: upgradePathService }); diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 84b1442f6..89865b1a1 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -58,8 +58,8 @@ import { registerSecretRequestsRouter } from "./secret-requests-router"; import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSlackRouter } from "./slack-router"; -import { registerUpgradePathRouter } from "./upgrade-path-router"; import { registerSsoRouter } from "./sso-router"; +import { registerUpgradePathRouter } from "./upgrade-path-router"; import { registerUserActionRouter } from "./user-action-router"; import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; diff --git a/backend/src/server/routes/v1/pki-sync-routers/aws-certificate-manager-pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/aws-certificate-manager-pki-sync-router.ts new file mode 100644 index 000000000..21bfadbac --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/aws-certificate-manager-pki-sync-router.ts @@ -0,0 +1,22 @@ +import { + AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION, + AwsCertificateManagerPkiSyncSchema, + CreateAwsCertificateManagerPkiSyncSchema, + UpdateAwsCertificateManagerPkiSyncSchema +} from "@app/services/pki-sync/aws-certificate-manager"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +import { registerSyncPkiEndpoints } from "./pki-sync-endpoints"; + +export const registerAwsCertificateManagerPkiSyncRouter = async (server: FastifyZodProvider) => + registerSyncPkiEndpoints({ + destination: PkiSync.AwsCertificateManager, + server, + responseSchema: AwsCertificateManagerPkiSyncSchema, + createSchema: CreateAwsCertificateManagerPkiSyncSchema, + updateSchema: UpdateAwsCertificateManagerPkiSyncSchema, + syncOptions: { + canImportCertificates: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION.canImportCertificates, + canRemoveCertificates: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION.canRemoveCertificates + } + }); diff --git a/backend/src/server/routes/v1/pki-sync-routers/index.ts b/backend/src/server/routes/v1/pki-sync-routers/index.ts index 326b650e9..4b81db27f 100644 --- a/backend/src/server/routes/v1/pki-sync-routers/index.ts +++ b/backend/src/server/routes/v1/pki-sync-routers/index.ts @@ -1,9 +1,11 @@ import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; +import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router"; import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router"; export * from "./pki-sync-router"; export const PKI_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { - [PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter + [PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter, + [PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts index 9db3a2013..9da2f4b69 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -425,4 +425,42 @@ export const registerSyncSecretsEndpoints = { + const { destinationConfig, excludeSyncId, projectId } = req.body; + + const result = await server.services.secretSync.checkDuplicateDestination( + { + destinationConfig: destinationConfig as Record, + destination, + excludeSyncId, + projectId + }, + req.permission + ); + + return result; + } + }); }; diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index a86fa7420..73abef78d 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -215,6 +215,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { return false; case ProjectType.SSH: return false; + case ProjectType.PAM: + return false; default: return true; } diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts index fe6724251..552eed307 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts @@ -33,6 +33,7 @@ export const constructGroupOrgMembershipRoleMappings = async ({ let customRolesMap: Map = new Map(); if (customRoleSlugs.length > 0) { const customRoles = await orgRoleDAL.find({ + orgId, $in: { slug: customRoleSlugs } diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-constants.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-constants.ts new file mode 100644 index 000000000..265e00cac --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-constants.ts @@ -0,0 +1,52 @@ +import RE2 from "re2"; + +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +/** + * AWS Certificate Manager naming constraints for certificates + */ +export const AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING = { + /** + * Regular expression pattern for valid AWS Certificate Manager certificate names + * Must contain only alphanumeric characters, spaces, hyphens, and underscores + * Must be 1-256 characters long + */ + NAME_PATTERN: new RE2("^[a-zA-Z0-9\\s\\-_]{1,256}$"), + + /** + * String of characters that are forbidden in AWS Certificate Manager certificate names + */ + FORBIDDEN_CHARACTERS: "!@#$%^&*()+={}[]|\\:;\"'<>,.?/~`", + + /** + * Maximum length for certificate names in AWS Certificate Manager + */ + MAX_LENGTH: 256, + + /** + * Minimum length for certificate names in AWS Certificate Manager + */ + MIN_LENGTH: 1, + + /** + * String representation of the allowed character pattern (for UI display) + */ + ALLOWED_CHARACTER_PATTERN: "^[a-zA-Z0-9\\s\\-_]{1,256}$" +} as const; + +/** + * AWS Certificate Manager PKI Sync list option configuration + */ +export const AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION = { + name: "AWS Certificate Manager" as const, + connection: AppConnection.AWS, + destination: PkiSync.AwsCertificateManager, + canImportCertificates: false, + canRemoveCertificates: true, + defaultCertificateNameSchema: "Infisical-{{certificateId}}", + forbiddenCharacters: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS, + allowedCharacterPattern: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN, + maxCertificateNameLength: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MAX_LENGTH, + minCertificateNameLength: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MIN_LENGTH +} as const; diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts new file mode 100644 index 000000000..f78bd790e --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts @@ -0,0 +1,634 @@ +/* eslint-disable no-await-in-loop */ +import * as AWS from "aws-sdk"; +import RE2 from "re2"; +import { z } from "zod"; + +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { AwsConnectionMethod } from "@app/services/app-connection/aws/aws-connection-enums"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { + AwsConnectionAccessTokenCredentialsSchema, + AwsConnectionAssumeRoleCredentialsSchema +} from "@app/services/app-connection/aws/aws-connection-schemas"; +import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; +import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types"; + +import { PkiSyncError } from "../pki-sync-errors"; +import { TPkiSyncWithCredentials } from "../pki-sync-types"; +import { + ACMCertificateWithKey, + CertificateImportRequest, + RemoveCertificatesResult, + SyncCertificatesResult, + TAwsCertificateManagerPkiSyncConfig +} from "./aws-certificate-manager-pki-sync-types"; + +const INFISICAL_CERTIFICATE_TAG = "InfisicalCertificate"; +const AWS_CERTIFICATE_ARN_PATTERN = new RE2("^arn:aws:acm:[a-z0-9-]+:\\d{12}:certificate/[a-f0-9-]{36}$"); + +type TAwsAssumeRoleCredentials = z.infer; +type TAwsAccessKeyCredentials = z.infer; + +const AWS_RATE_LIMIT_CONFIG: RateLimitConfig = { + MAX_CONCURRENT_REQUESTS: 10, + BASE_DELAY: 1000, + MAX_DELAY: 30000, + MAX_RETRIES: 3, + RATE_LIMIT_STATUS_CODES: [429, 503] +}; + +const awsConnectionQueue = createConnectionQueue(AWS_RATE_LIMIT_CONFIG); + +const { withRateLimitRetry, executeWithConcurrencyLimit } = awsConnectionQueue; + +const validateCertificateArn = (arn: string): boolean => { + return AWS_CERTIFICATE_ARN_PATTERN.test(arn); +}; + +const extractCertificateNameFromArn = (certificateArn: string): string => { + if (!validateCertificateArn(certificateArn)) { + throw new Error(`Invalid AWS Certificate Manager ARN format: ${certificateArn}`); + } + const parts = certificateArn.split("/"); + return parts[parts.length - 1]; +}; + +const sanitizeInput = (input: string): string => { + return input.trim().replace(new RE2("[^\\w\\s-]", "g"), ""); +}; + +const validateCertificateContent = (cert: string, privateKey: string): void => { + if (!cert || cert.trim().length === 0) { + throw new Error("Certificate content is empty or missing"); + } + + if (!privateKey || privateKey.trim().length === 0) { + throw new Error("Private key content is empty or missing"); + } + + if (!cert.includes("-----BEGIN CERTIFICATE-----") || !cert.includes("-----END CERTIFICATE-----")) { + throw new Error("Certificate is not in valid PEM format"); + } + + if (!privateKey.includes("-----BEGIN") || !privateKey.includes("-----END")) { + throw new Error("Private key is not in valid PEM format"); + } +}; + +const isAwsIssuedCertificate = (certificate: AWS.ACM.CertificateSummary): boolean => { + return certificate.Type === "AMAZON_ISSUED"; +}; + +const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): boolean => { + return isAwsIssuedCertificate(certificate); +}; + +const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => { + if (!tags || !Array.isArray(tags)) { + return undefined; + } + return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value); +}; + +const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => { + return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG); +}; + +const validateCertificateIdentification = ( + certName: string, + existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string } +): boolean => { + if (!existingCert?.arn || !existingCert?.Tags) { + return false; + } + + const certNameTag = findInfisicalCertificateTag(existingCert.Tags); + + if (!certNameTag || !certNameTag.Value) { + return false; + } + + return certNameTag.Value === certName; +}; + +type TAwsCertificateManagerPkiSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const validateCertificateNameSchema = (schema: string): void => { + if (!schema.includes("{{certificateId}}")) { + throw new Error( + "Certificate name schema must include {{certificateId}} placeholder for proper certificate identification" + ); + } +}; + +const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithCredentials): string => { + if (!certificateName || typeof certificateName !== "string") { + throw new Error("Certificate name must be a non-empty string"); + } + + const sanitizedCertificateName = sanitizeInput(certificateName); + const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; + const certificateNameSchema = syncOptions?.certificateNameSchema; + + if (certificateNameSchema) { + validateCertificateNameSchema(certificateNameSchema); + + let certificateId: string; + + if (sanitizedCertificateName.startsWith("Infisical-")) { + certificateId = sanitizedCertificateName.substring("Infisical-".length); + } else { + certificateId = sanitizedCertificateName; + } + + if (!certificateId || certificateId.trim().length === 0) { + throw new Error(`Certificate ID cannot be empty after processing certificate name: ${certificateName}`); + } + + const environment = "global"; + const generatedName = certificateNameSchema + .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), certificateId) + .replace(new RE2("\\{\\{environment\\}\\}", "g"), environment); + + if (generatedName.length > 256 || generatedName.length < 1) { + throw new Error( + `Generated certificate name length (${generatedName.length}) must be between 1 and 256 characters` + ); + } + + if (generatedName.includes("{{certificateId}}")) { + throw new Error("Certificate name schema failed to properly replace {{certificateId}} placeholder"); + } + + return generatedName; + } + + return sanitizedCertificateName; +}; + +const getAwsAcmClient = async ( + connectionId: string, + region: AWSRegion, + appConnectionDAL: Pick, + kmsService: Pick +): Promise => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AWS) { + throw new BadRequestError({ + message: `Connection '${connectionId}' is not an AWS connection (found: ${appConnection.app})` + }); + } + + const decryptedCredentials = await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId + }); + + let awsConnectionConfig: TAwsConnectionConfig; + switch (appConnection.method) { + case AwsConnectionMethod.AssumeRole: + awsConnectionConfig = { + app: AppConnection.AWS, + method: AwsConnectionMethod.AssumeRole, + credentials: decryptedCredentials as TAwsAssumeRoleCredentials, + orgId: appConnection.orgId + }; + break; + case AwsConnectionMethod.AccessKey: + awsConnectionConfig = { + app: AppConnection.AWS, + method: AwsConnectionMethod.AccessKey, + credentials: decryptedCredentials as TAwsAccessKeyCredentials, + orgId: appConnection.orgId + }; + break; + default: + throw new BadRequestError({ + message: `Unsupported AWS connection method: ${appConnection.method}` + }); + } + + const awsConfig = await getAwsConnectionConfig(awsConnectionConfig, region); + + return new AWS.ACM(awsConfig); +}; + +export const awsCertificateManagerPkiSyncFactory = ({ + kmsService, + appConnectionDAL +}: TAwsCertificateManagerPkiSyncFactoryDeps) => { + const deleteCertificateFromAcm = async ( + acm: AWS.ACM, + certificateArn: string, + operation: string, + syncId: string, + throwOnError = false + ): Promise<{ arn: string; success: boolean; error?: Error }> => { + try { + await withRateLimitRetry(() => acm.deleteCertificate({ CertificateArn: certificateArn }).promise(), { + operation, + syncId + }); + return { arn: certificateArn, success: true }; + } catch (error) { + const errorObj = error instanceof Error ? error : new Error("Unknown error"); + + if (throwOnError) { + throw new PkiSyncError({ + message: `Failed to remove certificate from AWS Certificate Manager: ${errorObj.message}`, + cause: errorObj, + context: { + certificateArn, + operation + } + }); + } + + return { + arn: certificateArn, + success: false, + error: errorObj + }; + } + }; + const $getAwsAcmCertificates = async ( + acm: AWS.ACM, + syncId = "unknown" + ): Promise<{ + acmCertificates: Record< + string, + { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } + >; + }> => { + const paginateAwsAcmCertificates = async () => { + const certificates: AWS.ACM.CertificateSummary[] = []; + let nextToken: string | undefined; + + do { + const listParams: AWS.ACM.ListCertificatesRequest = { + CertificateStatuses: ["ISSUED"], + NextToken: nextToken, + MaxItems: 100 + }; + + const response = await withRateLimitRetry(() => acm.listCertificates(listParams).promise(), { + operation: "list-certificates", + syncId + }); + + if (response.CertificateSummaryList) { + certificates.push(...response.CertificateSummaryList); + } + nextToken = response.NextToken; + } while (nextToken); + + return certificates; + }; + + const certificateSummaries = await paginateAwsAcmCertificates(); + + const certificateResults = await executeWithConcurrencyLimit( + certificateSummaries, + async (certSummary) => { + if (!certSummary.CertificateArn) { + throw new Error("Certificate ARN is missing"); + } + + const [certificateDetails, tagsResponse] = await Promise.all([ + acm.describeCertificate({ CertificateArn: certSummary.CertificateArn }).promise(), + acm.listTagsForCertificate({ CertificateArn: certSummary.CertificateArn }).promise() + ]); + + let certificateContent: AWS.ACM.GetCertificateResponse | undefined; + if (!shouldSkipCertificateExport(certSummary)) { + try { + certificateContent = await acm.getCertificate({ CertificateArn: certSummary.CertificateArn }).promise(); + } catch (error) { + // Certificate content cannot be imported + } + } + + return { + ...certificateDetails.Certificate, + Tags: tagsResponse.Tags, + key: extractCertificateNameFromArn(certSummary.CertificateArn), + cert: certificateContent?.Certificate || "", + certificateChain: certificateContent?.CertificateChain || "", + privateKey: "", // Private keys cannot be exported from ACM + arn: certSummary.CertificateArn + }; + }, + { operation: "fetch-certificate-details", syncId } + ); + + const successfulCertificates: ACMCertificateWithKey[] = []; + certificateResults.forEach((result) => { + if (result.status === "fulfilled") { + successfulCertificates.push(result.value as ACMCertificateWithKey); + } + }); + + const failedFetches = certificateResults.filter((result) => result.status === "rejected"); + if (failedFetches.length > 0) { + throw new PkiSyncError({ + message: `Failed to fetch ${failedFetches.length} certificate details from AWS Certificate Manager`, + shouldRetry: true, + context: { + failedCount: failedFetches.length, + totalCount: certificateSummaries.length + } + }); + } + + const res: Record< + string, + { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } + > = successfulCertificates.reduce( + (obj, certificate) => ({ + ...obj, + [certificate.key]: { + cert: certificate.cert, + privateKey: certificate.privateKey, + certificateChain: certificate.certificateChain, + arn: certificate.CertificateArn, + Tags: certificate.Tags + } + }), + {} as Record< + string, + { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } + > + ); + + return { + acmCertificates: res + }; + }; + + const syncCertificates = async ( + pkiSync: TPkiSyncWithCredentials, + certificateMap: TCertificateMap + ): Promise => { + const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig; + const acm = await getAwsAcmClient( + pkiSync.connection.id, + destinationConfig.region as AWSRegion, + appConnectionDAL, + kmsService + ); + + const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id); + + const setCertificates: CertificateImportRequest[] = []; + + const activeCertificateNames = Object.keys(certificateMap); + + Object.entries(certificateMap).forEach(([certName, certData]) => { + const { cert, privateKey, certificateChain } = certData; + const certificateName = generateCertificateName(certName, pkiSync); + + const existingCert = Object.values(acmCertificates).find((acmCert) => + validateCertificateIdentification(certName, acmCert) + ); + + const shouldUpdateCert = !existingCert || existingCert.cert !== cert; + + try { + validateCertificateContent(cert, privateKey); + } catch (validationError) { + throw new PkiSyncError({ + message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`, + shouldRetry: false, + context: { + certificateName, + certName + } + }); + } + + if (shouldUpdateCert) { + setCertificates.push({ + key: certName, + name: certificateName, + cert, + privateKey, + certificateChain, + existingArn: existingCert?.arn + }); + } + }); + + // Identify expired/removed certificates that need to be cleaned up from ACM + const certificatesToRemove = Object.values(acmCertificates) + .filter((acmCert) => { + if (!acmCert.arn || !acmCert.Tags) { + return false; + } + + const certNameTag = findInfisicalCertificateTag(acmCert.Tags); + if (!certNameTag || !certNameTag.Value) { + return false; + } + + const isActive = activeCertificateNames.includes(certNameTag.Value); + return !isActive; + }) + .map((acmCert) => acmCert.arn!) + .filter((arn) => arn); + + const uploadResults = await executeWithConcurrencyLimit( + setCertificates, + async ({ key, name, cert, privateKey, certificateChain, existingArn }) => { + try { + const importParams: AWS.ACM.ImportCertificateRequest = { + Certificate: cert, + PrivateKey: privateKey, + Tags: [ + { + Key: INFISICAL_CERTIFICATE_TAG, + Value: key + } + ] + }; + + if (certificateChain && certificateChain.trim().length > 0) { + importParams.CertificateChain = certificateChain; + } + if (existingArn) { + importParams.CertificateArn = existingArn; + } + + const response = await withRateLimitRetry(() => acm.importCertificate(importParams).promise(), { + operation: "import-certificate", + syncId: pkiSync.id + }); + + return { key, name, success: true, response }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + throw new PkiSyncError({ + message: `Failed to import certificate ${key} to AWS Certificate Manager: ${errorMessage}`, + cause: error instanceof Error ? error : new Error(errorMessage), + context: { + certificateKey: key, + certificateName: name, + region: destinationConfig.region + } + }); + } + }, + { operation: "import-certificates", syncId: pkiSync.id } + ); + + const results = uploadResults; + const failedUploads = results.filter((result) => result.status === "rejected"); + const successfulUploads = results.filter((result) => result.status === "fulfilled"); + + let removedCertificates = 0; + let failedRemovals = 0; + let removeResults: PromiseSettledResult<{ arn: string; success: boolean; error?: Error }>[] = []; + + if (certificatesToRemove.length > 0) { + removeResults = await executeWithConcurrencyLimit( + certificatesToRemove, + async (certificateArn) => deleteCertificateFromAcm(acm, certificateArn, "delete-certificate", pkiSync.id), + { operation: "remove-certificates", syncId: pkiSync.id } + ); + + const successfulRemovals = removeResults.filter( + (result) => result.status === "fulfilled" && result.value.success + ); + removedCertificates = successfulRemovals.length; + failedRemovals = removeResults.length - removedCertificates; + } + + const details: { + failedUploads?: Array<{ name: string; error: string }>; + failedRemovals?: Array<{ name: string; error: string }>; + } = {}; + + if (failedUploads.length > 0) { + details.failedUploads = failedUploads.map((failure, index) => { + const certificateName = setCertificates[index]?.name || "unknown"; + let errorMessage = "Unknown error"; + + if (failure.status === "rejected") { + errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error"; + } + + return { + name: certificateName, + error: errorMessage + }; + }); + } + + if (failedRemovals > 0 && removeResults.length > 0) { + const actualFailedRemovals = removeResults + .map((result, index) => { + if (result.status === "rejected") { + const arn = certificatesToRemove[index] || "unknown"; + const errorMessage = result.reason instanceof Error ? result.reason.message : "Unknown error"; + return { + name: arn.includes("certificate/") ? extractCertificateNameFromArn(arn) : arn, + error: errorMessage + }; + } + return null; + }) + .filter((item): item is { name: string; error: string } => item !== null); + + details.failedRemovals = actualFailedRemovals; + } + + return { + uploaded: successfulUploads.length, + removed: removedCertificates, + failedRemovals, + skipped: Object.keys(certificateMap).length - setCertificates.length, + details: Object.keys(details).length > 0 ? details : undefined + }; + }; + + const removeCertificates = async ( + pkiSync: TPkiSyncWithCredentials, + certificateNames: string[] + ): Promise => { + const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig; + const acm = await getAwsAcmClient( + pkiSync.connection.id, + destinationConfig.region as AWSRegion, + appConnectionDAL, + kmsService + ); + + const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id); + + const certificateArnsToRemove: string[] = []; + + for (const certName of certificateNames) { + const matchingCerts = Object.values(acmCertificates).filter((acmCert) => + validateCertificateIdentification(certName, acmCert) + ); + + for (const acmCert of matchingCerts) { + if (acmCert.arn) { + certificateArnsToRemove.push(acmCert.arn); + } + } + } + + const results = await executeWithConcurrencyLimit( + certificateArnsToRemove, + async (certificateArn) => + deleteCertificateFromAcm(acm, certificateArn, "delete-specific-certificate", pkiSync.id, true), + { operation: "remove-specific-certificates", syncId: pkiSync.id } + ); + + const failedRemovals = results.filter((result) => result.status === "rejected"); + + if (failedRemovals.length > 0) { + const failedReasons = failedRemovals.map((failure) => { + if (failure.status === "rejected") { + return failure.reason instanceof Error ? failure.reason.message : "Unknown error"; + } + return "Unknown error"; + }); + + throw new PkiSyncError({ + message: `Failed to remove ${failedRemovals.length} certificate(s) from AWS Certificate Manager`, + context: { + failedReasons, + totalCertificates: certificateArnsToRemove.length, + failedCount: failedRemovals.length + } + }); + } + + return { + removed: certificateArnsToRemove.length - failedRemovals.length, + failed: failedRemovals.length, + skipped: certificateNames.length - certificateArnsToRemove.length + }; + }; + + return { + syncCertificates, + removeCertificates + }; +}; diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-schemas.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-schemas.ts new file mode 100644 index 000000000..eb9ae5444 --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-schemas.ts @@ -0,0 +1,84 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; +import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas"; + +import { AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING } from "./aws-certificate-manager-pki-sync-constants"; + +export const AwsCertificateManagerPkiSyncConfigSchema = z.object({ + region: z.nativeEnum(AWSRegion) +}); + +const AwsCertificateManagerPkiSyncOptionsSchema = z.object({ + canImportCertificates: z.boolean().default(false), + canRemoveCertificates: z.boolean().default(true), + certificateNameSchema: z + .string() + .optional() + .refine( + (schema) => { + if (!schema) return true; + + // Validate that {{certificateId}} placeholder is present + if (!schema.includes("{{certificateId}}")) { + return false; + } + + const testName = schema + .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id") + .replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env"); + + const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some( + (char) => testName.includes(char) + ); + + return ( + AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) && + !hasForbiddenChars && + testName.length >= AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MIN_LENGTH && + testName.length <= AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MAX_LENGTH + ); + }, + { + message: + "Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager" + } + ) +}); + +export const AwsCertificateManagerPkiSyncSchema = PkiSyncSchema.extend({ + destination: z.literal(PkiSync.AwsCertificateManager), + destinationConfig: AwsCertificateManagerPkiSyncConfigSchema, + syncOptions: AwsCertificateManagerPkiSyncOptionsSchema +}); + +export const CreateAwsCertificateManagerPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().default(true), + destinationConfig: AwsCertificateManagerPkiSyncConfigSchema, + syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}), + subscriberId: z.string().optional(), + connectionId: z.string(), + projectId: z.string().trim().min(1) +}); + +export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64).optional(), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().optional(), + destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(), + syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(), + subscriberId: z.string().optional(), + connectionId: z.string().optional() +}); + +export const AwsCertificateManagerPkiSyncListItemSchema = z.object({ + name: z.literal("AWS Certificate Manager"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(PkiSync.AwsCertificateManager), + canImportCertificates: z.literal(false), + canRemoveCertificates: z.literal(true) +}); diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts new file mode 100644 index 000000000..717e86438 --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts @@ -0,0 +1,58 @@ +import * as AWS from "aws-sdk"; +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; + +import { + AwsCertificateManagerPkiSyncConfigSchema, + AwsCertificateManagerPkiSyncSchema, + CreateAwsCertificateManagerPkiSyncSchema, + UpdateAwsCertificateManagerPkiSyncSchema +} from "./aws-certificate-manager-pki-sync-schemas"; + +export type TAwsCertificateManagerPkiSyncConfig = z.infer; + +export type TAwsCertificateManagerPkiSync = z.infer; + +export type TAwsCertificateManagerPkiSyncInput = z.infer; + +export type TAwsCertificateManagerPkiSyncUpdate = z.infer; + +export type TAwsCertificateManagerPkiSyncWithCredentials = TAwsCertificateManagerPkiSync & { + connection: TAwsConnection; +}; + +export interface ACMCertificateWithKey extends AWS.ACM.CertificateDetail { + Tags?: AWS.ACM.TagList; + key: string; + cert: string; + certificateChain: string; + privateKey: string; + arn?: string; +} + +export interface SyncCertificatesResult { + uploaded: number; + removed: number; + failedRemovals: number; + skipped: number; + details?: { + failedUploads?: Array<{ name: string; error: string }>; + failedRemovals?: Array<{ name: string; error: string }>; + }; +} + +export interface RemoveCertificatesResult { + removed: number; + failed: number; + skipped: number; +} + +export interface CertificateImportRequest { + key: string; + name: string; + cert: string; + privateKey: string; + certificateChain?: string; + existingArn?: string; +} diff --git a/backend/src/services/pki-sync/aws-certificate-manager/index.ts b/backend/src/services/pki-sync/aws-certificate-manager/index.ts new file mode 100644 index 000000000..fb30b5c71 --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-certificate-manager-pki-sync-constants"; +export * from "./aws-certificate-manager-pki-sync-fns"; +export * from "./aws-certificate-manager-pki-sync-schemas"; +export * from "./aws-certificate-manager-pki-sync-types"; diff --git a/backend/src/services/pki-sync/pki-sync-enums.ts b/backend/src/services/pki-sync/pki-sync-enums.ts index fadd70914..46a7fc975 100644 --- a/backend/src/services/pki-sync/pki-sync-enums.ts +++ b/backend/src/services/pki-sync/pki-sync-enums.ts @@ -1,5 +1,6 @@ export enum PkiSync { - AzureKeyVault = "azure-key-vault" + AzureKeyVault = "azure-key-vault", + AwsCertificateManager = "aws-certificate-manager" } export enum PkiSyncStatus { diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index 343afa626..75f312fff 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -6,6 +6,8 @@ import { BadRequestError } from "@app/lib/errors"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants"; +import { awsCertificateManagerPkiSyncFactory } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-fns"; import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-constants"; import { azureKeyVaultPkiSyncFactory } from "./azure-key-vault/azure-key-vault-pki-sync-fns"; import { PkiSync } from "./pki-sync-enums"; @@ -14,7 +16,8 @@ import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types"; const ENTERPRISE_PKI_SYNCS: PkiSync[] = []; const PKI_SYNC_LIST_OPTIONS = { - [PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION + [PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION, + [PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION }; export const enterprisePkiSyncCheck = async ( @@ -144,8 +147,10 @@ export const matchesCertificateNameSchema = (name: string, environment: string, return name.startsWith(prefix) && name.endsWith(suffix); }; -const isAzureKeyVaultPkiSync = (pkiSync: TPkiSyncWithCredentials): boolean => { - return pkiSync.destination === PkiSync.AzureKeyVault; +const checkPkiSyncDestination = (pkiSync: TPkiSyncWithCredentials, destination: PkiSync): void => { + if (pkiSync.destination !== destination) { + throw new Error(`Invalid PKI sync destination: ${pkiSync.destination}`); + } }; export const PkiSyncFns = { @@ -163,6 +168,11 @@ export const PkiSyncFns = { "Azure Key Vault does not support importing certificates into Infisical (private keys cannot be extracted)" ); } + case PkiSync.AwsCertificateManager: { + throw new Error( + "AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)" + ); + } default: throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); } @@ -188,12 +198,15 @@ export const PkiSyncFns = { }> => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { - if (!isAzureKeyVaultPkiSync(pkiSync)) { - throw new Error("Invalid Azure Key Vault PKI sync configuration"); - } + checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap); } + case PkiSync.AwsCertificateManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); + const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); + return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap); + } default: throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); } @@ -209,13 +222,17 @@ export const PkiSyncFns = { ): Promise => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { - if (!isAzureKeyVaultPkiSync(pkiSync)) { - throw new Error("Invalid Azure Key Vault PKI sync configuration"); - } + checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames); break; } + case PkiSync.AwsCertificateManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); + const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); + await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames); + break; + } default: throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); } diff --git a/backend/src/services/pki-sync/pki-sync-maps.ts b/backend/src/services/pki-sync/pki-sync-maps.ts index b667416c6..5c416b513 100644 --- a/backend/src/services/pki-sync/pki-sync-maps.ts +++ b/backend/src/services/pki-sync/pki-sync-maps.ts @@ -3,9 +3,11 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { PkiSync } from "./pki-sync-enums"; export const PKI_SYNC_NAME_MAP: Record = { - [PkiSync.AzureKeyVault]: "Azure Key Vault" + [PkiSync.AzureKeyVault]: "Azure Key Vault", + [PkiSync.AwsCertificateManager]: "AWS Certificate Manager" }; export const PKI_SYNC_CONNECTION_MAP: Record = { - [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault + [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [PkiSync.AwsCertificateManager]: AppConnection.AWS }; diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index 57c6581ce..64d2c0bdb 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -204,5 +204,19 @@ export const secretSyncDALFactory = ( } }; - return { ...secretSyncOrm, findById, findOne, find, create, updateById }; + const findByDestinationAndOrgId = async (destination: string, orgId: string, tx?: Knex) => { + try { + const response = await (tx || db.replicaNode())(TableName.SecretSync) + .join(TableName.Project, `${TableName.SecretSync}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.SecretSync}.destination`, destination) + .where(`${TableName.Project}.orgId`, orgId) + .select(selectAllTableCols(TableName.SecretSync)); + + return response; + } catch (error) { + throw new DatabaseError({ error, name: "Find By Destination And Org ID - Secret Sync" }); + } + }; + + return { ...secretSyncOrm, findById, findOne, find, create, updateById, findByDestinationAndOrgId }; }; diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 04e91051d..1fbc66cca 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -1,5 +1,6 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { SecretSync, SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; +import { DestinationDuplicateCheckFn } from "@app/services/secret-sync/secret-sync-types"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", @@ -99,3 +100,104 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Netlify]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: SecretSyncPlanType.Regular }; + +export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { + [SecretSync.AWSParameterStore]: [], + [SecretSync.AWSSecretsManager]: ["mappingBehavior", "secretName"], + [SecretSync.GitHub]: [], + [SecretSync.GCPSecretManager]: [], + [SecretSync.AzureKeyVault]: [], + [SecretSync.AzureAppConfiguration]: ["label"], + [SecretSync.AzureDevOps]: ["devopsProjectName"], + [SecretSync.Databricks]: [], + [SecretSync.Humanitec]: [], + [SecretSync.TerraformCloud]: ["variableSetName", "workspaceName"], + [SecretSync.Camunda]: [], + [SecretSync.Vercel]: ["appName"], + [SecretSync.Windmill]: [], + [SecretSync.HCVault]: [], + [SecretSync.TeamCity]: [], + [SecretSync.OCIVault]: [], + [SecretSync.OnePass]: ["valueLabel"], + [SecretSync.Heroku]: ["appName"], + [SecretSync.Render]: [], + [SecretSync.Flyio]: [], + [SecretSync.GitLab]: [ + "projectName", + "shouldProtectSecrets", + "shouldMaskSecrets", + "shouldHideSecrets", + "targetEnvironment", + "groupName", + "groupId", + "projectId" + ], + [SecretSync.CloudflarePages]: [], + [SecretSync.CloudflareWorkers]: [], + [SecretSync.Supabase]: ["projectName"], + [SecretSync.Zabbix]: ["hostName", "macroType"], + [SecretSync.Railway]: ["projectName", "environmentName", "serviceName"], + [SecretSync.Checkly]: ["groupName", "accountName"], + [SecretSync.DigitalOceanAppPlatform]: ["appName"], + [SecretSync.Netlify]: ["accountName", "siteName"], + [SecretSync.Bitbucket]: [] +}; + +const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true; + +export const DESTINATION_DUPLICATE_CHECK_MAP: Record = { + [SecretSync.AWSParameterStore]: defaultDuplicateCheck, + [SecretSync.AWSSecretsManager]: defaultDuplicateCheck, + [SecretSync.GitHub]: defaultDuplicateCheck, + [SecretSync.GCPSecretManager]: defaultDuplicateCheck, + [SecretSync.AzureKeyVault]: defaultDuplicateCheck, + [SecretSync.AzureAppConfiguration]: defaultDuplicateCheck, + [SecretSync.AzureDevOps]: defaultDuplicateCheck, + [SecretSync.Databricks]: defaultDuplicateCheck, + [SecretSync.Humanitec]: defaultDuplicateCheck, + [SecretSync.TerraformCloud]: defaultDuplicateCheck, + [SecretSync.Camunda]: defaultDuplicateCheck, + [SecretSync.Vercel]: defaultDuplicateCheck, + [SecretSync.Windmill]: defaultDuplicateCheck, + [SecretSync.HCVault]: defaultDuplicateCheck, + [SecretSync.TeamCity]: defaultDuplicateCheck, + [SecretSync.OCIVault]: defaultDuplicateCheck, + [SecretSync.OnePass]: defaultDuplicateCheck, + [SecretSync.Heroku]: defaultDuplicateCheck, + [SecretSync.Render]: defaultDuplicateCheck, + [SecretSync.Flyio]: defaultDuplicateCheck, + [SecretSync.GitLab]: (existingConfig, newConfig) => { + const existingTargetEnv = existingConfig.targetEnvironment as string | undefined; + const newTargetEnv = newConfig.targetEnvironment as string | undefined; + + const wildcardValues = ["*", ""]; + + if ( + (newConfig.scope as string) === "group" + ? existingConfig.groupId !== newConfig.groupId + : existingConfig.projectId !== newConfig.projectId + ) + return false; + + // If either has wildcard, it conflicts with any targetEnvironment + if ( + !existingTargetEnv || + !newTargetEnv || + wildcardValues.includes(existingTargetEnv) || + wildcardValues.includes(newTargetEnv) + ) { + return true; + } + + return existingTargetEnv === newTargetEnv; + }, + [SecretSync.CloudflarePages]: defaultDuplicateCheck, + [SecretSync.CloudflareWorkers]: defaultDuplicateCheck, + [SecretSync.Supabase]: defaultDuplicateCheck, + [SecretSync.Zabbix]: defaultDuplicateCheck, + [SecretSync.Railway]: defaultDuplicateCheck, + [SecretSync.Checkly]: defaultDuplicateCheck, + [SecretSync.DigitalOceanAppPlatform]: defaultDuplicateCheck, + [SecretSync.Netlify]: defaultDuplicateCheck, + [SecretSync.Bitbucket]: defaultDuplicateCheck +}; diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index ecd7d04a5..6a2f49386 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -12,6 +12,7 @@ import { import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { deepEqualSkipFields } from "@app/lib/fn/object"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -20,6 +21,7 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { enterpriseSyncCheck, listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncStatus, + TCheckDuplicateDestinationDTO, TCreateSecretSyncDTO, TDeleteSecretSyncDTO, TFindSecretSyncByIdDTO, @@ -35,7 +37,12 @@ import { import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretSyncDALFactory } from "./secret-sync-dal"; -import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps"; +import { + DESTINATION_DUPLICATE_CHECK_MAP, + SECRET_SYNC_CONNECTION_MAP, + SECRET_SYNC_NAME_MAP, + SECRET_SYNC_SKIP_FIELDS_MAP +} from "./secret-sync-maps"; import { TSecretSyncQueueFactory } from "./secret-sync-queue"; type TSecretSyncServiceFactoryDep = { @@ -696,6 +703,61 @@ export const secretSyncServiceFactory = ({ return updatedSecretSync as TSecretSync; }; + const checkDuplicateDestination = async ( + { destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO, + actor: OrgServiceActor + ) => { + const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination]; + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSub.SecretSyncs + ); + + if (!destinationConfig || Object.keys(destinationConfig).length === 0) { + return { hasDuplicate: false, duplicateProjectId: undefined }; + } + + try { + const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId); + + const duplicates = existingSyncs.filter((sync) => { + if (sync.id === excludeSyncId) { + return false; + } + + try { + const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields); + if (baseFieldsMatch) { + return DESTINATION_DUPLICATE_CHECK_MAP[destination]( + sync.destinationConfig as Record, + destinationConfig + ); + } + return false; + } catch { + return false; + } + }); + + const hasDuplicate = duplicates.length > 0; + return { + hasDuplicate, + duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined + }; + } catch (error) { + return { hasDuplicate: false, duplicateProjectId: undefined }; + } + }; + return { listSecretSyncOptions, listSecretSyncsByProjectId, @@ -707,6 +769,7 @@ export const secretSyncServiceFactory = ({ deleteSecretSync, triggerSecretSyncSyncSecretsById, triggerSecretSyncImportSecretsById, - triggerSecretSyncRemoveSecretsById + triggerSecretSyncRemoveSecretsById, + checkDuplicateDestination }; }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 6435e19d3..478c44c92 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -324,6 +324,13 @@ export type TDeleteSecretSyncDTO = { removeSecrets: boolean; }; +export type TCheckDuplicateDestinationDTO = { + destination: SecretSync; + destinationConfig: Record; + excludeSyncId?: string; + projectId: string; +}; + export enum SecretSyncStatus { Pending = "pending", Running = "running", @@ -408,3 +415,8 @@ export type TSecretMap = Record< secretMetadata?: ResourceMetadataDTO; } >; + +export type DestinationDuplicateCheckFn = ( + existingConfig: Record, + newConfig: Record +) => boolean; diff --git a/backend/src/services/smtp/emails/AccountDeletionConfirmationTemplate.tsx b/backend/src/services/smtp/emails/AccountDeletionConfirmationTemplate.tsx new file mode 100644 index 000000000..ff4cb7de9 --- /dev/null +++ b/backend/src/services/smtp/emails/AccountDeletionConfirmationTemplate.tsx @@ -0,0 +1,36 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface AccountDeletionConfirmationTemplateProps + extends Omit { + email: string; +} + +export const AccountDeletionConfirmationTemplate = ({ email, siteUrl }: AccountDeletionConfirmationTemplateProps) => { + return ( + + + Account Deleted + +
+ + This email confirms that your Infisical account {email} has been deleted, including all + associated data. + +
+
+ ); +}; + +export default AccountDeletionConfirmationTemplate; + +AccountDeletionConfirmationTemplate.PreviewProps = { + email: "test@infisical.com", + siteUrl: "https://infisical.com" +} as AccountDeletionConfirmationTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 066744596..d7204085c 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -1,5 +1,6 @@ export * from "./AccessApprovalRequestTemplate"; export * from "./AccessApprovalRequestUpdatedTemplate"; +export * from "./AccountDeletionConfirmationTemplate"; export * from "./EmailMfaTemplate"; export * from "./EmailVerificationTemplate"; export * from "./ExternalImportFailedTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index d64582fe0..63645cf67 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -9,6 +9,7 @@ import { logger } from "@app/lib/logger"; import { AccessApprovalRequestTemplate, AccessApprovalRequestUpdatedTemplate, + AccountDeletionConfirmationTemplate, EmailMfaTemplate, EmailVerificationTemplate, ExternalImportFailedTemplate, @@ -83,7 +84,8 @@ export enum SmtpTemplates { OrgAdminBreakglassAccess = "orgAdminBreakglassAccess", ServiceTokenExpired = "serviceTokenExpired", SecretScanningV2ScanFailed = "secretScanningV2ScanFailed", - SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected" + SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected", + AccountDeletionConfirmation = "accountDeletionConfirmation" } export enum SmtpHost { @@ -128,7 +130,8 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.SetupPassword]: PasswordSetupTemplate, [SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate, [SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate, - [SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate + [SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate, + [SmtpTemplates.AccountDeletionConfirmation]: AccountDeletionConfirmationTemplate }; export const smtpServiceFactory = (cfg: TSmtpConfig) => { diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b8058f327..f5210290c 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -347,6 +347,23 @@ export const userServiceFactory = ({ const deleteUser = async (userId: string) => { const user = await userDAL.deleteById(userId); + + try { + if (user?.email) { + // Send email to user to confirm account deletion + await smtpService.sendMail({ + template: SmtpTemplates.AccountDeletionConfirmation, + subjectLine: "Your Infisical account has been deleted", + recipients: [user.email], + substitutions: { + email: user.email + } + }); + } + } catch (error) { + logger.error(error, `Failed to send account deletion confirmation email to ${user.email}`); + } + return user; }; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2de271180..00dc19a46 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -197,4 +197,4 @@ volumes: driver: local ldap_data: ldap_config: - grafana_storage: \ No newline at end of file + grafana_storage: diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx new file mode 100644 index 000000000..dcd58cf32 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create AWS Certificate Manager PKI Sync" +openapi: "POST /api/v1/pki/syncs/aws-certificate-manager" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx new file mode 100644 index 000000000..73fed2cdb --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete AWS Certificate Manager PKI Sync" +openapi: "DELETE /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx new file mode 100644 index 000000000..9191bbde3 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get AWS Certificate Manager PKI Sync by ID" +openapi: "GET /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx new file mode 100644 index 000000000..821ddbd61 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List AWS Certificate Manager PKI Syncs" +openapi: "GET /api/v1/pki/syncs/aws-certificate-manager" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx new file mode 100644 index 000000000..5ea989f2a --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Certificates from AWS Certificate Manager" +openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/remove-certificates" +--- diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx new file mode 100644 index 000000000..b97b7a9ab --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Certificates to AWS Certificate Manager" +openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/sync" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx new file mode 100644 index 000000000..9b7382ce8 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update AWS Certificate Manager PKI Sync" +openapi: "PATCH /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 210c5145c..eb69cd2b8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -723,6 +723,7 @@ { "group": "Syncs", "pages": [ + "documentation/platform/pki/certificate-syncs/aws-certificate-manager", "documentation/platform/pki/certificate-syncs/azure-key-vault" ] } @@ -2523,6 +2524,18 @@ "api-reference/endpoints/pki/syncs/list", "api-reference/endpoints/pki/syncs/get-by-id", "api-reference/endpoints/pki/syncs/options", + { + "group": "AWS Certificate Manager", + "pages": [ + "api-reference/endpoints/pki/syncs/aws-certificate-manager/list", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/create", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/update", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/delete", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates" + ] + }, { "group": "Azure Key Vault", "pages": [ diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 2d500a83a..c1fc85257 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -145,6 +145,9 @@ Infisical needs an AWS IAM principal (a user or a role) with the required permis 2. Select **AWS Account** as the **Trusted Entity Type**. 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. + + **For Dedicated Instances**: Your AWS account ID differs from the one provided above. Please reach out to Infisical support to obtain your AWS account ID. + 4. (Recommended) Enable "Require external ID" and input your **Project ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). 5. Assign permission as shared in prerequisite. diff --git a/docs/documentation/platform/gateways/gateway-deployment.mdx b/docs/documentation/platform/gateways/gateway-deployment.mdx index 40b98965a..cef258c41 100644 --- a/docs/documentation/platform/gateways/gateway-deployment.mdx +++ b/docs/documentation/platform/gateways/gateway-deployment.mdx @@ -95,7 +95,7 @@ To successfully deploy an Infisical Gateway for use, follow these steps in order Ensure a relay server is running and accessible before you deploy any gateways. You have two options: - **Managed relay (Infisical Cloud, US/EU only):** Managed relays are only available for Infisical Cloud instances in the US and EU regions. If you are using Infisical Cloud in these regions, you can use the provided managed relay. - - **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the Relay Deployment Guide. + - **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the [Relay Deployment Guide](/documentation/platform/gateways/relay-deployment). Make sure the Infisical CLI is installed on the machine or environment where you plan to deploy the gateway. The CLI is required for gateway installation and management. diff --git a/docs/documentation/platform/kms-configuration/aws-kms.mdx b/docs/documentation/platform/kms-configuration/aws-kms.mdx index b4631b1c3..b57efca22 100644 --- a/docs/documentation/platform/kms-configuration/aws-kms.mdx +++ b/docs/documentation/platform/kms-configuration/aws-kms.mdx @@ -23,6 +23,9 @@ Before you begin, you'll first need to choose a method of authentication with AW 2. Select **AWS Account** as the **Trusted Entity Type**. 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. + + **For Dedicated Instances**: Your AWS account ID differs from the one provided above. Please reach out to Infisical support to obtain your AWS account ID. + 4. Optionally, enable **Require external ID** and enter your Infisical **project ID** to further enhance security. diff --git a/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx b/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx new file mode 100644 index 000000000..de064bf4e --- /dev/null +++ b/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx @@ -0,0 +1,146 @@ +--- +title: "AWS Certificate Manager" +description: "Learn how to configure an AWS Certificate Manager Certificate Sync for Infisical PKI." +--- + +**Prerequisites:** + +- Set up and configure a [Certificate Authority](/documentation/platform/pki/overview) +- Create an [AWS Connection](/integrations/app-connections/aws) + + + The AWS Certificate Manager Certificate Sync requires the following ACM permissions to be set on the IAM user/role + for Infisical to sync certificates to AWS Certificate Manager: `acm:ListCertificates`, `acm:DescribeCertificate`, `acm:ImportCertificate`, `acm:DeleteCertificate`, and `acm:ListTagsForCertificate`. + + These permissions allow Infisical to list, import, tag, and manage certificates in your AWS Certificate Manager service. + + + + Certificates synced to AWS Certificate Manager will be stored as imported certificates, preserving both the certificate and private key components. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Certificate Syncs** tab. Click on the **Add Sync** button. + ![Certificate Syncs Tab](/images/certificate-syncs/general/certificate-sync-tab.png) + + 2. Select the **AWS Certificate Manager** option. + ![Select ACM](/images/certificate-syncs/aws-certificate-manager/select-acm-option.png) + + 3. Configure the **Source** from where certificates should be retrieved, then click **Next**. + ![Configure Source](/images/certificate-syncs/aws-certificate-manager/acm-source.png) + + - **PKI Subscriber**: The PKI subscriber to retrieve certificates from. + + 4. Configure the **Destination** to where certificates should be deployed, then click **Next**. + ![Configure Destination](/images/certificate-syncs/aws-certificate-manager/acm-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **AWS Region**: The AWS region where certificates should be stored. + + 5. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**. + ![Configure Options](/images/certificate-syncs/aws-certificate-manager/acm-options.png) + + - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced from the source PKI subscriber when changes occur. Disable to enforce manual syncing only. + - **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually. + - **Certificate Name Schema** (Optional): Customize how certificate tags are generated in AWS Certificate Manager. Must include `{{certificateId}}` as a placeholder for the certificate ID to ensure proper certificate identification and management. If not specified, defaults to `Infisical-{{certificateId}}`. + + + **AWS Certificate Manager Certificate Limits**: AWS Certificate Manager has limits on the number of certificates per account and region. Refer to AWS documentation for current limits. Deleted certificates count toward your quota until they are permanently purged by AWS (typically after 30 days). + + + 6. Configure the **Details** of your AWS Certificate Manager Certificate Sync, then click **Next**. + ![Configure Details](/images/certificate-syncs/aws-certificate-manager/acm-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your AWS Certificate Manager Certificate Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/certificate-syncs/aws-certificate-manager/acm-review.png) + + 8. If enabled, your AWS Certificate Manager Certificate Sync will begin syncing your certificates to the destination endpoint. + ![Sync Certificates](/images/certificate-syncs/aws-certificate-manager/acm-synced.png) + + + + To create an **AWS Certificate Manager Certificate Sync**, make an API request to the [Create AWS Certificate Manager Certificate Sync](/api-reference/endpoints/pki/syncs/aws-certificate-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/pki/syncs/aws-certificate-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-acm-cert-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example certificate sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "destination": "aws-certificate-manager", + "isAutoSyncEnabled": true, + "syncOptions": { + "canRemoveCertificates": true, + "certificateNameSchema": "myapp-{{certificateId}}" + }, + "destinationConfig": { + "region": "us-east-1" + } + }' + ``` + + ### Sample response + + ```json Response + { + "pkiSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-acm-cert-sync", + "description": "an example certificate sync", + "destination": "aws-certificate-manager", + "isAutoSyncEnabled": true, + "destinationConfig": { + "region": "us-east-1" + }, + "syncOptions": { + "canRemoveCertificates": true, + "certificateNameSchema": "myapp-{{certificateId}}" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z" + } + } + ``` + + + +## Certificate Management + +Your AWS Certificate Manager Certificate Sync will: + +- **Automatic Deployment**: Deploy new certificates issued by your PKI subscriber to AWS Certificate Manager +- **Certificate Updates**: Update certificates in AWS Certificate Manager when renewals occur +- **Expiration Handling**: Optionally remove expired certificates from AWS Certificate Manager (if enabled) +- **Tagging**: Automatically tag certificates with an InfisicalCertificate tag for easy identification and management + + + AWS Certificate Manager Certificate Syncs support both automatic and manual synchronization modes. When auto-sync is enabled, certificates are automatically deployed as they are issued or renewed. + + +## Manual Certificate Sync + +You can manually trigger certificate synchronization from your PKI subscriber to AWS Certificate Manager using the sync certificates functionality. This is useful for: + +- Initial setup when you have existing certificates to deploy +- One-time sync of specific certificates +- Testing certificate sync configurations +- Force sync after making changes + +To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates) API endpoint or the manual sync option in the Infisical UI. + + +AWS Certificate Manager does not support importing certificates back into Infisical due to security limitations where private keys cannot be extracted from AWS Certificate Manager. Only certificates imported into ACM (not AWS-issued certificates) can be managed by the sync. + \ No newline at end of file diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 84888b2f9..639a11321 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -53,6 +53,13 @@ description: "Learn how to configure Google SAML for Infisical SSO." ![Google SAML attribute mapping](../../../images/sso/google-saml/attribute-mapping.png) + + If you want to sync Google groups to Infisical groups, you can also configure: + - **groups** -> **groups** + + This requires setting up group claims in Google Workspace. See the [Group Membership Mapping](#saml-group-membership-mapping) section below for details. + + Click **Finish**. @@ -90,6 +97,34 @@ description: "Learn how to configure Google SAML for Infisical SSO." +## SAML Group Membership Mapping + +Automatically sync Google Workspace group memberships to Infisical. + + + + In your Google Admin console SAML app, go to **Attribute mapping** and add: + + - **Google groups**: Include all groups you want to include in the SAML claim. Only these groups will be synced to Infisical. + - **App attribute**: `groups` + + ![Google SAML groups attribute mapping](../../../images/sso/google-saml/groups-attribute-mapping.png) + + + + Back in Infisical, under Organization Settings, enable **SAML Group Membership Mapping** in the **Single Sign-On (SSO)** tab. + + ![Google SAML group membership mapping](../../../images/sso/google-saml/group-membership-mapping.png) + + + Once configured, Google groups will now be automatically synchronized when users log in through SAML. Users will be added to or removed from Infisical groups based on their current Google group memberships. + + + + +Group membership changes in the SAML provider only sync with Infisical when a user logs in via SAML. For example, if you remove a user from a group in the SAML provider, this change will not be reflected in Infisical until their next SAML login. To ensure this behavior, Infisical recommends enabling Enforce SAML SSO. + + If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login. diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-destination.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-destination.png new file mode 100644 index 000000000..42a20fc99 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-destination.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-details.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-details.png new file mode 100644 index 000000000..483cee003 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-details.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-options.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-options.png new file mode 100644 index 000000000..aa08b2d19 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-options.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-review.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-review.png new file mode 100644 index 000000000..5f7b216ad Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-review.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-source.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-source.png new file mode 100644 index 000000000..0d92fe69e Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-source.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-synced.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-synced.png new file mode 100644 index 000000000..7e1ed12c5 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-synced.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/select-acm-option.png b/docs/images/certificate-syncs/aws-certificate-manager/select-acm-option.png new file mode 100644 index 000000000..79515516a Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/select-acm-option.png differ diff --git a/docs/images/sso/google-saml/group-membership-mapping.png b/docs/images/sso/google-saml/group-membership-mapping.png new file mode 100644 index 000000000..eb7ff67ec Binary files /dev/null and b/docs/images/sso/google-saml/group-membership-mapping.png differ diff --git a/docs/images/sso/google-saml/groups-attribute-mapping.png b/docs/images/sso/google-saml/groups-attribute-mapping.png new file mode 100644 index 000000000..16a494c3c Binary files /dev/null and b/docs/images/sso/google-saml/groups-attribute-mapping.png differ diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index 08ae05be7..fc9f6722e 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -56,6 +56,9 @@ Infisical supports two methods for connecting to AWS. 2. Select **AWS Account** as the **Trusted Entity Type**. 3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. + + **For Dedicated Instances**: Your AWS account ID differs from the one provided above. Please reach out to Infisical support to obtain your AWS account ID. + 4. (Recommended) Enable "Require external ID" and input your **Organization ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). @@ -174,6 +177,45 @@ Infisical supports two methods for connecting to AWS. + + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync certificates to AWS Certificate Manager: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCertificateManagerAccess", + "Effect": "Allow", + "Action": [ + "acm:ListCertificates", + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:ImportCertificate", + "acm:ExportCertificate", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:ListTagsForCertificate" + ], + "Resource": "*" + } + ] + } + ``` + + - **ListCertificates**: Lists all certificates in the account + - **ImportCertificate**: Imports certificates from Infisical into AWS Certificate Manager + - **ExportCertificate**: Exports certificates for synchronization + - **DeleteCertificate**: Removes certificates that are no longer managed by Infisical + - **DescribeCertificate** and **GetCertificate**: Retrieves certificate details for comparison during sync + - Tag-related permissions: Manages certificate tags for identification and organization + + + + @@ -351,6 +393,45 @@ Infisical supports two methods for connecting to AWS. + + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync certificates to AWS Certificate Manager: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCertificateManagerAccess", + "Effect": "Allow", + "Action": [ + "acm:ListCertificates", + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:ImportCertificate", + "acm:ExportCertificate", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:ListTagsForCertificate" + ], + "Resource": "*" + } + ] + } + ``` + + - **ListCertificates**: Lists all certificates in the account + - **ImportCertificate**: Imports certificates from Infisical into AWS Certificate Manager + - **ExportCertificate**: Exports certificates for synchronization + - **DeleteCertificate**: Removes certificates that are no longer managed by Infisical + - **DescribeCertificate** and **GetCertificate**: Retrieves certificate details for comparison during sync + - Tag-related permissions: Manages certificate tags for identification and organization + + + + diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx index 7ead4d3dd..f6ddaad58 100644 --- a/docs/sdks/languages/java.mdx +++ b/docs/sdks/languages/java.mdx @@ -105,6 +105,22 @@ sdk.Auth().UniversalAuthLogin( - `clientId` (string): The client ID of your Machine Identity. - `clientSecret` (string): The client secret of your Machine Identity. +### AWS Auth + +```java +public void AwsAuthLogin( + String identityId +) +throws InfisicalException +``` + +```java +sdk.Auth().AwsAuthLogin(""); +``` + +**Parameters:** +- `identityId` (String): The ID of the machine identity to authenticate with. + ### LDAP Auth ```java diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3c73d9fe3..1251de3e4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -55,7 +55,7 @@ "@ucast/mongo2js": "^1.3.4", "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", @@ -65,7 +65,7 @@ "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", - "jspdf": "^2.5.2", + "jspdf": "^3.0.2", "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", "lexical": "^0.29.0", @@ -4387,6 +4387,12 @@ "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -5204,18 +5210,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -5258,7 +5252,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -5282,9 +5275,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -5626,18 +5619,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -5681,7 +5662,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -5697,9 +5677,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5710,14 +5690,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.2.tgz", - "integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "get-intrinsic": "^1.2.5" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -6085,7 +6064,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -6499,7 +6477,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -6661,12 +6638,12 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -6860,9 +6837,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7709,6 +7686,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7826,7 +7820,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.3" @@ -7986,21 +7979,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8018,6 +8011,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -8181,7 +8187,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -8234,19 +8239,62 @@ } }, "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=4" + "node": ">= 0.8" } }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -8628,6 +8676,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -8775,7 +8829,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9090,13 +9143,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -9152,7 +9204,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -9306,29 +9357,22 @@ } }, "node_modules/jspdf": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", - "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2", - "atob": "^2.1.2", - "btoa": "^1.2.1", + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { - "canvg": "^3.0.6", + "canvg": "^3.0.11", "core-js": "^3.6.0", - "dompurify": "^2.5.4", + "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, - "node_modules/jspdf/node_modules/dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "optional": true - }, "node_modules/jsrp": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsrp/-/jsrp-0.2.4.tgz", @@ -9535,9 +9579,9 @@ } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10807,20 +10851,21 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=0.12" + "node": ">= 0.10" } }, "node_modules/performance-now": { @@ -10894,7 +10939,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11219,7 +11263,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/prop-types": { @@ -11749,6 +11792,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -11980,13 +12024,16 @@ } }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/rollup": { @@ -12142,7 +12189,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -12180,16 +12226,23 @@ "license": "MIT" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -12377,6 +12430,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -12888,6 +12942,20 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13501,15 +13569,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -14219,7 +14286,6 @@ "version": "1.1.16", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", "integrity": "sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/frontend/package.json b/frontend/package.json index 1bc55c1c7..7366bb16f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,11 @@ "lint:fix": "eslint --fix ./src", "type:check": "tsc --noEmit --project ./tsconfig.app.json" }, + + "overrides": { + "sha.js": "2.4.12" + }, + "dependencies": { "@casl/ability": "^6.7.2", "@casl/react": "^4.0.0", @@ -59,7 +64,7 @@ "@ucast/mongo2js": "^1.3.4", "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", @@ -69,7 +74,7 @@ "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", - "jspdf": "^2.5.2", + "jspdf": "^3.0.2", "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", "lexical": "^0.29.0", diff --git a/frontend/public/images/integrations/IBM.png b/frontend/public/images/integrations/IBM.png new file mode 100644 index 000000000..7fafbb6c3 Binary files /dev/null and b/frontend/public/images/integrations/IBM.png differ diff --git a/frontend/public/images/integrations/RDP.png b/frontend/public/images/integrations/RDP.png new file mode 100644 index 000000000..1336888b7 Binary files /dev/null and b/frontend/public/images/integrations/RDP.png differ diff --git a/frontend/public/images/integrations/SSH.png b/frontend/public/images/integrations/SSH.png new file mode 100644 index 000000000..ff7708f9f Binary files /dev/null and b/frontend/public/images/integrations/SSH.png differ diff --git a/frontend/src/components/pki-syncs/forms/AwsCertificateManagerPkiSyncFields.tsx b/frontend/src/components/pki-syncs/forms/AwsCertificateManagerPkiSyncFields.tsx new file mode 100644 index 000000000..83d85a35f --- /dev/null +++ b/frontend/src/components/pki-syncs/forms/AwsCertificateManagerPkiSyncFields.tsx @@ -0,0 +1,50 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { FormControl, Select, SelectItem } from "@app/components/v2"; +import { AWS_REGIONS } from "@app/helpers/appConnections"; +import { PkiSync } from "@app/hooks/api/pkiSyncs"; + +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; +import { PkiSyncConnectionField } from "./PkiSyncConnectionField"; + +export const AwsCertificateManagerPkiSyncFields = () => { + const { control, setValue } = useFormContext< + TPkiSyncForm & { destination: PkiSync.AwsCertificateManager } + >(); + + return ( + <> + { + setValue("destinationConfig.region", ""); + }} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/pki-syncs/forms/AzureKeyVaultPkiSyncFields.tsx b/frontend/src/components/pki-syncs/forms/AzureKeyVaultPkiSyncFields.tsx index 87feebff7..2c4af9376 100644 --- a/frontend/src/components/pki-syncs/forms/AzureKeyVaultPkiSyncFields.tsx +++ b/frontend/src/components/pki-syncs/forms/AzureKeyVaultPkiSyncFields.tsx @@ -3,8 +3,8 @@ import { Controller, useFormContext } from "react-hook-form"; import { FormControl, Input } from "@app/components/v2"; import { PkiSync } from "@app/hooks/api/pkiSyncs"; +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; import { PkiSyncConnectionField } from "./PkiSyncConnectionField"; -import { TPkiSyncForm } from "./schemas"; export const AzureKeyVaultPkiSyncFields = () => { const { control, setValue } = useFormContext< diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index 02066c1cc..132db1e98 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -12,12 +12,12 @@ import { useProject } from "@app/context"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs"; +import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields"; import { PkiSyncReviewFields } from "./PkiSyncReviewFields"; import { PkiSyncSourceFields } from "./PkiSyncSourceFields"; -import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas"; type Props = { onComplete: (pkiSync: TPkiSync) => void; diff --git a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx index c4407ceee..73f5bc2bc 100644 --- a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx @@ -8,11 +8,11 @@ import { Button, ModalClose } from "@app/components/v2"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs"; +import { TUpdatePkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas/pki-sync-schema"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields"; import { PkiSyncSourceFields } from "./PkiSyncSourceFields"; -import { TPkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas"; type Props = { onComplete: (pkiSync: TPkiSync) => void; @@ -24,7 +24,7 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => { const updatePkiSync = useUpdatePkiSync(); const { name: destinationName } = PKI_SYNC_MAP[pkiSync.destination]; - const formMethods = useForm({ + const formMethods = useForm({ resolver: zodResolver(UpdatePkiSyncFormSchema), defaultValues: { ...pkiSync, @@ -33,11 +33,11 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => { id: pkiSync.connectionId, name: pkiSync.appConnectionName } - } as Partial, + } as Partial, reValidateMode: "onChange" }); - const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => { + const onSubmit = async ({ connection, ...formData }: TUpdatePkiSyncForm) => { try { const updatedPkiSync = await updatePkiSync.mutateAsync({ syncId: pkiSync.id, diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx index 95bbfc531..445f9169d 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx @@ -10,7 +10,7 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; -import { TPkiSyncForm } from "./schemas"; +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; type Props = { onChange?: VoidFunction; diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx index 4566b76e3..e7f90670d 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx @@ -2,8 +2,9 @@ import { useFormContext } from "react-hook-form"; import { PkiSync } from "@app/hooks/api/pkiSyncs"; +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; +import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields"; import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields"; -import { TPkiSyncForm } from "./schemas"; export const PkiSyncDestinationFields = () => { const { watch } = useFormContext(); @@ -13,6 +14,8 @@ export const PkiSyncDestinationFields = () => { switch (destination) { case PkiSync.AzureKeyVault: return ; + case PkiSync.AwsCertificateManager: + return ; default: return (
diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncDetailsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncDetailsFields.tsx index d7fc282dd..110744e2c 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncDetailsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncDetailsFields.tsx @@ -2,7 +2,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { FormControl, Input, TextArea } from "@app/components/v2"; -import { TPkiSyncForm } from "./schemas"; +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; export const PkiSyncDetailsFields = () => { const { control } = useFormContext(); diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx index 9043b36a3..ce581bb88 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FormControl, Input, Switch, Tooltip } from "@app/components/v2"; import { PkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs"; -import { TPkiSyncForm } from "../schemas"; +import { TPkiSyncForm } from "../schemas/pki-sync-schema"; type Props = { destination?: PkiSync; diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx index 7ddf6a514..be6b91948 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx @@ -5,7 +5,7 @@ import { useProject } from "@app/context"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; -import { TPkiSyncForm } from "./schemas"; +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; export const PkiSyncReviewFields = () => { const { watch } = useFormContext(); diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncSourceFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncSourceFields.tsx index 80f17f4e6..ea856cfb4 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncSourceFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncSourceFields.tsx @@ -4,7 +4,7 @@ import { FilterableSelect, FormControl } from "@app/components/v2"; import { useProject } from "@app/context"; import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; -import { TPkiSyncForm } from "./schemas"; +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; export const PkiSyncSourceFields = () => { const { control } = useFormContext(); diff --git a/frontend/src/components/pki-syncs/forms/index.ts b/frontend/src/components/pki-syncs/forms/index.ts index 7d12526b1..5f92b599a 100644 --- a/frontend/src/components/pki-syncs/forms/index.ts +++ b/frontend/src/components/pki-syncs/forms/index.ts @@ -5,4 +5,4 @@ export { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields/PkiSyncOptionsFields"; export { PkiSyncReviewFields } from "./PkiSyncReviewFields"; export { PkiSyncSourceFields } from "./PkiSyncSourceFields"; -export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas"; +export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas/pki-sync-schema"; diff --git a/frontend/src/components/pki-syncs/forms/schemas/aws-certificate-manager-pki-sync-destination-schema.ts b/frontend/src/components/pki-syncs/forms/schemas/aws-certificate-manager-pki-sync-destination-schema.ts new file mode 100644 index 000000000..aa522ef7d --- /dev/null +++ b/frontend/src/components/pki-syncs/forms/schemas/aws-certificate-manager-pki-sync-destination-schema.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +import { PkiSync } from "@app/hooks/api/pkiSyncs"; + +import { BasePkiSyncSchema } from "./base-pki-sync-schema"; + +const AwsCertificateManagerSyncOptionsSchema = z.object({ + canImportCertificates: z.boolean().default(false), + canRemoveCertificates: z.boolean().default(false), + certificateNameSchema: z + .string() + .optional() + .refine( + (val) => { + // For AWS Certificate Manager, {{certificateId}} is always required if certificateNameSchema is provided + if (!val) return true; + + if (!val.includes("{{certificateId}}")) { + return false; + } + + const allowedOptionalPlaceholders = ["{{environment}}"]; + const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders] + .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) + .join("|"); + + const allowedContentRegex = new RegExp( + `^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$` + ); + + return allowedContentRegex.test(val); + }, + { + message: + "Certificate name schema must include {{certificateId}} placeholder for AWS Certificate Manager." + } + ) +}); + +export const AwsCertificateManagerPkiSyncDestinationSchema = BasePkiSyncSchema( + AwsCertificateManagerSyncOptionsSchema +).merge( + z.object({ + destination: z.literal(PkiSync.AwsCertificateManager), + destinationConfig: z.object({ + region: z.string().min(1, "AWS region is required") + }) + }) +); + +export const UpdateAwsCertificateManagerPkiSyncDestinationSchema = + AwsCertificateManagerPkiSyncDestinationSchema.partial().merge( + z.object({ + name: z + .string() + .trim() + .min(1, "Name is required") + .max(255, "Name must be less than 255 characters"), + destination: z.literal(PkiSync.AwsCertificateManager), + connection: z.object({ + id: z.string().uuid("Invalid connection ID format"), + name: z.string().max(255, "Connection name must be less than 255 characters") + }) + }) + ); diff --git a/frontend/src/components/pki-syncs/forms/schemas/azure-key-vault-pki-sync-destination-schema.ts b/frontend/src/components/pki-syncs/forms/schemas/azure-key-vault-pki-sync-destination-schema.ts new file mode 100644 index 000000000..e87e7a52c --- /dev/null +++ b/frontend/src/components/pki-syncs/forms/schemas/azure-key-vault-pki-sync-destination-schema.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +import { PkiSync } from "@app/hooks/api/pkiSyncs"; + +import { BasePkiSyncSchema } from "./base-pki-sync-schema"; + +export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema().merge( + z.object({ + destination: z.literal(PkiSync.AzureKeyVault), + destinationConfig: z.object({ + vaultBaseUrl: z.string().min(1, "Vault base URL is required").url("Valid URL is required") + }) + }) +); + +export const UpdateAzureKeyVaultPkiSyncDestinationSchema = + AzureKeyVaultPkiSyncDestinationSchema.partial().merge( + z.object({ + name: z + .string() + .trim() + .min(1, "Name is required") + .max(255, "Name must be less than 255 characters"), + destination: z.literal(PkiSync.AzureKeyVault), + connection: z.object({ + id: z.string().uuid("Invalid connection ID format"), + name: z + .string() + .min(1, "Connection name is required") + .max(255, "Connection name must be less than 255 characters") + }) + }) + ); diff --git a/frontend/src/components/pki-syncs/forms/schemas.ts b/frontend/src/components/pki-syncs/forms/schemas/base-pki-sync-schema.ts similarity index 63% rename from frontend/src/components/pki-syncs/forms/schemas.ts rename to frontend/src/components/pki-syncs/forms/schemas/base-pki-sync-schema.ts index 1c31c8c5a..f8c9d599f 100644 --- a/frontend/src/components/pki-syncs/forms/schemas.ts +++ b/frontend/src/components/pki-syncs/forms/schemas/base-pki-sync-schema.ts @@ -1,25 +1,9 @@ -import { z } from "zod"; +import { AnyZodObject, z } from "zod"; -import { PkiSync } from "@app/hooks/api/pkiSyncs"; - -export const PkiSyncFormSchema = z.object({ - name: z - .string() - .trim() - .min(1, "Name is required") - .max(255, "Name must be less than 255 characters"), - description: z.string().optional(), - destination: z.nativeEnum(PkiSync), - isAutoSyncEnabled: z.boolean().default(true), - subscriberId: z.string().min(1, "PKI Subscriber is required"), - connection: z.object({ - id: z.string().uuid("Invalid connection ID format"), - name: z.string().max(255, "Connection name must be less than 255 characters") - }), - destinationConfig: z.object({ - vaultBaseUrl: z.string().url("Valid URL is required") - }), - syncOptions: z.object({ +export const BasePkiSyncSchema = ( + additionalSyncOptions?: T +) => { + const baseSyncOptionsSchema = z.object({ canImportCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(false), certificateNameSchema: z @@ -53,22 +37,27 @@ export const PkiSyncFormSchema = z.object({ "Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." } ) - }) -}); + }); -export type TPkiSyncForm = z.infer; + const syncOptionsSchema = additionalSyncOptions + ? baseSyncOptionsSchema.merge(additionalSyncOptions) + : (baseSyncOptionsSchema as T extends AnyZodObject + ? z.ZodObject> + : typeof baseSyncOptionsSchema); -export const UpdatePkiSyncFormSchema = PkiSyncFormSchema.partial().merge( - z.object({ + return z.object({ name: z .string() .trim() .min(1, "Name is required") .max(255, "Name must be less than 255 characters"), - destination: z.nativeEnum(PkiSync), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().default(true), + subscriberId: z.string().min(1, "PKI Subscriber is required"), connection: z.object({ id: z.string().uuid("Invalid connection ID format"), name: z.string().max(255, "Connection name must be less than 255 characters") - }) - }) -); + }), + syncOptions: syncOptionsSchema + }); +}; diff --git a/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts b/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts new file mode 100644 index 000000000..6efa5e24d --- /dev/null +++ b/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +import { + AwsCertificateManagerPkiSyncDestinationSchema, + UpdateAwsCertificateManagerPkiSyncDestinationSchema +} from "./aws-certificate-manager-pki-sync-destination-schema"; +import { + AzureKeyVaultPkiSyncDestinationSchema, + UpdateAzureKeyVaultPkiSyncDestinationSchema +} from "./azure-key-vault-pki-sync-destination-schema"; + +const PkiSyncUnionSchema = z.discriminatedUnion("destination", [ + AzureKeyVaultPkiSyncDestinationSchema, + AwsCertificateManagerPkiSyncDestinationSchema +]); + +const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [ + UpdateAzureKeyVaultPkiSyncDestinationSchema, + UpdateAwsCertificateManagerPkiSyncDestinationSchema +]); + +export const PkiSyncFormSchema = PkiSyncUnionSchema; + +export const UpdatePkiSyncFormSchema = UpdatePkiSyncUnionSchema; + +export type TPkiSyncForm = z.infer; + +export type TUpdatePkiSyncForm = z.infer; diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index dcb11d873..5dd2ea4ce 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -80,6 +80,10 @@ const PROJECT_TYPE_MENU_ITEMS = [ { label: "Secret Scanning", value: ProjectType.SecretScanning + }, + { + label: "PAM", + value: ProjectType.PAM } ]; @@ -193,12 +197,12 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { errorText={error?.message} className="flex-1" > -
+
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
field.onChange(el.value)} diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx index 18feefa09..77fa687e7 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx @@ -1,7 +1,7 @@ import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay"; +import { TRedisCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/redis-credentials-rotation"; import { ViewRotationGeneratedCredentialsDisplay } from "./shared"; -import { TRedisCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/redis-credentials-rotation"; type Props = { generatedCredentialsResponse: TRedisCredentialsRotationGeneratedCredentialsResponse; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx index 0aeffef21..aed65a424 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx @@ -3,6 +3,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; import { FormControl, Input } from "@app/components/v2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + import { DEFAULT_PASSWORD_REQUIREMENTS } from "../schemas/shared"; export const RedisCredentialsRotationParametersFields = () => { @@ -18,7 +19,7 @@ export const RedisCredentialsRotationParametersFields = () => { ( = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields, diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index 05b6ad63c..e484a64b1 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -11,8 +11,8 @@ import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotation import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields"; import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields"; import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields"; -import { SqlCredentialsRotationReviewFields } from "./shared"; import { RedisCredentialsRotationReviewFields } from "./RedisCredentialsRotationReviewFields"; +import { SqlCredentialsRotationReviewFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields, diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 15338c48a..e05fd31f5 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -8,8 +8,8 @@ import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecret import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields"; import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields"; import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields"; -import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; import { RedisCredentialsRotationSecretsMappingFields } from "./RedisCredentialsRotationSecretsMappingFields"; +import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields, diff --git a/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx b/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx new file mode 100644 index 000000000..82ab06038 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/DuplicateDestinationConfirmationModal.tsx @@ -0,0 +1,57 @@ +import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + onConfirm: () => void; + isLoading?: boolean; + duplicateProjectId?: string; +}; + +export const DuplicateDestinationConfirmationModal = ({ + isOpen, + onOpenChange, + onConfirm, + isLoading, + duplicateProjectId +}: Props) => { + return ( + + +
+

+ Another secret sync in your organization is already configured with the same + destination. Proceeding may cause conflicts or overwrite existing data. +

+ {duplicateProjectId && ( +

+ Duplicate found in project ID:{" "} + + {duplicateProjectId} + +

+ )} +

Are you sure you want to continue?

+
+ +
+ + + + + + +
+
+
+ ); +}; diff --git a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx index dc2ab385a..3c0e4ea17 100644 --- a/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/EditSecretSyncForm.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -6,9 +6,14 @@ import { createNotification } from "@app/components/notifications"; import { SecretSyncEditFields } from "@app/components/secret-syncs/types"; import { Button, ModalClose } from "@app/components/v2"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; -import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs"; +import { + TSecretSync, + useCheckDuplicateDestination, + useUpdateSecretSync +} from "@app/hooks/api/secretSyncs"; import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields"; +import { DuplicateDestinationConfirmationModal } from "./DuplicateDestinationConfirmationModal"; import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas"; import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields"; import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields"; @@ -23,6 +28,8 @@ type Props = { export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => { const updateSecretSync = useUpdateSecretSync(); const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination]; + const [showDuplicateConfirmation, setShowDuplicateConfirmation] = useState(false); + const [pendingFormData, setPendingFormData] = useState(null); const formMethods = useForm({ resolver: zodResolver(UpdateSecretSyncFormSchema), @@ -35,29 +42,114 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => reValidateMode: "onChange" }); - const onSubmit = async ({ environment, connection, ...formData }: TSecretSyncForm) => { - try { - const updatedSecretSync = await updateSecretSync.mutateAsync({ - syncId: secretSync.id, - ...formData, - environment: environment?.slug, - connectionId: connection.id, - projectId: secretSync.projectId + const [destinationConfigToCheck, setDestinationConfigToCheck] = useState(null); + const [checkDuplicateEnabled, setCheckDuplicateEnabled] = useState(false); + const [storedDuplicateProjectId, setStoredDuplicateProjectId] = useState(); + + const { data: duplicateData, isLoading: isCheckingDuplicate } = useCheckDuplicateDestination( + secretSync.destination, + destinationConfigToCheck, + secretSync.projectId, + secretSync.id, + { enabled: checkDuplicateEnabled && Boolean(destinationConfigToCheck) } + ); + + const performUpdate = useCallback( + async (formData: TSecretSyncForm) => { + try { + const { environment, connection, ...updateData } = formData; + const updatedSecretSync = await updateSecretSync.mutateAsync({ + syncId: secretSync.id, + ...updateData, + environment: environment?.slug, + connectionId: connection.id, + projectId: secretSync.projectId + }); + + createNotification({ + text: `Successfully updated ${destinationName} Sync`, + type: "success" + }); + onComplete(updatedSecretSync); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to update ${destinationName} Sync`, + text: err.message, + type: "error" + }); + } + }, + [updateSecretSync, secretSync.id, secretSync.projectId, destinationName, onComplete] + ); + + useEffect(() => { + if (checkDuplicateEnabled && !isCheckingDuplicate && destinationConfigToCheck) { + if (duplicateData?.hasDuplicate) { + setStoredDuplicateProjectId(duplicateData.duplicateProjectId); + setShowDuplicateConfirmation(true); + } else if (pendingFormData) { + performUpdate(pendingFormData); + setPendingFormData(null); + } + setCheckDuplicateEnabled(false); + setDestinationConfigToCheck(null); + } + }, [ + checkDuplicateEnabled, + isCheckingDuplicate, + duplicateData?.hasDuplicate, + duplicateData?.duplicateProjectId, + destinationConfigToCheck, + pendingFormData, + performUpdate + ]); + + const normalizeConfig = (config: unknown): unknown => { + if (config === null || config === undefined || typeof config !== "object") { + return config; + } + + if (Array.isArray(config)) { + return config.map(normalizeConfig); + } + + const normalized: Record = {}; + Object.keys(config as Record) + .sort() + .forEach((key) => { + normalized[key] = normalizeConfig((config as Record)[key]); }); - createNotification({ - text: `Successfully updated ${destinationName} Sync`, - type: "success" - }); - onComplete(updatedSecretSync); - } catch (err: any) { - console.error(err); - createNotification({ - title: `Failed to update ${destinationName} Sync`, - text: err.message, - type: "error" - }); + return normalized; + }; + + const hasDestinationConfigChanged = (formData: TSecretSyncForm) => { + const originalConfig = normalizeConfig(secretSync.destinationConfig); + const currentConfig = normalizeConfig(formData.destinationConfig); + + return JSON.stringify(originalConfig) !== JSON.stringify(currentConfig); + }; + + const onSubmit = async (formData: TSecretSyncForm) => { + if (fields === SecretSyncEditFields.Destination && hasDestinationConfigChanged(formData)) { + setDestinationConfigToCheck(formData.destinationConfig); + setPendingFormData(formData); + setCheckDuplicateEnabled(true); + return; } + + await performUpdate(formData); + }; + + const handleConfirmDuplicate = async () => { + if (pendingFormData) { + await performUpdate(pendingFormData); + setPendingFormData(null); + } + setShowDuplicateConfirmation(false); + setCheckDuplicateEnabled(false); + setDestinationConfigToCheck(null); }; let Component: ReactNode; @@ -83,24 +175,41 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => formState: { isSubmitting, isDirty } } = formMethods; + const isLoading = isSubmitting || isCheckingDuplicate; + return ( -
- {Component} -
- - + + - - -
-
+
+ + + { + setShowDuplicateConfirmation(open); + if (!open) { + setStoredDuplicateProjectId(undefined); + } + }} + onConfirm={handleConfirmDuplicate} + isLoading={updateSecretSync.isPending} + duplicateProjectId={storedDuplicateProjectId} + /> + ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index eaeab2e66..84801b000 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -1,11 +1,14 @@ import { ReactNode } from "react"; import { useFormContext } from "react-hook-form"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { GenericFieldLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; import { Badge } from "@app/components/v2"; +import { useProject } from "@app/context"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; -import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreDestinationReviewFields, @@ -46,6 +49,7 @@ import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields"; export const SecretSyncReviewFields = () => { const { watch } = useFormContext(); + const { currentProject } = useProject(); let DestinationFieldsComponent: ReactNode; let AdditionalSyncOptionsFieldsComponent: ReactNode; @@ -63,6 +67,13 @@ export const SecretSyncReviewFields = () => { const destinationName = SECRET_SYNC_MAP[destination].name; + const { hasDuplicate, duplicateProjectId, isChecking } = useDuplicateDestinationCheck({ + destination, + projectId: currentProject?.id || "", + enabled: true, + destinationConfig: watch("destinationConfig") + }); + switch (destination) { case SecretSync.AWSParameterStore: DestinationFieldsComponent = ; @@ -173,9 +184,28 @@ export const SecretSyncReviewFields = () => {
-
+
Destination + {isChecking && Checking...}
+ {hasDuplicate && ( +
+
+ +
+

+ Another secret sync in your organization is already configured with the same + destination. This may lead to conflicts or unexpected behavior. +

+ {duplicateProjectId && ( +

+ Duplicate found in project ID: {duplicateProjectId} +

+ )} +
+
+
+ )}
{connection.name} {DestinationFieldsComponent} diff --git a/frontend/src/components/v2/HighlightText/HighlightText.tsx b/frontend/src/components/v2/HighlightText/HighlightText.tsx index 3ce7c8f19..c81dab2df 100644 --- a/frontend/src/components/v2/HighlightText/HighlightText.tsx +++ b/frontend/src/components/v2/HighlightText/HighlightText.tsx @@ -8,9 +8,24 @@ export const HighlightText = ({ highlightClassName?: string; }) => { if (!text) return null; + + const renderTextWithNewlines = (input: string, baseKeyPrefix: string = ""): React.ReactNode[] => { + if (!input) return []; + const lines = input.split("\n"); + return lines.flatMap((line, index) => { + const nodes: React.ReactNode[] = [line]; + if (index < lines.length - 1) { + nodes.push(
); + } + return nodes; + }); + }; + const searchTerm = highlight.toLowerCase().trim(); - if (!searchTerm) return {text}; + if (!searchTerm) { + return {renderTextWithNewlines(text, "full-text")}; + } const parts: React.ReactNode[] = []; let lastIndex = 0; @@ -20,12 +35,17 @@ export const HighlightText = ({ text.replace(regex, (match: string, offset: number) => { if (offset > lastIndex) { - parts.push({text.substring(lastIndex, offset)}); + const preMatchText = text.substring(lastIndex, offset); + parts.push( + + {renderTextWithNewlines(preMatchText, `pre-${lastIndex}`)} + + ); } parts.push( - {match} + {renderTextWithNewlines(match, `match-${offset}`)} ); @@ -35,7 +55,12 @@ export const HighlightText = ({ }); if (lastIndex < text.length) { - parts.push({text.substring(lastIndex)}); + const postMatchText = text.substring(lastIndex); + parts.push( + + {renderTextWithNewlines(postMatchText, `post-${lastIndex}`)} + + ); } return parts; diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index b1c87dc2d..2e835a1db 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -350,6 +350,24 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings" ) }, + Pam: { + AccountsPage: setRoute( + "/projects/pam/$projectId/accounts", + "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts" + ), + ResourcesPage: setRoute( + "/projects/pam/$projectId/resources", + "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources" + ), + SessionsPage: setRoute( + "/projects/pam/$projectId/sessions", + "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/" + ), + PamSessionByIDPage: setRoute( + "/projects/pam/$projectId/sessions/$sessionId", + "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId" + ) + }, Public: { ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"), ViewSecretRequestByIDPage: setRoute( diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 306d92808..d3019409f 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -187,6 +187,19 @@ export enum ProjectPermissionCommitsActions { PerformRollback = "perform-rollback" } +export enum ProjectPermissionPamAccountActions { + Access = "access", + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum ProjectPermissionPamSessionActions { + Read = "read" + // Terminate = "terminate" +} + export type IdentityManagementSubjectFields = { identityId: string; }; @@ -208,7 +221,8 @@ export type ConditionalProjectPermissionSubject = | ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.SecretEvents - | ProjectPermissionSub.AppConnections; + | ProjectPermissionSub.AppConnections + | ProjectPermissionSub.PamAccounts; export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { [PermissionConditionOperators.$EQ]: "equal to", @@ -289,7 +303,11 @@ export enum ProjectPermissionSub { SecretScanningFindings = "secret-scanning-findings", SecretScanningConfigs = "secret-scanning-configs", SecretEvents = "secret-events", - AppConnections = "app-connections" + AppConnections = "app-connections", + PamFolders = "pam-folders", + PamResources = "pam-resources", + PamAccounts = "pam-accounts", + PamSessions = "pam-sessions" } export type SecretSubjectFields = { @@ -350,6 +368,12 @@ export type PkiTemplateSubjectFields = { // (dangtony98): consider adding [commonName] as a subject field in the future }; +export type PamAccountSubjectFields = { + resourceName: string; + accountName: string; + accountPath: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -475,6 +499,16 @@ export type ProjectPermissionSet = | ProjectPermissionSub.AppConnections | (ForcedSubject & AppConnectionSubjectFields) ) - ]; + ] + | [ProjectPermissionActions, ProjectPermissionSub.PamFolders] + | [ProjectPermissionActions, ProjectPermissionSub.PamResources] + | [ + ProjectPermissionPamAccountActions, + ( + | ProjectPermissionSub.PamAccounts + | (ForcedSubject & PamAccountSubjectFields) + ) + ] + | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/helpers/auditLogStreams.ts b/frontend/src/helpers/auditLogStreams.ts index faa132dd2..ab216957c 100644 --- a/frontend/src/helpers/auditLogStreams.ts +++ b/frontend/src/helpers/auditLogStreams.ts @@ -12,7 +12,8 @@ export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record< [LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 }, [LogProvider.Custom]: { name: "Custom", icon: faCode }, [LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" }, - [LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 } + [LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 }, + [LogProvider.QRadar]: { name: "IBM QRadar", image: "IBM.png" } }; // Strictly for showing to the client in the front-end diff --git a/frontend/src/helpers/pkiSyncs.ts b/frontend/src/helpers/pkiSyncs.ts index 15031a198..d8c87853d 100644 --- a/frontend/src/helpers/pkiSyncs.ts +++ b/frontend/src/helpers/pkiSyncs.ts @@ -11,9 +11,14 @@ export const PKI_SYNC_MAP: Record< [PkiSync.AzureKeyVault]: { name: "Azure Key Vault", image: "Microsoft Azure.png" + }, + [PkiSync.AwsCertificateManager]: { + name: "AWS Certificate Manager", + image: "Amazon Web Services.png" } }; export const PKI_SYNC_CONNECTION_MAP: Record = { - [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault + [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [PkiSync.AwsCertificateManager]: AppConnection.AWS }; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 6227b9696..843ded3e3 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -82,6 +82,8 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[] return "/projects/cert-management/$projectId/subscribers" as const; case ProjectType.SecretScanning: return `/projects/${type}/$projectId/data-sources` as const; + case ProjectType.PAM: + return `/projects/${type}/$projectId/accounts` as const; default: return `/projects/${type}/$projectId/overview` as const; } @@ -93,7 +95,8 @@ export const getProjectTitle = (type: ProjectType) => { [ProjectType.KMS]: "Key Management", [ProjectType.CertificateManager]: "Cert Management", [ProjectType.SSH]: "SSH", - [ProjectType.SecretScanning]: "Secret Scanning" + [ProjectType.SecretScanning]: "Secret Scanning", + [ProjectType.PAM]: "PAM" }; return titleConvert[type]; }; @@ -104,7 +107,8 @@ export const getProjectLottieIcon = (type: ProjectType) => { [ProjectType.KMS]: "unlock", [ProjectType.CertificateManager]: "note", [ProjectType.SSH]: "terminal", - [ProjectType.SecretScanning]: "secret-scan" + [ProjectType.SecretScanning]: "secret-scan", + [ProjectType.PAM]: "groups" }; return titleConvert[type]; }; diff --git a/frontend/src/hooks/api/auditLogStreams/enums.ts b/frontend/src/hooks/api/auditLogStreams/enums.ts index ebef18574..71d11500a 100644 --- a/frontend/src/hooks/api/auditLogStreams/enums.ts +++ b/frontend/src/hooks/api/auditLogStreams/enums.ts @@ -3,5 +3,6 @@ export enum LogProvider { Cribl = "cribl", Custom = "custom", Datadog = "datadog", - Splunk = "splunk" + Splunk = "splunk", + QRadar = "qradar" } diff --git a/frontend/src/hooks/api/auditLogStreams/types/index.ts b/frontend/src/hooks/api/auditLogStreams/types/index.ts index f780510c2..6efd5a77d 100644 --- a/frontend/src/hooks/api/auditLogStreams/types/index.ts +++ b/frontend/src/hooks/api/auditLogStreams/types/index.ts @@ -3,6 +3,7 @@ import { TAzureProviderLogStream } from "./providers/azure-provider"; import { TCriblProviderLogStream } from "./providers/cribl-provider"; import { TCustomProviderLogStream } from "./providers/custom-provider"; import { TDatadogProviderLogStream } from "./providers/datadog-provider"; +import { TQRadarProviderLogStream } from "./providers/qradar-provider"; import { TSplunkProviderLogStream } from "./providers/splunk-provider"; export type TAuditLogStream = @@ -18,6 +19,7 @@ export type TAuditLogStreamProviderMap = { [LogProvider.Custom]: TCustomProviderLogStream; [LogProvider.Datadog]: TDatadogProviderLogStream; [LogProvider.Splunk]: TSplunkProviderLogStream; + [LogProvider.QRadar]: TQRadarProviderLogStream; }; export type TCreateAuditLogStreamDTO = Pick; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/qradar-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/qradar-provider.ts new file mode 100644 index 000000000..d97226f4a --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/qradar-provider.ts @@ -0,0 +1,7 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TQRadarProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.QRadar; + // credentials: {}; +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index a36b4a37a..970e0592d 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -1,3 +1,4 @@ +import { ProjectType } from "../projects/types"; import { EventType, UserAgentType } from "./enums"; export const secretEvents: EventType[] = [ @@ -246,7 +247,26 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_ORG_ROLE]: "Create Org Role", [EventType.UPDATE_ORG_ROLE]: "Update Org Role", - [EventType.DELETE_ORG_ROLE]: "Delete Org Role" + [EventType.DELETE_ORG_ROLE]: "Delete Org Role", + + [EventType.PAM_SESSION_START]: "PAM Session Start", + [EventType.PAM_SESSION_LOGS_UPDATE]: "PAM Session Logs Update", + [EventType.PAM_SESSION_END]: "PAM Session End", + [EventType.PAM_SESSION_GET]: "PAM Session Get", + [EventType.PAM_SESSION_LIST]: "PAM Session List", + [EventType.PAM_FOLDER_CREATE]: "PAM Folder Create", + [EventType.PAM_FOLDER_UPDATE]: "PAM Folder Update", + [EventType.PAM_FOLDER_DELETE]: "PAM Folder Delete", + [EventType.PAM_ACCOUNT_LIST]: "PAM Account List", + [EventType.PAM_ACCOUNT_ACCESS]: "PAM Account Access", + [EventType.PAM_ACCOUNT_CREATE]: "PAM Account Create", + [EventType.PAM_ACCOUNT_UPDATE]: "PAM Account Update", + [EventType.PAM_ACCOUNT_DELETE]: "PAM Account Delete", + [EventType.PAM_RESOURCE_LIST]: "PAM Resource List", + [EventType.PAM_RESOURCE_GET]: "PAM Resource Get", + [EventType.PAM_RESOURCE_CREATE]: "PAM Resource Create", + [EventType.PAM_RESOURCE_UPDATE]: "PAM Resource Update", + [EventType.PAM_RESOURCE_DELETE]: "PAM Resource Delete" }; export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { @@ -258,3 +278,35 @@ export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { [UserAgentType.PYTHON_SDK]: "InfisicalPythonSDK", [UserAgentType.OTHER]: "Other" }; + +const sharedProjectEvents = [ + EventType.ADD_PROJECT_MEMBER, + EventType.REMOVE_PROJECT_MEMBER, + EventType.CREATE_PROJECT_ROLE, + EventType.UPDATE_PROJECT_ROLE, + EventType.DELETE_PROJECT_ROLE +]; + +export const projectToEventsMap: Partial> = { + [ProjectType.PAM]: [ + ...sharedProjectEvents, + EventType.PAM_SESSION_START, + EventType.PAM_SESSION_LOGS_UPDATE, + EventType.PAM_SESSION_END, + EventType.PAM_SESSION_GET, + EventType.PAM_SESSION_LIST, + EventType.PAM_FOLDER_CREATE, + EventType.PAM_FOLDER_UPDATE, + EventType.PAM_FOLDER_DELETE, + EventType.PAM_ACCOUNT_LIST, + EventType.PAM_ACCOUNT_ACCESS, + EventType.PAM_ACCOUNT_CREATE, + EventType.PAM_ACCOUNT_UPDATE, + EventType.PAM_ACCOUNT_DELETE, + EventType.PAM_RESOURCE_LIST, + EventType.PAM_RESOURCE_GET, + EventType.PAM_RESOURCE_CREATE, + EventType.PAM_RESOURCE_UPDATE, + EventType.PAM_RESOURCE_DELETE + ] +}; diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 94c41fd11..b10fcb60a 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -240,5 +240,24 @@ export enum EventType { CREATE_ORG_ROLE = "create-org-role", UPDATE_ORG_ROLE = "update-org-role", - DELETE_ORG_ROLE = "delete-org-role" + DELETE_ORG_ROLE = "delete-org-role", + + PAM_SESSION_START = "pam-session-start", + PAM_SESSION_LOGS_UPDATE = "pam-session-logs-update", + PAM_SESSION_END = "pam-session-end", + PAM_SESSION_GET = "pam-session-get", + PAM_SESSION_LIST = "pam-session-list", + PAM_FOLDER_CREATE = "pam-folder-create", + PAM_FOLDER_UPDATE = "pam-folder-update", + PAM_FOLDER_DELETE = "pam-folder-delete", + PAM_ACCOUNT_LIST = "pam-account-list", + PAM_ACCOUNT_ACCESS = "pam-account-access", + PAM_ACCOUNT_CREATE = "pam-account-create", + PAM_ACCOUNT_UPDATE = "pam-account-update", + PAM_ACCOUNT_DELETE = "pam-account-delete", + PAM_RESOURCE_LIST = "pam-resource-list", + PAM_RESOURCE_GET = "pam-resource-get", + PAM_RESOURCE_CREATE = "pam-resource-create", + PAM_RESOURCE_UPDATE = "pam-resource-update", + PAM_RESOURCE_DELETE = "pam-resource-delete" } diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts new file mode 100644 index 000000000..b6e5ce64c --- /dev/null +++ b/frontend/src/hooks/api/pam/enums.ts @@ -0,0 +1,13 @@ +export enum PamResourceType { + Postgres = "postgres", + RDP = "rdp", + SSH = "ssh", + Kubernetes = "kubernetes" +} + +export enum PamSessionStatus { + Starting = "starting", + Active = "active", + Ended = "ended", + Terminated = "terminated" +} diff --git a/frontend/src/hooks/api/pam/index.ts b/frontend/src/hooks/api/pam/index.ts new file mode 100644 index 000000000..dda90a234 --- /dev/null +++ b/frontend/src/hooks/api/pam/index.ts @@ -0,0 +1,5 @@ +export * from "./enums"; +export * from "./maps"; +export * from "./mutations"; +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/pam/maps.ts b/frontend/src/hooks/api/pam/maps.ts new file mode 100644 index 000000000..a27507d7d --- /dev/null +++ b/frontend/src/hooks/api/pam/maps.ts @@ -0,0 +1,11 @@ +import { PamResourceType } from "./enums"; + +export const PAM_RESOURCE_TYPE_MAP: Record< + PamResourceType, + { name: string; image: string; size?: number } +> = { + [PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" }, + [PamResourceType.RDP]: { name: "RDP", image: "RDP.png" }, + [PamResourceType.SSH]: { name: "SSH", image: "SSH.png" }, + [PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" } +}; diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx new file mode 100644 index 000000000..99a89b425 --- /dev/null +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -0,0 +1,169 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { pamKeys } from "./queries"; +import { + TCreatePamAccountDTO, + TCreatePamFolderDTO, + TCreatePamResourceDTO, + TDeletePamAccountDTO, + TDeletePamFolderDTO, + TDeletePamResourceDTO, + TPamAccount, + TPamFolder, + TPamResource, + TUpdatePamAccountDTO, + TUpdatePamFolderDTO, + TUpdatePamResourceDTO +} from "./types"; + +// Resources +export const useCreatePamResource = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ resourceType, ...params }: TCreatePamResourceDTO) => { + const { data } = await apiRequest.post<{ resource: TPamResource }>( + `/api/v1/pam/resources/${resourceType}`, + params + ); + + return data.resource; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + } + }); +}; + +export const useUpdatePamResource = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ resourceId, resourceType, ...params }: TUpdatePamResourceDTO) => { + const { data } = await apiRequest.patch<{ resource: TPamResource }>( + `/api/v1/pam/resources/${resourceType}/${resourceId}`, + params + ); + + return data.resource; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + } + }); +}; + +export const useDeletePamResource = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ resourceId, resourceType }: TDeletePamResourceDTO) => { + const { data } = await apiRequest.delete<{ resource: TPamResource }>( + `/api/v1/pam/resources/${resourceType}/${resourceId}` + ); + + return data.resource; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + } + }); +}; + +// Accounts +export const useCreatePamAccount = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ resourceType, ...params }: TCreatePamAccountDTO) => { + const { data } = await apiRequest.post<{ account: TPamAccount }>( + `/api/v1/pam/accounts/${resourceType}`, + params + ); + + return data.account; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + } + }); +}; + +export const useUpdatePamAccount = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ resourceType, accountId, ...params }: TUpdatePamAccountDTO) => { + const { data } = await apiRequest.patch<{ account: TPamAccount }>( + `/api/v1/pam/accounts/${resourceType}/${accountId}`, + params + ); + + return data.account; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + } + }); +}; + +export const useDeletePamAccount = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ resourceType, accountId }: TDeletePamAccountDTO) => { + const { data } = await apiRequest.delete<{ account: TPamAccount }>( + `/api/v1/pam/accounts/${resourceType}/${accountId}` + ); + + return data.account; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + } + }); +}; + +// Folders +export const useCreatePamFolder = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (params: TCreatePamFolderDTO) => { + const { data } = await apiRequest.post<{ folder: TPamFolder }>("/api/v1/pam/folders", params); + + return data.folder; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + } + }); +}; + +export const useUpdatePamFolder = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ folderId, ...params }: TUpdatePamFolderDTO) => { + const { data } = await apiRequest.patch<{ folder: TPamFolder }>( + `/api/v1/pam/folders/${folderId}`, + params + ); + + return data.folder; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + } + }); +}; + +export const useDeletePamFolder = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ folderId }: TDeletePamFolderDTO) => { + const { data } = await apiRequest.delete<{ folder: TPamFolder }>( + `/api/v1/pam/folders/${folderId}` + ); + + return data.folder; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + } + }); +}; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx new file mode 100644 index 000000000..288d65ab9 --- /dev/null +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -0,0 +1,138 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TPamResourceOption } from "./types/resource-options"; +import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; + +export const pamKeys = { + all: ["pam"] as const, + resource: () => [...pamKeys.all, "resource"] as const, + account: () => [...pamKeys.all, "account"] as const, + session: () => [...pamKeys.all, "session"] as const, + listResourceOptions: () => [...pamKeys.resource(), "options"] as const, + listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId], + listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], + getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], + listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] +}; + +// Resources +export const useListPamResourceOptions = ( + options?: Omit< + UseQueryOptions< + TPamResourceOption[], + unknown, + TPamResourceOption[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: pamKeys.listResourceOptions(), + queryFn: async () => { + const { data } = await apiRequest.get<{ resourceOptions: TPamResourceOption[] }>( + "/api/v1/pam/resources/options" + ); + + return data.resourceOptions; + }, + ...options + }); +}; + +export const useListPamResources = ( + projectId: string, + options?: Omit< + UseQueryOptions< + TPamResource[], + unknown, + TPamResource[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: pamKeys.listResources(projectId), + queryFn: async () => { + const { data } = await apiRequest.get<{ resources: TPamResource[] }>( + "/api/v1/pam/resources", + { params: { projectId } } + ); + + return data.resources; + }, + ...options + }); +}; + +// Accounts +export const useListPamAccounts = ( + projectId: string, + options?: Omit< + UseQueryOptions< + { accounts: TPamAccount[]; folders: TPamFolder[] }, + unknown, + { accounts: TPamAccount[]; folders: TPamFolder[] }, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: pamKeys.listAccounts(projectId), + queryFn: async () => { + const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>( + "/api/v1/pam/accounts", + { params: { projectId } } + ); + + return data; + }, + ...options + }); +}; + +// Sessions +export const useGetPamSessionById = ( + sessionId: string, + options?: Omit< + UseQueryOptions>, + "queryKey" | "queryFn" | "enabled" + > +) => { + return useQuery({ + queryKey: pamKeys.getSession(sessionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ session: TPamSession }>( + `/api/v1/pam/sessions/${sessionId}` + ); + + return data.session; + }, + enabled: !!sessionId, + ...options + }); +}; + +export const useListPamSessions = ( + projectId: string, + options?: Omit< + UseQueryOptions>, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: pamKeys.listSessions(projectId), + queryFn: async () => { + const { data } = await apiRequest.get<{ sessions: TPamSession[] }>("/api/v1/pam/sessions", { + params: { projectId } + }); + + return data.sessions; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/pam/types/base-account.ts b/frontend/src/hooks/api/pam/types/base-account.ts new file mode 100644 index 000000000..9f45b1a4a --- /dev/null +++ b/frontend/src/hooks/api/pam/types/base-account.ts @@ -0,0 +1,17 @@ +import { PamResourceType } from "../enums"; + +export interface TBasePamAccount { + id: string; + projectId: string; + folderId?: string | null; + resourceId: string; + resource: { + id: string; + name: string; + resourceType: PamResourceType; + }; + name: string; + description?: string | null; + createdAt: string; + updatedAt: string; +} diff --git a/frontend/src/hooks/api/pam/types/base-resource.ts b/frontend/src/hooks/api/pam/types/base-resource.ts new file mode 100644 index 000000000..763e564ad --- /dev/null +++ b/frontend/src/hooks/api/pam/types/base-resource.ts @@ -0,0 +1,8 @@ +export interface TBasePamResource { + id: string; + projectId: string; + name: string; + gatewayId: string; + createdAt: string; + updatedAt: string; +} diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts new file mode 100644 index 000000000..1b4acf6d4 --- /dev/null +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -0,0 +1,95 @@ +import { PamResourceType, PamSessionStatus } from "../enums"; +import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; + +export * from "./postgres-resource"; + +export type TPamResource = TPostgresResource; + +export type TPamAccount = TPostgresAccount; + +export type TPamFolder = { + id: string; + projectId: string; + parentId?: string | null; + name: string; + description?: string | null; + createdAt: string; + updatedAt: string; +}; + +export type TPamSession = { + id: string; + projectId: string; + accountId?: string | null; + resourceType: PamResourceType; + resourceName: string; + accountName: string; + userId?: string | null; + actorName: string; + actorEmail: string; + actorIp: string; + actorUserAgent: string; + status: PamSessionStatus; + expiresAt?: string | null; + startedAt?: string | null; + endedAt?: string | null; + createdAt: string; + updatedAt: string; + commandLogs: { + input: string; + output: string; + timestamp: string; + }[]; +}; + +// Resource DTOs +export type TCreatePamResourceDTO = Pick< + TPamResource, + "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" +>; + +export type TUpdatePamResourceDTO = Partial< + Pick +> & { + resourceId: string; + resourceType: PamResourceType; +}; + +export type TDeletePamResourceDTO = { + resourceId: string; + resourceType: PamResourceType; +}; + +// Account DTOs +export type TCreatePamAccountDTO = Pick< + TPamAccount, + "name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId" +> & { + resourceType: PamResourceType; +}; + +export type TUpdatePamAccountDTO = Partial< + Pick +> & { + accountId: string; + resourceType: PamResourceType; +}; + +export type TDeletePamAccountDTO = { + accountId: string; + resourceType: PamResourceType; +}; + +// Folder DTOs +export type TCreatePamFolderDTO = Pick< + TPamFolder, + "name" | "description" | "parentId" | "projectId" +>; + +export type TUpdatePamFolderDTO = Partial> & { + folderId: string; +}; + +export type TDeletePamFolderDTO = { + folderId: string; +}; diff --git a/frontend/src/hooks/api/pam/types/postgres-resource.ts b/frontend/src/hooks/api/pam/types/postgres-resource.ts new file mode 100644 index 000000000..513610be1 --- /dev/null +++ b/frontend/src/hooks/api/pam/types/postgres-resource.ts @@ -0,0 +1,14 @@ +import { PamResourceType } from "../enums"; +import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource"; +import { TBasePamAccount } from "./base-account"; +import { TBasePamResource } from "./base-resource"; + +// Resources +export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & { + connectionDetails: TBaseSqlConnectionDetails; +}; + +// Accounts +export type TPostgresAccount = TBasePamAccount & { + credentials: TBaseSqlCredentials; +}; diff --git a/frontend/src/hooks/api/pam/types/resource-options.ts b/frontend/src/hooks/api/pam/types/resource-options.ts new file mode 100644 index 000000000..4efb9e3b5 --- /dev/null +++ b/frontend/src/hooks/api/pam/types/resource-options.ts @@ -0,0 +1,11 @@ +import { PamResourceType } from "../enums"; + +export type TPamResourceOptionBase = { + name: string; +}; + +export type TPostgresResourceOption = TPamResourceOptionBase & { + resource: PamResourceType.Postgres; +}; + +export type TPamResourceOption = TPostgresResourceOption; diff --git a/frontend/src/hooks/api/pam/types/shared/sql-resource.ts b/frontend/src/hooks/api/pam/types/shared/sql-resource.ts new file mode 100644 index 000000000..1122fa5ed --- /dev/null +++ b/frontend/src/hooks/api/pam/types/shared/sql-resource.ts @@ -0,0 +1,12 @@ +export type TBaseSqlConnectionDetails = { + host: string; + port: number; + database: string; + sslEnabled: boolean; + sslRejectUnauthorized: boolean; +}; + +export type TBaseSqlCredentials = { + username: string; + password: string; +}; diff --git a/frontend/src/hooks/api/pkiSyncs/enums.ts b/frontend/src/hooks/api/pkiSyncs/enums.ts index 25080a1ae..507f3336c 100644 --- a/frontend/src/hooks/api/pkiSyncs/enums.ts +++ b/frontend/src/hooks/api/pkiSyncs/enums.ts @@ -1,5 +1,6 @@ export enum PkiSync { - AzureKeyVault = "azure-key-vault" + AzureKeyVault = "azure-key-vault", + AwsCertificateManager = "aws-certificate-manager" } export enum PkiSyncStatus { diff --git a/frontend/src/hooks/api/pkiSyncs/types/aws-certificate-manager-sync.ts b/frontend/src/hooks/api/pkiSyncs/types/aws-certificate-manager-sync.ts new file mode 100644 index 000000000..e31ff1378 --- /dev/null +++ b/frontend/src/hooks/api/pkiSyncs/types/aws-certificate-manager-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { PkiSync } from "../enums"; +import { TRootPkiSync } from "./common"; + +export type TAwsCertificateManagerPkiSync = TRootPkiSync & { + destination: PkiSync.AwsCertificateManager; + destinationConfig: { + region: string; + }; + connection: { + app: AppConnection.AWS; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/pkiSyncs/types/index.ts b/frontend/src/hooks/api/pkiSyncs/types/index.ts index baf8a693b..899217f52 100644 --- a/frontend/src/hooks/api/pkiSyncs/types/index.ts +++ b/frontend/src/hooks/api/pkiSyncs/types/index.ts @@ -1,6 +1,6 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs"; -import { DiscriminativePick } from "@app/types"; +import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync"; import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync"; export type TPkiSyncOption = { @@ -16,27 +16,38 @@ export type TPkiSyncOption = { minCertificateNameLength?: number; }; -export type TPkiSync = TAzureKeyVaultPkiSync; +export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync; export type TListPkiSyncs = { pkiSyncs: TPkiSync[] }; export type TListPkiSyncOptions = { pkiSyncOptions: TPkiSyncOption[] }; -export type TCreatePkiSyncDTO = DiscriminativePick< - TPkiSync, - | "name" - | "destinationConfig" - | "description" - | "connectionId" - | "syncOptions" - | "destination" - | "isAutoSyncEnabled" -> & { subscriberId?: string; projectId: string }; +type TCreatePkiSyncDTOBase = { + name: string; + description?: string; + connectionId: string; + syncOptions: { + canImportCertificates: boolean; + canRemoveCertificates: boolean; + certificateNamePrefix?: string; + certificateNameSchema?: string; + }; + isAutoSyncEnabled: boolean; + subscriberId?: string; + projectId: string; +}; + +export type TCreatePkiSyncDTO = TCreatePkiSyncDTOBase & { + destination: PkiSync; + destinationConfig: { + vaultBaseUrl?: string; + region?: string; + }; +}; export type TUpdatePkiSyncDTO = Partial> & { syncId: string; projectId: string; - destination: PkiSync; }; export type TDeletePkiSyncDTO = { @@ -63,4 +74,6 @@ export type TTriggerPkiSyncRemoveCertificatesDTO = { projectId: string; }; +export * from "./aws-certificate-manager-sync"; +export * from "./azure-key-vault-sync"; export * from "./common"; diff --git a/frontend/src/hooks/api/projects/types.ts b/frontend/src/hooks/api/projects/types.ts index 59b242d05..977afd179 100644 --- a/frontend/src/hooks/api/projects/types.ts +++ b/frontend/src/hooks/api/projects/types.ts @@ -13,7 +13,8 @@ export enum ProjectType { CertificateManager = "cert-manager", KMS = "kms", SSH = "ssh", - SecretScanning = "secret-scanning" + SecretScanning = "secret-scanning", + PAM = "pam" } export enum ProjectUserMembershipTemporaryMode { diff --git a/frontend/src/hooks/api/secretSyncs/index.ts b/frontend/src/hooks/api/secretSyncs/index.ts index f49a872a5..0050bad8d 100644 --- a/frontend/src/hooks/api/secretSyncs/index.ts +++ b/frontend/src/hooks/api/secretSyncs/index.ts @@ -2,3 +2,4 @@ export * from "./enums"; export * from "./mutations"; export * from "./queries"; export * from "./types"; +export * from "./useDuplicateDestinationCheck"; diff --git a/frontend/src/hooks/api/secretSyncs/queries.tsx b/frontend/src/hooks/api/secretSyncs/queries.tsx index bb0787a7b..250d02ff8 100644 --- a/frontend/src/hooks/api/secretSyncs/queries.tsx +++ b/frontend/src/hooks/api/secretSyncs/queries.tsx @@ -14,7 +14,15 @@ export const secretSyncKeys = { options: () => [...secretSyncKeys.all, "options"] as const, list: (projectId: string) => [...secretSyncKeys.all, "list", projectId] as const, byId: (destination: SecretSync, syncId: string) => - [...secretSyncKeys.all, destination, "by-id", syncId] as const + [...secretSyncKeys.all, destination, "by-id", syncId] as const, + duplicateCheck: (destination: SecretSync, destinationConfig: unknown, excludeSyncId?: string) => + [ + ...secretSyncKeys.all, + destination, + "duplicate-check", + destinationConfig, + excludeSyncId + ] as const }; export const useSecretSyncOptions = ( @@ -88,3 +96,37 @@ export const useGetSecretSync = ( ...options }); }; + +export const useCheckDuplicateDestination = ( + destination: SecretSync, + destinationConfig: unknown, + projectId: string, + excludeSyncId?: string, + options?: Omit< + UseQueryOptions< + { hasDuplicate: boolean; duplicateProjectId?: string }, + unknown, + { hasDuplicate: boolean; duplicateProjectId?: string }, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: secretSyncKeys.duplicateCheck(destination, destinationConfig, excludeSyncId), + queryFn: async () => { + const { data } = await apiRequest.post<{ + hasDuplicate: boolean; + duplicateProjectId?: string; + }>(`/api/v1/secret-syncs/${destination}/check-destination`, { + destinationConfig, + excludeSyncId, + projectId + }); + + return data; + }, + enabled: Boolean(destinationConfig) && Object.keys(destinationConfig || {}).length > 0, + ...options + }); +}; diff --git a/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts b/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts new file mode 100644 index 000000000..97985fba9 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/useDuplicateDestinationCheck.ts @@ -0,0 +1,51 @@ +import { useMemo } from "react"; + +import { SecretSync, useCheckDuplicateDestination } from "@app/hooks/api/secretSyncs"; + +type UseDuplicateDestinationCheckProps = { + destination: SecretSync; + projectId: string; + excludeSyncId?: string; + enabled?: boolean; + destinationConfig?: unknown; +}; + +export const useDuplicateDestinationCheck = ({ + destination, + projectId, + excludeSyncId, + enabled = true, + destinationConfig +}: UseDuplicateDestinationCheckProps) => { + const hasValidConfig = useMemo(() => { + if (!destinationConfig || typeof destinationConfig !== "object") return false; + + const values = Object.values(destinationConfig); + return ( + values.length > 0 && + values.some((value) => value !== null && value !== undefined && value !== "") + ); + }, [destinationConfig]); + + const shouldCheck = enabled && hasValidConfig; + + const { + data: duplicateData, + isLoading, + error, + refetch + } = useCheckDuplicateDestination(destination, destinationConfig, projectId, excludeSyncId, { + enabled: shouldCheck, + staleTime: 0, + gcTime: 0 + }); + + return { + hasDuplicate: shouldCheck ? Boolean(duplicateData?.hasDuplicate) : false, + duplicateProjectId: duplicateData?.duplicateProjectId, + isChecking: shouldCheck && isLoading, + hasError: Boolean(error), + hasValidConfig, + refetch + }; +}; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index de960cd04..5c1986b21 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -337,7 +337,7 @@ export const useMoveSecrets = ({ destinationSecretPath, secretIds, shouldOverwrite, - projectId + projectSlug }) => { const { data } = await apiRequest.post<{ isSourceUpdated: boolean; @@ -349,7 +349,7 @@ export const useMoveSecrets = ({ destinationSecretPath, secretIds, shouldOverwrite, - projectId + projectSlug }); return data; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 68a3852c0..6dc59762d 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -239,6 +239,7 @@ export type TDeleteSecretBatchDTO = { export type TMoveSecretsDTO = { projectId: string; + projectSlug: string; sourceEnvironment: string; sourceSecretPath: string; destinationEnvironment: string; diff --git a/frontend/src/hooks/api/ssoConfig/queries.tsx b/frontend/src/hooks/api/ssoConfig/queries.tsx index 17dda5c1d..3ee5ec1b1 100644 --- a/frontend/src/hooks/api/ssoConfig/queries.tsx +++ b/frontend/src/hooks/api/ssoConfig/queries.tsx @@ -34,7 +34,8 @@ export const useCreateSSOConfig = () => { isActive, entryPoint, issuer, - cert + cert, + enableGroupSync }: { organizationId: string; authProvider: string; @@ -42,6 +43,7 @@ export const useCreateSSOConfig = () => { entryPoint: string; issuer: string; cert: string; + enableGroupSync?: boolean; }) => { const { data } = await apiRequest.post("/api/v1/sso/config", { organizationId, @@ -49,7 +51,8 @@ export const useCreateSSOConfig = () => { isActive, entryPoint, issuer, - cert + cert, + ...(enableGroupSync !== undefined ? { enableGroupSync } : {}) }); return data; @@ -69,7 +72,8 @@ export const useUpdateSSOConfig = () => { isActive, entryPoint, issuer, - cert + cert, + enableGroupSync }: { organizationId: string; authProvider?: string; @@ -77,6 +81,7 @@ export const useUpdateSSOConfig = () => { entryPoint?: string; issuer?: string; cert?: string; + enableGroupSync?: boolean; }) => { const { data } = await apiRequest.patch("/api/v1/sso/config", { organizationId, @@ -84,7 +89,8 @@ export const useUpdateSSOConfig = () => { ...(isActive !== undefined ? { isActive } : {}), ...(entryPoint !== undefined ? { entryPoint } : {}), ...(issuer !== undefined ? { issuer } : {}), - ...(cert !== undefined ? { cert } : {}) + ...(cert !== undefined ? { cert } : {}), + ...(enableGroupSync !== undefined ? { enableGroupSync } : {}) }); return data; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index f4f3dfe34..ede2f8cf1 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -58,4 +58,5 @@ export type SubscriptionPlan = { cardDeclined?: boolean; cardDeclinedReason?: string; machineIdentityAuthTemplates: boolean; + pam: boolean; }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 22d4f39d8..9c2d00b05 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -87,6 +87,8 @@ export const useDeleteMe = () => { localStorage.removeItem("PRIVATE_KEY"); localStorage.removeItem("orgData.id"); + setAuthToken(""); + queryClient.clear(); } }); diff --git a/frontend/src/layouts/PamLayout/PamLayout.tsx b/frontend/src/layouts/PamLayout/PamLayout.tsx new file mode 100644 index 000000000..dd268cce6 --- /dev/null +++ b/frontend/src/layouts/PamLayout/PamLayout.tsx @@ -0,0 +1,195 @@ +import { useEffect } from "react"; +import { + faBook, + faBoxOpen, + faCog, + faDisplay, + faHome, + faUser, + faUsers +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link, Outlet } from "@tanstack/react-router"; +import { motion } from "framer-motion"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { useProject, useProjectPermission, useSubscription } from "@app/context"; +import { usePopUp } from "@app/hooks"; + +import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; + +export const PamLayout = () => { + const { currentProject } = useProject(); + const { subscription } = useSubscription(); + const { assumedPrivilegeDetails } = useProjectPermission(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]); + + useEffect(() => { + if (subscription && !subscription.pam) { + handlePopUpOpen("upgradePlan"); + } + }, [subscription]); + + return ( + <> +
+
+ +
+ + +
+ {assumedPrivilegeDetails && } + +
+
+
+ { + handlePopUpToggle("upgradePlan", isOpen); + }} + text="You can use PAM if you switch to a paid Infisical plan." + /> + + ); +}; diff --git a/frontend/src/layouts/PamLayout/index.tsx b/frontend/src/layouts/PamLayout/index.tsx new file mode 100644 index 000000000..eedf27b21 --- /dev/null +++ b/frontend/src/layouts/PamLayout/index.tsx @@ -0,0 +1 @@ +export { PamLayout } from "./PamLayout"; diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx index bb96e72f0..491b95d25 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx @@ -164,7 +164,10 @@ export const PkiSyncRow = ({
{subscriberId ? ( - + ) : ( diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index bb331bf3d..42b00f44a 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -40,6 +40,7 @@ import { OktaConnectionForm } from "./OktaConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { RailwayConnectionForm } from "./RailwayConnectionForm"; +import { RedisConnectionForm } from "./RedisConnectionForm"; import { RenderConnectionForm } from "./RenderConnectionForm"; import { SupabaseConnectionForm } from "./SupabaseConnectionForm"; import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; @@ -47,7 +48,6 @@ import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; import { ZabbixConnectionForm } from "./ZabbixConnectionForm"; -import { RedisConnectionForm } from "./RedisConnectionForm"; type FormProps = { onComplete: (appConnection: TAppConnection) => void; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx index 602b9486c..0de9074bc 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx @@ -1,9 +1,11 @@ import { useState } from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tab } from "@headlessui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { Tab } from "@headlessui/react"; import { Button, FormControl, @@ -24,8 +26,6 @@ import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields } from "./GenericAppConnectionFields"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; type Props = { appConnection?: TRedisConnection; @@ -123,175 +123,173 @@ export const RedisConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> - <> - - - - `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ - selected - ? "border-b-2 border-mineshaft-300 text-mineshaft-200" - : "text-bunker-300" - }` - } - > - Configuration - - - `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ - selected - ? "border-b-2 border-mineshaft-300 text-mineshaft-200" - : "text-bunker-300" - }` - } - > - SSL ({sslEnabled ? "Enabled" : "Disabled"}) - - - - -
- ( - - - - )} - /> - ( - - - - )} - /> -
-
- ( - - - - )} - /> - ( - - onChange(e.target.value)} - /> - - )} - /> -
-
- + + + + `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + Configuration + + + `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + SSL ({sslEnabled ? "Enabled" : "Disabled"}) + + + + +
( - - - Enable SSL - + render={({ field, fieldState: { error } }) => ( + + )} /> ( + + + + )} + /> +
+
+ ( -